# Adapty Documentation (Full Content) > Complete documentation content across all platforms. Locale: vi Generated on: 2026-07-24T13:01:43.702Z --- # ANDROID - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.339Z Total files: 41 --- # File: sdk-installation-android --- --- title: "Cài đặt và cấu hình Android SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên Android cho các ứng dụng dựa trên gói đăng ký." --- SDK Adapty bao gồm hai module chính để tích hợp liền mạch vào ứng dụng di động của bạn: - **Core Adapty**: SDK cốt lõi này bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), công cụ no-code thân thiện với người dùng để tạo paywall đa nền tảng một cách dễ dàng. AdaptyUI được kích hoạt tự động cùng với module cốt lõi. :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Yêu cầu \{#requirements\} Yêu cầu SDK tối thiểu: `minSdkVersion 21` :::info Adapty tương thích với Google Play Billing Library lên đến phiên bản 8.x. Mặc định, Adapty sử dụng Google Play Billing Library v7.0.0, nhưng nếu bạn muốn dùng phiên bản mới hơn, bạn có thể tự [thêm dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK \{#install-adapty-sdk\} Chọn phương thức thiết lập dependency của bạn: - Standard Gradle: Thêm dependency vào **module-level** `build.gradle` - Nếu dự án của bạn dùng file `.gradle.kts`, thêm dependency vào module-level `build.gradle.kts` - Nếu bạn dùng version catalogs, thêm dependency vào file `libs.versions.toml`, sau đó tham chiếu nó trong `build.gradle.kts` [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Android.svg?style=flat&logo=android)](https://github.com/adaptyteam/AdaptySDK-Android/releases) ```groovy showLineNumbers dependencies { ... implementation platform('io.adapty:adapty-bom:') implementation 'io.adapty:android-sdk' // Only add this line if you plan to use Paywall Builder implementation 'io.adapty:android-ui' } ``` ```kotlin showLineNumbers dependencies { ... implementation(platform("io.adapty:adapty-bom:")) implementation("io.adapty:android-sdk") // Only add this line if you plan to use Paywall Builder: implementation("io.adapty:android-ui") } ``` ```toml showLineNumbers //libs.versions.toml [versions] .. adaptyBom = "" [libraries] .. adapty-bom = { module = "io.adapty:adapty-bom", version.ref = "adaptyBom" } adapty = { module = "io.adapty:android-sdk" } // Only add this line if you plan to use Paywall Builder: adapty-ui = { module = "io.adapty:android-ui" } //module-level build.gradle.kts dependencies { ... implementation(platform(libs.adapty.bom)) implementation(libs.adapty) // Only add this line if you plan to use Paywall Builder: implementation(libs.adapty.ui) } ``` Nếu dependency không được resolve, hãy đảm bảo rằng bạn có `mavenCentral()` trong Gradle scripts.
Hướng dẫn cách thêm Nếu project của bạn không có `dependencyResolutionManagement` trong `settings.gradle`, hãy thêm đoạn sau vào `build.gradle` cấp cao nhất ở cuối phần repositories: ```groovy showLineNumbers title="top-level build.gradle" allprojects { repositories { ... mavenCentral() } } ``` Nếu không, hãy thêm đoạn sau vào `settings.gradle` trong phần `repositories` của `dependencyResolutionManagement`: ```groovy showLineNumbers title="settings.gradle" dependencyResolutionManagement { ... repositories { ... mavenCentral() } } ```
:::important Adapty Android SDK 4.0 là phiên bản pre-release. Gradle không tự động chọn các phiên bản pre-release thông qua dynamic version ranges (như `+` hay `latest.release`), vì vậy bạn phải chỉ định chính xác phiên bản. Đặt phiên bản `adapty-bom` thành phiên bản pre-release 4.0 — ví dụ `io.adapty:adapty-bom:4.0.0-beta.2`, hoặc `adaptyBom = "4.0.0-beta.2"` trong `libs.versions.toml`. BOM sẽ tự động xác định phiên bản `android-sdk` và `android-ui` tương ứng. Xem [Migrate Adapty Android SDK sang v4](migration-to-android-sdk-v4). ::: ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} ### Thiết lập cơ bản \{#basic-setup\} Kích hoạt SDK trong code của ứng dụng. :::note SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: Để lấy **Public SDK Key**: 1. Truy cập Adapty Dashboard và điều hướng đến [**App settings → General**](https://app.adapty.io/settings/general). 2. Trong phần **Api keys**, sao chép **Public SDK Key** (KHÔNG phải Secret Key). 3. Thay thế `"YOUR_PUBLIC_SDK_KEY"` trong code. Hoặc lấy theo cách lập trình, sử dụng [Adapty CLI](developer-cli): ``` npm install -g adapty adapty auth login adapty apps list ``` Hoặc, trực tiếp: ``` npx adapty auth login adapty apps list ``` - Đảm bảo bạn sử dụng **Public SDK key** để khởi tạo Adapty, **Secret key** chỉ nên dùng cho [server-side API](getting-started-with-server-side-api). - **SDK keys** là duy nhất cho mỗi ứng dụng, vì vậy nếu bạn có nhiều ứng dụng, hãy đảm bảo chọn đúng key. ```kotlin showLineNumbers // In your Application class class MyApplication : Application() { override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .build() ) } } ``` ```java showLineNumbers // In your Application class public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Adapty.activate( getApplicationContext(), new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .build() ); } } ``` :::important Hãy đợi `Adapty.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 Android SDK](android-sdk-call-order) để biết trình tự đầy đủ. ::: Bây giờ hãy thiết lập paywall trong ứng dụng của bạn: - Nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), hãy làm theo [hướng dẫn nhanh về Paywall Builder](android-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](android-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder), bạn cần module AdaptyUI. Module này được kích hoạt tự động khi bạn kích hoạt module cốt lõi; bạn không cần thực hiện thêm bất cứ điều gì. ## Cấu hình Proguard \{#configure-proguard\} Trước khi phát hành ứng dụng lên production, hãy thêm `-keep class com.adapty.** { *; }` vào cấu hình Proguard của bạn. ## Thiết lập tùy chọn \{#optional-setup\} ### Ghi nhật ký \{#logging\} #### Thiết lập hệ thống ghi nhật ký \{#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ác cấp độ sau: | Level | Mô tả | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------- | | `AdaptyLogLevel.NONE` | Không có gì được ghi log. Giá trị mặc định | | `AdaptyLogLevel.ERROR` | Chỉ các lỗi được ghi log | | `AdaptyLogLevel.WARN` | 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ú ý sẽ được ghi log. | | `AdaptyLogLevel.INFO` | Các lỗi, cảnh báo và nhiều thông báo thông tin khác sẽ được ghi log. | | `AdaptyLogLevel.VERBOSE` | Mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như lời gọi hàm, truy vấn API, v.v. sẽ được ghi log. | Bạn có thể đặt mức độ log trong ứng dụng trước khi cấu hình Adapty. ```kotlin showLineNumbers Adapty.logLevel = AdaptyLogLevel.VERBOSE //recommended for development and the first production release ``` ```java showLineNumbers Adapty.setLogLevel(AdaptyLogLevel.VERBOSE); //recommended for development and the first production release ``` #### Chuyển hướng thông báo từ hệ thống ghi log \{#redirect-the-logging-system-messages\} Nếu vì lý do nào đó bạn cần gửi thông báo từ Adapty sang hệ thống của mình hoặc lưu chúng vào file, bạn có thể ghi đè hành vi mặc định: ```kotlin showLineNumbers Adapty.setLogHandler { level, message -> //handle the log } ``` ```java showLineNumbers Adapty.setLogHandler((level, message) -> { //handle the log }); ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi lên, nhưng bạn có thể áp dụng thêm các chính sách bảo mật dữ liệu để tuân thủ hướng dẫn của cửa hàng hoặc quy định của từng quốc gia. #### Tắt tính năng 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 tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư của người dùng, tuân thủ các quy định bảo vệ dữ liệu khu vực (như GDPR hoặc CCPA), hoặc giảm thiểu việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên địa chỉ IP không cần thiết cho ứng dụng của bạn. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build(); ``` #### Vô hiệu hóa thu thập và chia sẻ advertising ID (Ad ID) \{#disable-advertising-id-ad-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `adIdCollectionDisabled` thành `true` để vô hiệu hóa việc thu thập [advertising ID](https://support.google.com/googleplay/android-developer/answer/6048248) của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách Play Store, tránh kích hoạt lời nhắc cấp quyền advertising ID, hoặc nếu ứng dụng của bạn không cần attribution quảng cáo hay analytics dựa trên Ad ID. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withAdIdCollectionDisabled(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withAdIdCollectionDisabled(true) .build(); ``` #### Cấu hình bộ nhớ cache media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Theo 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 lưu lượng mạng. Bạn có thể tùy chỉnh các cài đặt cache bằng cách cung cấp một cấu hình tùy chỉnh. Dùng `AdaptyUI.configureMediaCache` để ghi đè kích thước cache mặc định và thời gian hiệu lực. Đâ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 sử dụng (100MB dung lượng đĩa, hiệu lực 7 ngày). ```kotlin showLineNumbers val cacheConfig = MediaCacheConfiguration.Builder() .overrideDiskStorageSizeLimit(200L * 1024 * 1024) // 200 MB .overrideDiskCacheValidityTime(3.days) .build() AdaptyUI.configureMediaCache(cacheConfig) ``` ```java showLineNumbers MediaCacheConfiguration cacheConfig = new MediaCacheConfiguration.Builder() .overrideDiskStorageSizeLimit(200L * 1024 * 1024) // 200 MB .overrideDiskCacheValidityTime(TimeInterval.days(3)) .build(); AdaptyUI.configureMediaCache(cacheConfig); ``` **Tham số:** | Tham số | Bắt buộc | Mô tả | |-------------------------|----------|-----------------------------------------------------------------------------| | diskStorageSizeLimit | tùy chọn | Tổng kích thước cache trên đĩa tính bằng byte. Mặc định là 100 MB. | | diskCacheValidityTime | tùy chọn | Thời gian các file được lưu cache còn hiệu lực. Mặc định là 7 ngày. | :::tip Bạn có thể xóa bộ nhớ đệm media lúc runtime bằng cách dùng `AdaptyUI.clearMediaCache(strategy)`, trong đó `strategy` có thể là `CLEAR_ALL` hoặc `CLEAR_EXPIRED_ONLY`. ::: ### Đặt obfuscated account ID \{#set-obfuscated-account-ids\} Google Play yêu cầu obfuscated account ID cho một số trường hợp sử dụng nhất định nhằm tăng cường quyền riêng tư và bảo mật cho 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 ở dạng ẩn danh, điều này đặc biệt quan trọng cho việc ngăn chặn gian lận và phân tích dữ liệu. 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 bắt buộc phải 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 thực của người dùng. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObfuscatedAccountId("YOUR_OBFUSCATED_ACCOUNT_ID") .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObfuscatedAccountId("YOUR_OBFUSCATED_ACCOUNT_ID") .build(); ``` ### Chạy Adapty trong một process tùy chỉnh \{#run-adapty-in-a-custom-process\} Mặc định, Adapty chỉ có thể chạy trong process chính của ứng dụng. Nếu ứng dụng của bạn sử dụng nhiều process, hãy khởi tạo Adapty chỉ một lần; nếu không, có thể xảy ra các hành vi không mong muốn. Nếu bạn cần chạy Adapty trong một process khác, hãy chỉ định nó trong cấu hình của bạn: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withProcessName(":custom") .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withProcessName(":custom") .build(); ``` Nếu bạn cố kích hoạt Adapty trong một tiến trình khác mà không thiết lập giá trị này, SDK sẽ ghi log cảnh báo và bỏ qua việc kích hoạt. ### Bật mức độ truy cập cục bộ \{#enable-local-access-levels\} Theo mặc định, [mức độ truy cập cục bộ](local-access-levels) bị tắt trên Android. Để bật tính năng này, đặt `withLocalAccessLevelAllowed` thành `true`: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withLocalAccessLevelAllowed(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withLocalAccessLevelAllowed(true) .build(); ``` ## Khắc phục sự cố \{#troubleshooting\} #### Quy tắc sao lưu Android (Cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm cả 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ùng định nghĩa quy tắc sao lưu, trình hợp nhất Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Biểu hiện lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/sample_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` Để giải quyết vấn đề này, 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. - Gộp các quy tắc backup từ Adapty và các SDK khác vào một file XML duy nhất (hoặc một cặp file cho Android 12+). #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Nếu chưa có, hãy thêm namespace `tools` vào thẻ `` gốc: ```xml ... ``` #### 2. Ghi đè các thuộc tính backup trong `` \{#2-override-backup-attributes-in-application\} Trong file `AndroidManifest.xml` của app, hãy cập nhật thẻ `` để app cung cấp các giá trị cuối cùng và yêu cầu manifest merger thay thế các giá trị của 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 đã được gộp \{#3-create-merged-backup-rules-files\} Tạo các file XML trong `app/src/main/res/xml/` kết hợp các rule của Adapty với các rule 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 hệ điều hành, vì vậy việc tạo cả hai file đảm bảo khả năng tương thích trên tất cả các phiên bản Android mà ứng dụng của bạn hỗ trợ. :::note Các ví dụ bên dưới sử dụng AppsFlyer như một SDK bên thứ ba mẫu. Hãy thay thế hoặc bổ sung các rule cho bất kỳ SDK nào 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 quy tắc trích xuất dữ liệu mới): ```xml title="sample_data_extraction_rules.xml" ``` **Đối với Android 11 và thấp hơn** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" ``` Với cấu hình này: - Các quy tắc loại trừ backup của Adapty (`AdaptySDKPrefs.xml`) được giữ nguyên. - Các quy tắc loại trừ của SDK khác (ví dụ: `appsflyer-data`) cũng được áp dụng. - Manifest merger sử dụng cấu hình của ứng dụng và không còn bị lỗi do xung đột thuộc tính backup. #### Giao dịch mua thất bại sau khi quay lại từ app khác \{#purchases-fail-after-returning-from-another-app\} Nếu Activity khởi động flow mua hàng sử dụng `launchMode` không 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, app ngân hàng, hoặc trình duyệt. Điều này có thể khiến kết quả giao dịch bị mất hoặc bị coi là đã hủy. Để đảm bảo giao dịch mua hoạt động đúng, chỉ sử dụng `standard` hoặc `singleTop` làm launch mode 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 ``` --- # File: android-quickstart-paywalls --- --- title: "Bật tính năng mua hàng với Flow Builder trong Android SDK" description: "Hướng dẫn nhanh để bật in-app purchase với Adapty Flow Builder." --- Để bật in-app purchase, bạn cần hiểu ba khái niệm cốt lõi: - [**Sản phẩm**](product) – những gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flows**](adapty-flow-builder) – chuỗi màn hình trình bày sản phẩm cho người dùng, được xây dựng bằng Flow Builder không cần code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện bằng code, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](android-quickstart-manual). - [**Placements**](placements) – nơi và thời điểm hiển thị flow trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trên dashboard, sau đó yêu cầu chúng theo placement ID trong code. Điều này giúp dễ dàng chạy A/B test và hiển thị các flow khác nhau cho từng nhóm người dùng. Adapty cung cấp cho bạn ba cách để kích hoạt tính năng mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Triển khai | Độ phức tạp | Khi nào nên dùng | |---|---|---| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng để mua hàng trong no-code builder](quickstart-paywalls). Adapty tự động hiển thị flow và xử lý toàn bộ quy trình mua hàng phức tạp, xác thực receipt và quản lý gói đăng ký. | | Paywall tự 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 flow từ Adapty để linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](android-quickstart-manual). | | Chế độ Observer | 🔴 Khó | Bạn đã có sẵn 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 một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, xem [Triển khai paywall thủ công](android-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code của ứng dụng, bạn chỉ cần: 1. **Lấy flow**: Lấy nó từ Adapty. 2. **Hiển thị nó và Adapty sẽ xử lý các giao dịch mua cho bạn**: Hiển thị view trong ứng dụng của bạn. 3. **Xử lý các hành động nút**: Liên kết các tương tác của người dùng với phản hồi của ứng dụng đối với chúng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn vào các 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 với Google Play](initial-android) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo một flow và thêm sản phẩm vào đó](create-paywall). 4. [Tạo một placement và thêm flow vào đó](create-placement). 5. [Cài đặt và kích hoạt Adapty SDK](sdk-installation-android) trong code ứng dụng của bạn. Hướng dẫn này sử dụng Adapty Android SDK v4 APIs. :::tip Cách nhanh nhất để hoàn thành các bước này là làm theo [hướng dẫn quickstart](quickstart) hoặc tạo flow và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy flow \{#get-the-flow\} Các flow của bạn được liên kết với các placement được cấu hình trong dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy một flow đã tạo trong Adapty Flow Builder, bạn cần: 1. Lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow` và kiểm tra xem nó có cấu hình view hay không. 2. Lấy cấu hình view bằng phương thức `getFlowConfiguration`. Cấu hình view chứa các thành phần UI và kiểu dáng cần thiết để hiển thị flow. :::important Để lấy cấu hình view, bạn phải bật toggle **Show on device** trong Flow Builder. Nếu không, bạn sẽ nhận được cấu hình view trống và flow sẽ không hiển thị. ::: ```kotlin showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID") { result -> if (result is AdaptyResult.Success) { val flow = result.value if (!flow.hasViewConfiguration) { return@getFlow } AdaptyUI.getFlowConfiguration(flow) { configResult -> if (configResult is AdaptyResult.Success) { val flowConfiguration = configResult.value } } } } ``` ```java showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) result).getValue(); if (!flow.hasViewConfiguration()) { return; } AdaptyUI.getFlowConfiguration(flow, configResult -> { if (configResult instanceof AdaptyResult.Success) { AdaptyUI.FlowConfiguration flowConfiguration = ((AdaptyResult.Success) configResult).getValue(); // use loaded configuration } }); } }); ``` ## 2. Hiển thị flow \{#display-the-flow\} Sau khi đã có cấu hình flow, bạn chỉ cần thêm vài dòng code để hiển thị flow. Để hiển thị flow trực quan trên màn hình thiết bị, bạn cần cấu hình nó trước. Để làm điều này, hãy gọi phương thức `AdaptyUI.getFlowView()` hoặc tạo `AdaptyFlowView` trực tiếp: ```kotlin showLineNumbers val flowView = AdaptyUI.getFlowView( activity, flowConfiguration, null, // products = null means auto-fetch eventListener, ) ``` ```kotlin showLineNumbers val flowView = AdaptyFlowView(activity) // or retrieve it from xml ... with(flowView) { showFlow( flowConfiguration, null, // products = null means auto-fetch eventListener, ) } ``` ```java showLineNumbers AdaptyFlowView flowView = AdaptyUI.getFlowView( activity, flowConfiguration, null, // products = null means auto-fetch eventListener ); ``` ```java showLineNumbers AdaptyFlowView flowView = new AdaptyFlowView(activity); //add to the view hierarchy if needed, or you receive it from xml ... flowView.showFlow(flowConfiguration, products, eventListener); ``` ```xml showLineNumbers ``` Sau khi view được tạo thành công, bạn có thể thêm nó vào cây view hierarchy và hiển thị trên màn hình thiết bị. :::tip Để biết thêm chi tiết về cách hiển thị một flow, hãy xem [hướng dẫn](android-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút bấm \{#handle-button-actions\} Khi người dùng nhấn các nút trong flow, Android SDK tự động xử lý việc mua hàng, khôi phục, đóng flow và mở liên kết. Tuy nhiên, một số nút khác có ID tùy chỉnh hoặc được định nghĩa sẵn và cần bạn xử lý hành động trong code. Hoặc bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ, đây là hành vi mặc định của nút đóng. Bạn không cần thêm đoạn code này, nhưng ở đây bạn có thể thấy cách thực hiện nếu cần. :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [actions](android-handle-paywall-actions) và [events](android-handling-events) của button. ::: ```kotlin showLineNumbers title="Kotlin" override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { AdaptyUI.Action.Close -> (context as? Activity)?.onBackPressed() // default behavior } } ``` ```java showLineNumbers @Override public void onActionPerformed(@NonNull AdaptyUI.Action action, @NonNull Context context) { if (action instanceof AdaptyUI.Action.Close) { if (context instanceof Activity) { ((Activity) context).onBackPressed(); } } } ``` ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. [Kiểm tra mua hàng trên Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử nghiệm từ paywall. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](android-check-subscription-status) để đảm bảo hiển thị paywall hoặc cấp quyền truy cập tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Đây là cách tích hợp tất cả các bước trên vào ứng dụng của bạn. ```kotlin showLineNumbers title="Kotlin" class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Adapty.getFlow("YOUR_PLACEMENT_ID") { flowResult -> if (flowResult is AdaptyResult.Success) { val flow = flowResult.value if (!flow.hasViewConfiguration) { // Use custom logic return@getFlow } AdaptyUI.getFlowConfiguration(flow) { configResult -> if (configResult is AdaptyResult.Success) { val flowConfiguration = configResult.value val flowView = AdaptyUI.getFlowView( this, flowConfiguration, null, // products = null means auto-fetch object : AdaptyFlowDefaultEventListener() { override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.Close -> { (context as? Activity)?.onBackPressed() } } } } ) setContentView(flowView) } } } } } } ``` ```java showLineNumbers public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Adapty.getFlow("YOUR_PLACEMENT_ID", flowResult -> { if (flowResult instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) flowResult).getValue(); if (!flow.hasViewConfiguration()) { // Use custom logic return; } AdaptyUI.getFlowConfiguration(flow, configResult -> { if (configResult instanceof AdaptyResult.Success) { AdaptyUI.FlowConfiguration flowConfiguration = ((AdaptyResult.Success) configResult).getValue(); AdaptyFlowView flowView = AdaptyUI.getFlowView( this, flowConfiguration, null, // products = null means auto-fetch new AdaptyFlowDefaultEventListener() { @Override public void onActionPerformed(@NonNull AdaptyUI.Action action, @NonNull Context context) { if (action instanceof AdaptyUI.Action.Close) { if (context instanceof Activity) { ((Activity) context).onBackPressed(); } } } } ); setContentView(flowView); } }); } }); } } ``` --- # File: android-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong Android SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Android 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 hiển thị paywall, bạn cần kiểm tra [mức độ truy cập](access-level) của họ trong hồ sơ người dùng. Bài viết này hướng dẫn cách truy cập trạng thái hồ sơ người dùng để quyết định hiển thị nội dung phù hợp — hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí. ## Lấy trạng thái gói đăng ký \{#get-subscription-status\} Khi quyết định có nên hiển thị paywall hay nội dung trả phí cho người dùng, bạn kiểm tra [mức độ truy cập](access-level) trong hồ sơ người dùng của họ. Bạn có hai lựa chọn: - Gọi `getProfile` nếu bạn cần dữ liệu hồ sơ mới nhất ngay lập tức (như 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** để lưu 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à sử dụng phương thức `getProfile` để truy cập hồ sơ người dùng: ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ### Lắng nghe cập nhật gói đăng ký \{#listen-to-subscription-updates\} Để tự động nhận cập nhật hồ sơ trong ứng dụng của bạn: 1. Dùng `Adapty.setOnProfileUpdatedListener()` để 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ập nhật khi phương thức này được gọi, để bạn có thể sử dụng trong toàn ứng dụng mà không cần thêm các yêu cầu mạng. ```kotlin class SubscriptionManager { private var currentProfile: AdaptyProfile? = null init { // Listen for profile updates Adapty.setOnProfileUpdatedListener { profile -> currentProfile = profile // Update UI, unlock content, etc. } } // Use stored profile instead of calling getProfile() fun hasAccess(): Boolean { return currentProfile?.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true } } ``` ```java public class SubscriptionManager { private AdaptyProfile currentProfile; public SubscriptionManager() { // Listen for profile updates Adapty.setOnProfileUpdatedListener(profile -> { this.currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() public boolean hasAccess() { if (currentProfile == null) { return false; } AdaptyAccessLevel premiumAccess = currentProfile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); return premiumAccess != null && premiumAccess.isActive(); } } ``` :::note Adapty tự động gọi listener cập nhật hồ sơ khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký đã được cache ngay cả khi thiết bị đang ngoại tuyến. ::: ## 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 vào các tính năng trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách tiếp cận này hữu ích trong các tình huống như: khi khởi động ứng dụng, khi vào các khu vực premium, hoặc trước khi hiển thị nội dung cụ thể. ```kotlin private fun initializePaywall() { loadPaywall { paywallView -> checkAccessLevel { result -> when (result) { is AdaptyResult.Success -> { if (!result.value && paywallView != null) { setContentView(paywallView) // Show paywall if no access } } is AdaptyResult.Error -> { if (paywallView != null) { setContentView(paywallView) // Show paywall if access check fails } } } } } } private fun checkAccessLevel(callback: ResultCallback) { Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val hasAccess = result.value.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true callback.onResult(AdaptyResult.Success(hasAccess)) } is AdaptyResult.Error -> { callback.onResult(AdaptyResult.Error(result.error)) } } } } ``` ```java private void initializePaywall() { loadPaywall(paywallView -> { checkAccessLevel(result -> { if (result instanceof AdaptyResult.Success) { boolean hasAccess = ((AdaptyResult.Success) result).getValue(); if (!hasAccess && paywallView != null) { setContentView(paywallView); // Show paywall if no access } } else if (result instanceof AdaptyResult.Error) { if (paywallView != null) { setContentView(paywallView); // Show paywall if access check fails } } }); }); } private void checkAccessLevel(ResultCallback callback) { Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); AdaptyAccessLevel premiumAccess = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); boolean hasAccess = premiumAccess != null && premiumAccess.isActive(); callback.onResult(AdaptyResult.success(hasAccess)); } else if (result instanceof AdaptyResult.Error) { callback.onResult(AdaptyResult.error(((AdaptyResult.Error) result).getError())); } }); } ``` ## Các bước tiếp theo \{#next-steps\} Bây giờ bạn đã biết cách theo dõi trạng thái gói đăng ký, hãy tìm hiểu cách [làm việc với hồ sơ người dùng](android-quickstart-identify) để đảm bảo họ có thể truy cập những gì họ đã trả tiền. --- # File: android-quickstart-identify --- --- title: "Xác định người dùng trong Android SDK" description: "Hướng dẫn nhanh thiết lập Adapty để quản lý gói đăng ký in-app trên Android." --- :::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ó phù hợp với hệ thống xác thực hiện có của bạn. ::: Cách bạn quản lý 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 không sử dụng xác thực backend và không lưu trữ dữ liệu người dùng, hãy xem [phần về người dùng ẩn danh](#anonymous-users). - Nếu ứng dụng có (hoặc sẽ có) xác thực backend, hãy xem [phần về người dùng đã xác định](#identified-users). **Các khái niệm chính**: - **Hồ sơ người dùng** là các thực thể cần thiết để SDK hoạt động. Adapty tự động tạo chúng. - Chúng có thể là ẩn danh **(không có customer user ID)** hoặc đã xác định **(có customer user ID)**. - Bạn cung cấp **customer user ID** để liên kết chéo hồ sơ người dùng trong Adapty với hệ thống xác thực nội bộ của bạn. Đâ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 các thiết bị thông qua customer user ID của họ | | **Quản lý hồ sơ người dùng** | Hồ sơ người dùng mới sau mỗi lần cài đặt lại | Cùng một hồ sơ người dùng trên các 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 được lưu trữ 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 khi khởi động ứng dụng, Adapty **tạo hồ sơ người dùng 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ơ người dùng 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 hồ sơ người dùng ẩ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 của bạn, theo mặc định, giao dịch mua của họ sẽ tự động được đồng bộ từ App Store khi kích hoạt SDK. Với người dùng ẩn danh, hồ sơ người dùng 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 phân tích Adapty, bạn có thể [cấu hình những gì sẽ được tính là lần cài đặt mới](general#4-installs-definition-for-analytics). Đối với người dùng ẩn danh, bạn cần đếm số lần cài đặt theo **ID thiết bị**. Trong trường hợp này, mỗi lần cài đặt ứng dụng trên thiết bị được tính là một lần cài đặt, bao gồm cả cài đặt lại. ## Người dùng đã xác định \{#identified-users\} Bạn có hai lựa 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 động, hãy gửi nó khi gọi `activate()`. :::important Theo mặc định, khi Adapty nhận được giao dịch mua từ Customer User ID hiện đang được liên kết với Customer User ID khác, mức độ truy cập sẽ được chia sẻ, vì vậy cả hai hồ sơ người dùng đề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ơ người dùng này sang hồ sơ người dùng khác hoặc tắt hoàn toàn việc chia sẻ. Xem [bài viết](general#6-sharing-paid-access-between-user-accounts) để biết thêm chi tiết. ::: ### Trong quá trình đăng nhập/đăng ký \{#during-loginsignup\} Nếu bạn xác định người dùng sau khi ứng dụng khởi động (ví dụ: sau khi họ đăng nhập vào ứng dụng hoặc đăng ký), hãy sử dụng phương thức `identify` để thiết lập customer user ID của họ. - Nếu bạn **chưa từng sử dụng customer user ID này trước đây**, Adapty sẽ tự động liên kết nó với hồ sơ người dùng hiện tại. - Nếu bạn **đã từng 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ơ người dùng đượ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. ::: Hãy chờ callback hoàn thành `identify` được kích hoạt trước khi gọi các phương thức SDK khác. Các cuộc gọi đồng thời có thể rơi vào hồ sơ người dùng ẩn danh thay vì hồ sơ người dùng đã xác định. Xem [Thứ tự gọi trong Android SDK](android-sdk-call-order). ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` ### 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 hồ sơ người dùng ẩn danh mới và chỉ chuyển sang hồ sơ người dùng hiện có sau khi bạn gọi `identify`. Bạn có thể truyền customer user ID đã có (cái bạn đã 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ơ người dùng mới được tạo khi kích hoạt sẽ được tự động liên kết với customer user ID đó. :::note Theo mặc định, việc tạo hồ sơ người dùng ẩn danh không ảnh hưởng đến dashboard phân tích, vì số lần cài đặt được tính dựa trên ID thiết bị. ID thiết bị đại diện cho một lần cài đặt ứng dụng duy nhất 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ặp lại, hay liệu có sử dụng customer user ID hiện có hay không. Việc tạo hồ sơ người dùng (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 sự kiện cài đặt bổ sung. 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). ::: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. .build(); ``` ### Đă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 hồ sơ người dùng ẩn danh mới cho người dùng. ::: ```kotlin showLineNumbers Adapty.logout { error -> if (error == null) { // successful logout } } ``` ```java showLineNumbers Adapty.logout(error -> { if (error == null) { // successful logout } }); ``` :::info Để đăng nhập lại người dùng vào ứng dụng, hãy sử dụng phương thức `identify`. ::: ### Cho phép mua hàng mà không cần đăng nhập \{#allow-purchases-without-login\} Nếu người dùng có 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ẽ giữ 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ơ người dùng ẩn danh của họ. 2. Khi người dùng đăng nhập vào tài khoản, Adapty chuyển sang làm việc với hồ sơ người dùng đã 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 gán customer user ID cho hồ sơ người dùng hiện tại, vì vậy toàn bộ lịch sử giao dịch mua được duy trì. - Nếu đây là customer user ID đã tồn tại (customer user ID đã được liên kết với hồ sơ người dùng), bạn cần lấy mức độ truy cập thực tế sau khi chuyển hồ sơ người dùng. Bạn có thể gọi [`getProfile`](android-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe cập nhật hồ sơ người dùng](android-check-subscription-status) để dữ liệu tự động đồng bộ. ## Bước tiếp theo \{#next-steps\} Chúc mừng! Bạn đã triển khai logic thanh toán in-app trong ứng dụng của mình! Chúc bạn thành công với việc kiếm tiền từ ứng dụng! Để khai thác tối đa Adapty, bạn có thể khám phá các chủ đề sau: - [**Kiểm thử**](troubleshooting-test-purchases): Đảm bảo rằng mọi thứ hoạt động như mong đợi - [**Onboardings**](android-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 dịch vụ attribution marketing và phân tích chỉ với một dòng code - [**Thiết lập thuộc tính hồ sơ người dùng tùy chỉnh**](android-setting-user-attributes): Thêm thuộc tính tùy chỉnh vào hồ sơ người dùng và tạo phân khúc, để bạn có thể khởi chạy A/B test hoặc hiển thị các paywall khác nhau cho những người dùng khác nhau --- # File: adapty-sdk-integration-skill-android --- --- title: "Tích hợp Adapty vào ứng dụng Android 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 Android 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-android) — hướng dẫn đó sẽ dẫn dắt công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn form phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt xong, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-android --- --- title: "Tích hợp Adapty vào ứng dụng Android 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 Android 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ẽ đưa bạn qua từng bước tích hợp Adapty vào ứng dụng Android với một công cụ AI — bạn chỉ cần cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ mã 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 ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng của mình](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm 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ả khi nào cần mở Dashboard để kết nối cửa hàng. ### Cách thủ công qua Dashboard \{#dashboard-approach\} Nếu bạn muốn tự cấu hình mọi thứ, đây là những gì bạn cần trước khi viết code. LLM của bạn không thể tra cứu các giá trị từ dashboard — bạn sẽ phải tự cung cấp chúng. 1. **Kết nối cửa hàng ứng dụng của bạn**: Trong Adapty Dashboard, vào **App settings → General**. Đây là bước bắt buộc để các giao dịch mua hoạt động. [Kết nối Google Play](integrate-payments) 2. **Sao chép Public SDK key của bạn**: Trong Adapty Dashboard, vào **App settings → General**, rồi tìm phần **API keys**. Trong code, đây là chuỗi bạn truyền vào Adapty configuration builder. 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 phân phối 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**, rồi 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("YOUR_PLACEMENT_ID")`. [Tạo paywall](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình theo 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 truy cập 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 cho LLM của bạn biết: "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ài đặt 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 mới 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 tự động phát hiện editor của bạn và cấu hình Context7 server. Để thiết lập thủ công, xem [Context7 GitHub repository](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 Android SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn rất 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. ::: ### Dùng tài liệu dạng văn bản thuần \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng Markdown thuần. Thêm `.md` vào cuối URL, hoặc nhấp **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor-android.md](https://adapty.io/docs/vi/adapty-cursor-android.md). Mỗi bước trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới đều có một khối "Gửi cho LLM của bạn" với các link `.md` để dán vào. Để có thêm tài liệu cùng một lúc, xem [file index và các tập con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này sẽ đi qua tích hợp Adapty theo đúng thứ tự triển khai. Mỗi bước bao gồm tài liệu cần gửi cho LLM, kết quả mong đợi 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 tay vào code, hãy yêu cầu LLM phân tích dự án của bạn và tạo một kế hoạch triển khai. Nếu công cụ AI của bạn hỗ trợ chế độ lên kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy sử dụng nó để LLM có thể đọc cả cấu trúc dự án lẫn tài liệu Adapty trước khi viết code. Hãy cho LLM biết bạn dùng cách tiếp cận nào cho việc mua hàng — điều này ảnh hưởng đến các hướng dẫn nó cần theo: - [**Adapty Paywall Builder**](adapty-paywall-builder): Bạn tạo paywall trong trình tạo không cần code của Adapty, và SDK tự động render chúng. - [**Paywall tự tạo**](android-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](android-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm dependency Adapty SDK qua Gradle trong Android Studio và kích hoạt nó bằng Public SDK key của bạn. Đây là nền tảng — mọi thứ khác đều không hoạt động nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-android) 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-android.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Ứng dụng build và chạy được. Logcat hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra xem bạn đã thay placeholder bằng key thật 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. Hãy test từng giao dịch mua trong sandbox khi bạn làm — đừng đợi đến cuối. Xem [Kiểm tra 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)](android-quickstart-paywalls) - [Lấy paywall Paywall Builder và cấu hình của chúng](android-get-pb-paywalls) - [Hiển thị paywall](android-present-paywalls) - [Xử lý sự kiện paywall](android-handling-events) - [Phản hồi các hành động nút](android-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/android-quickstart-paywalls.md - https://adapty.io/docs/vi/android-get-pb-paywalls.md - https://adapty.io/docs/vi/android-present-paywalls.md - https://adapty.io/docs/vi/android-handling-events.md - https://adapty.io/docs/vi/android-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 một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Paywall trống hoặc lỗi `getPaywall` → kiểm tra xem placement ID có khớp chính xác với dashboard không và placement đã được gán audience chưa. ::: **Hướng dẫn:** - [Kích hoạt giao dịch mua trong paywall tự tạo (quickstart)](android-quickstart-manual) - [Lấy paywall và sản phẩm](fetch-paywalls-and-products-android) - [Render paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-android) - [Thực hiện giao dịch mua](android-making-purchases) - [Khôi phục giao dịch mua](android-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/android-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products-android.md - https://adapty.io/docs/vi/present-remote-config-paywalls-android.md - https://adapty.io/docs/vi/android-making-purchases.md - https://adapty.io/docs/vi/android-restore-purchase.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Paywall tự tạo của bạn hiển thị các sản phẩm lấy từ Adapty. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Mảng sản phẩm trống → kiểm tra xem paywall đã được gán sản phẩm trong dashboard chưa và placement đã có audience chưa. ::: **Hướng dẫn:** - [Tổng quan Observer mode](observer-vs-full-mode) - [Triển khai Observer mode](implement-observer-mode-android) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode-android) 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-android.md - https://adapty.io/docs/vi/report-transactions-observer-mode-android.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi mua hàng sandbox bằng flow mua hàng hiện có, giao dịch xuất hiện trong **Event Feed** trên Adapty dashboard. - **Lưu ý:** Không có sự kiện → kiểm tra xem bạn đã báo cáo giao dịch cho Adapty chưa và Google Play Real-Time Developer Notifications đã được cấu hình chưa. ::: ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua hàng, kiểm tra hồ sơ người dùng để tìm mức độ truy cập đang hoạt động nhằm kiểm soát nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](android-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/android-check-subscription-status.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi mua hàng 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ơ 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](android-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/android-quickstart-identify.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi gọi `Adapty.identify("your-user-id")`, 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 đã hoạt động 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 production. **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 đã 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à các yêu cầu về quyền riêng tư. - **Lưu ý:** Thiếu Google Play Real-Time Developer Notifications → cấu hình trong **App settings → Android SDK** nếu không các sự kiện sẽ không xuất hiện trên dashboard. ::: ## File index tài liệu dạng văn bản thuần \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM ngữ cảnh rộng hơn ngoài các trang riêng lẻ, chúng tôi cung cấp các file index 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à một [tiêu chuẩn đang nổi lên](https://llmstxt.org/) giúp website dễ tiếp cận hơ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ộ tài liệu của Adapty được kết hợp thành một file duy nhất. Rất lớn — chỉ dùng khi bạn cần toàn cảnh. - Dành riêng cho Android: [`android-llms.txt`](https://adapty.io/docs/vi/android-llms.txt) và [`android-llms-full.txt`](https://adapty.io/docs/vi/android-llms-full.txt): Các tập con theo nền tảng giúp tiết kiệm token so với toàn bộ site. --- # File: android-get-pb-paywalls --- --- title: "Lấy flows & paywalls - Android" description: "Lấy flows và paywalls từ Adapty trong ứng dụng Android của bạn." --- Sau khi [bạn đã thiết kế flow hoặc paywall bằng Paywall Builder](adapty-paywall-builder), bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên là lấy flow hoặc paywall được liên kết với placement cùng cấu hình hiển thị của nó như mô tả bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. :::
Trước khi bắt đầu hiển thị flow trong ứng dụng di động của bạn (nhấn để mở rộng) 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow/paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-android) vào ứng dụng di động của bạn.
## Lấy flow/paywall \{#fetch-flowpaywall\} Nếu bạn đã thiết kế flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới hiển thị nó trong ứng dụng di động. Để đảm bảo hiệu suất tối ưu, điều quan trọng là cần lấy flow hoặc paywall và [cấu hình view](android-get-pb-paywalls#fetch-the-view-configuration) của nó càng sớm càng tốt, tạo đủ thời gian để tải ảnh trước khi hiển thị cho người dùng. Để lấy một flow hoặc paywall, sử dụng phương thức `getFlow`: ```kotlin showLineNumbers ... Adapty.getFlow("YOUR_PLACEMENT_ID", loadTimeout = 10.seconds) { result -> when (result) { is AdaptyResult.Success -> { val flow = result.value // the requested flow/paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers ... Adapty.getFlow("YOUR_PLACEMENT_ID", TimeInterval.seconds(10), result -> { if (result instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) result).getValue(); // the requested flow/paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

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

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

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

Adapty SDK lưu trữ flow và 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 nhanh hơn và một server 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 đồng thời đảm bảo độ tin cậy ngay cả khi kết nối mạng kém.

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

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

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

Đối với Android: Bạn có thể tạo `TimeInterval` bằng các extension function (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ả | | :-------- | :---------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, Remote Config và cờ `hasViewConfiguration` cho biết flow có bao gồm cấu hình giao diện hay không. Để lấy danh sách sản phẩm thực tế phục vụ việc tải trước, giao diện tùy chỉnh hoặc kiểm tra theo chương trình, hãy gọi `getPaywallProducts(flow)`. | ## Lấy cấu hình giao diện \{#fetch-the-view-configuration\} Sau khi tải flow hoặc paywall, hãy kiểm tra xem nó có chứa cấu hình giao diện hay không thông qua `flow.hasViewConfiguration`. Cờ này cho biết placement được thiết kế theo cách nào trong Adapty Dashboard: - **`true`** — placement được thiết kế trong **Flow Builder** (một flow) hoặc **Paywall Builder** (một paywall). Adapty sẽ tự render giao diện cho bạn. Tiếp tục với các bước bên dưới để lấy cấu hình view và [hiển thị flow hoặc paywall](android-present-paywalls). - **`false`** — placement là một paywall tùy chỉnh không có giao diện Builder. [Xử lý như một paywall Remote Config](present-remote-config-paywalls-android). :::important Hãy đảm bảo bật toggle **Show on device** trong Flow 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. ::: Sử dụng phương thức `getFlowConfiguration` để tải cấu hình view. ```kotlin showLineNumbers if (!flow.hasViewConfiguration) { // use your custom logic return } AdaptyUI.getFlowConfiguration(flow, loadTimeout = 10.seconds) { result -> when(result) { is AdaptyResult.Success -> { val flowConfiguration = result.value // use loaded configuration } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` | Tham số | Bắt buộc | Mô tả | | :-------------- | :------------- | :----------------------------------------------------------- | | **flow** | bắt buộc | Đối tượng `AdaptyFlow` lấy được thông qua `Adapty.getFlow`. | | **locale** |

tùy chọn

mặc định: ngôn ngữ thiết bị

| Mã định danh của [bản dịch](add-paywall-locale-in-adapty-paywall-builder), được nhập dưới dạng mã ngôn ngữ với một hoặc hai subtag phân tách bằng `-` (ví dụ: `en`, `pt-br`). Xem [Bản dịch và mã ngôn ngữ](android-localizations-and-locale-codes). | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã lưu trong bộ nhớ 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 hạn muộn hơn một chút so với thời gian chỉ định trong `loadTimeout`, vì quá trình thực thi có thể bao gồm nhiều yêu cầu khác nhau bên dưới. |
Sử dụng phương thức `getFlowConfiguration` để tải cấu hình view. ```java showLineNumbers if (!flow.hasViewConfiguration()) { // use your custom logic return; } AdaptyUI.getFlowConfiguration(flow, TimeInterval.seconds(10), result -> { if (result instanceof AdaptyResult.Success) { AdaptyUI.FlowConfiguration flowConfiguration = ((AdaptyResult.Success) result).getValue(); // use loaded configuration } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Tham số | Bắt buộc | Mô tả | | :-------------- | :------------- | :----------------------------------------------------------- | | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow`. | | **locale** |

tùy chọn

mặc định: ngôn ngữ thiết bị

| Mã định danh của [bản địa hóa](add-paywall-locale-in-adapty-paywall-builder), được truyền dưới dạng mã ngôn ngữ gồm một hoặc hai thẻ con ngăn cách bởi `-` (ví dụ: `en`, `pt-br`). Xem [Các bản địa hóa và mã ngôn ngữ](android-localizations-and-locale-codes). | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn thời gian chờ của phương thức. Nếu hết thời gian chờ, dữ liệu đã được cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do quá trình xử lý có thể bao gồm nhiều yêu cầu khác nhau bên dưới. |
:::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa trong Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã ngôn ngữ đúng cách [tại đây](android-localizations-and-locale-codes). ::: Sau khi tải xong, [hiển thị flow hoặc paywall](android-present-paywalls). ## Lấy flow hoặc paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, flow và paywall được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong những trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow hoặc paywall có thể mất nhiều thời gian hơn mong muốn. Trong tình huống đó, bạn có thể muốn hiển thị flow hoặc paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getFlowForDefaultAudience`, phương thức này lấy flow hoặc paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow hoặc paywall bằng phương thức `getFlow`, như đã trình bày trong phần [Lấy flow/paywall](#fetch-flowpaywall) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (legacy) hoặc chấp nhận rằng người dùng đang dùng phiên bản hiện tại (legacy) có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để đổi lấy tốc độ tải flow hoặc paywall nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` được mô tả [ở trên](#fetch-flowpaywall). ::: ```kotlin showLineNumbers Adapty.getFlowForDefaultAudience("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val flow = result.value // the requested flow } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getFlowForDefaultAudience("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) result).getValue(); // the requested flow } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

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

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

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

| ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong flow hoặc paywall của bạn, hãy triển khai custom assets. Hero image và video có các ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác nhau cho một số người dùng. - Hiển thị ảnh xem trước cục bộ trong khi ảnh chính từ xa đang tải. - Hiển thị ảnh xem trước trước khi phát video. Here's an example of how you can provide custom assets via a simple dictionary: ```kotlin showLineNumbers val customAssets = AdaptyCustomAssets.of( "hero_image" to AdaptyCustomImageAsset.remote( url = "https://example.com/image.jpg", preview = AdaptyCustomImageAsset.file( FileLocation.fromAsset("images/hero_image_preview.png"), ) ), "hero_video" to AdaptyCustomVideoAsset.file( FileLocation.fromResId(requireContext(), R.raw.custom_video), preview = AdaptyCustomImageAsset.file( FileLocation.fromResId(requireContext(), R.drawable.video_preview), ), ), ) val flowView = AdaptyUI.getFlowView( activity, flowConfiguration, products, eventListener, insets, customAssets, ) ``` :::note Nếu không tìm thấy asset, flow sẽ hiển thị giao diện mặc định của nó. ::: Đối với video, bạn có thể tùy chọn truyền `resolution` để giữ chỗ layout và đặt tỷ lệ khung hình (`width / height`) trước khi video tải: ```kotlin showLineNumbers AdaptyCustomVideoAsset.file( FileLocation.fromResId(requireContext(), R.raw.custom_video), preview = AdaptyCustomImageAsset.file( FileLocation.fromResId(requireContext(), R.drawable.video_preview), ), resolution = AdaptyCustomVideoAsset.Resolution(width = 1080, height = 1920), ) ```
Sau khi [bạn đã thiết kế phần giao diện cho paywall của mình](adapty-paywall-builder) bằng Paywall Builder mới trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động. Bước đầu tiên trong quá trình này là lấy paywall được liên kết với placement cùng cấu hình giao diện của nó như mô tả bên dưới. :::warning Paywall Builder mới hoạt động với Android SDK phiên bản 3.0 trở lên. ::: Vui lòng lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall theo cách thủ công, hãy tham khảo chủ đề [Lấy paywall và sản phẩm cho Remote Config paywall trong ứng dụng di động](fetch-paywalls-and-products-android). :::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 của bạn (nhấn để mở rộng) 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo một paywall và thêm các sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo các placement và thêm paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-android) trong ứng dụng di động của bạn.
## Tải paywall được thiết kế bằng Paywall Builder \{#fetch-paywall-designed-with-paywall-builder\} Nếu bạn đã [thiết kế paywall bằng Paywall Builder](adapty-paywall-builder), bạn không cần lo về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view của nó, rồi mới hiển thị trong ứng dụng. Để đảm bảo hiệu suất tối ưu, bạn nên lấy paywall và [cấu hình view](android-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, sử dụng phương thức `getPaywall`: ```kotlin showLineNumbers ... Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en", loadTimeout = 10.seconds) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers ... Adapty.getPaywall("YOUR_PLACEMENT_ID", "en", TimeInterval.seconds(10), result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

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

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

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

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

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ộ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 paywall mới nhất 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ờ của phương thức này. Nếu hết thời gian chờ, dữ liệu cache hoặc fallback cục bộ sẽ được trả về.

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

Đối với Android: Bạn có thể tạo `TimeInterval` bằng các hàm mở rộng (như `5.seconds`, trong đó `.seconds` đế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`.

| Các tham số phản hồi: | Tham số | Mô tả | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | Một đối tượng [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) chứa danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình hiển thị của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bật toggle **Show on device** trong Paywall Builder. Nếu tùy chọn này chưa được bật, cấu hình hiển thị sẽ không thể lấy được. ::: Sau khi fetch paywall, hãy kiểm tra xem nó có chứa `ViewConfiguration` hay không — đây là dấu hiệu cho thấy paywall được tạo bằng Paywall Builder. Thông tin này sẽ giúp bạn xác định cách hiển thị paywall. Nếu có `ViewConfiguration`, hãy xử lý nó như một paywall Paywall Builder; nếu không, [xử lý nó như một paywall Remote Config](present-remote-config-paywalls). Sử dụng phương thức `getViewConfiguration` để tải cấu hình view. ```kotlin showLineNumbers if (!paywall.hasViewConfiguration) { // use your custom logic return } AdaptyUI.getViewConfiguration(paywall, loadTimeout = 10.seconds) { result -> when(result) { is AdaptyResult.Success -> { val viewConfiguration = result.value // use loaded configuration } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` | Tham số | Bắt buộc | Mô tả | | :-------------- | :------------------ | :----------------------------------------------------------- | | **paywall** | bắt buộc | Đối tượng `AdaptyPaywall` dùng để lấy controller cho paywall mong muốn. | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn thời gian chờ của phương thức. Nếu hết thời gian, dữ liệu được cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức có thể hết thời gian chờ muộn hơn một chút so với giá trị đặt trong `loadTimeout`, vì thao tác có thể bao gồm nhiều request khác nhau bên dưới. | Sử dụng phương thức `getViewConfiguration` để tải cấu hình view. ```java showLineNumbers if (!paywall.hasViewConfiguration()) { // use your custom logic return; } AdaptyUI.getViewConfiguration(paywall, TimeInterval.seconds(10), result -> { if (result instanceof AdaptyResult.Success) { AdaptyUI.LocalizedViewConfiguration viewConfiguration = ((AdaptyResult.Success) result).getValue(); // use loaded configuration } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------------- | :----------------------------------------------------------- | | **paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu được lưu trong bộ nhớ cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã ngôn ngữ đúng cách [tại đây](android-localizations-and-locale-codes). ::: Sau khi tải xong, [hiển thị paywall](android-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo về việc tối ưu tốc độ. Tuy nhiên, nếu bạn có nhiều đối tượng và paywall, hoặc người dùng đang kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong trường hợp đó, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm mượt mà cho người dùng thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này 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ư đã trình bày chi tiết trong phần [Lấy thông tin Paywall](#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (legacy), hoặc chấp nhận rằng người dùng đang dùng phiên bản hiện tại (legacy) có thể gặp sự cố với các paywall không hiển thị được. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa (bao gồm nhắm mục tiêu theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` đã mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", locale = "en") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", "en", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Android SDK 2.11.3 trở lên ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con phân tách bằng dấu trừ (**-**). Thẻ đầ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 đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp sự 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 bất kể kết nối internet của họ kém đến đâu. Cache được cập nhật thường xuyên nên có thể sử dụng an toàn trong phiên để tránh các yêu cầu mạng.

Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện 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. Hero image và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác nhau cho một số người dùng. - Hiển thị ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị ảnh xem trước trước khi phát video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty Android SDK lên phiên bản 3.7.0 trở lên. ::: Đây là ví dụ về cách bạn có thể cung cấp các tài nguyên tùy chỉnh thông qua một dictionary đơn giản: ```kotlin showLineNumbers val customAssets = AdaptyCustomAssets.of( "hero_image" to AdaptyCustomImageAsset.remote( url = "https://example.com/image.jpg", preview = AdaptyCustomImageAsset.file( FileLocation.fromAsset("images/hero_image_preview.png"), ) ), "hero_video" to AdaptyCustomVideoAsset.file( FileLocation.fromResId(requireContext(), R.raw.custom_video), preview = AdaptyCustomImageAsset.file( FileLocation.fromResId(requireContext(), R.drawable.video_preview), ), ), ) val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, insets, customAssets, ) ``` :::note Nếu không tìm thấy asset, paywall sẽ hiển thị với giao diện mặc định của nó. :::
--- # File: android-present-paywalls --- --- title: "Hiển thị flows & paywalls - Android" description: "Trình bày flows và paywalls cho người dùng trong ứng dụng Android của bạn." --- Nếu bạn đã tạo một flow hoặc paywall, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall đó đã chứa cả nội dung cần hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này dành cho các flow và **paywall xây dựng bằng Paywall Builder mới** được render bởi Adapty. Quy trình sẽ khác đối với paywall dùng Remote Config và [Observer mode](observer-vs-full-mode). - Để hiển thị **paywall Remote Config**, xem [Hiển thị paywall được thiết kế bằng Remote Config](present-remote-config-paywalls). - Để hiển thị **paywall ở chế độ Observer**, xem [Android - Hiển thị paywall Paywall Builder ở chế độ Observer](android-present-paywall-builder-paywalls-in-observer-mode) ::: Để lấy đối tượng `flowConfiguration` dùng bên dưới, xem [Lấy flows & paywalls](android-get-pb-paywalls). Để hiển thị flow trực quan trên màn hình thiết bị, bạn phải cấu hình nó trước. Để làm điều này, hãy gọi phương thức `AdaptyUI.getFlowView()` hoặc tạo `AdaptyFlowView` trực tiếp: ```kotlin showLineNumbers val flowView = AdaptyUI.getFlowView( activity, flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, ) ``` ```kotlin showLineNumbers val flowView = AdaptyFlowView(activity) // or retrieve it from xml ... with(flowView) { showFlow( flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, ) } ``` ```java showLineNumbers AdaptyFlowView flowView = AdaptyUI.getFlowView( activity, flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver ); ``` ```java showLineNumbers AdaptyFlowView flowView = new AdaptyFlowView(activity); //add to the view hierarchy if needed, or you receive it from xml ... flowView.showFlow(flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver); ``` ```xml showLineNumbers ``` Sau khi view được tạo thành công, bạn có thể thêm nó vào hệ thống phân cấp view và hiển thị lên màn hình thiết bị. Nếu bạn lấy `AdaptyFlowView` _không_ thông qua `AdaptyUI.getFlowView()`, bạn cũng cần gọi phương thức `.showFlow()`. Để hiển thị flow trực quan trên màn hình thiết bị, bạn cần cấu hình nó trước. Để làm điều này, hãy sử dụng composable function sau: ```kotlin showLineNumbers AdaptyFlowScreen( flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, ) ``` Các tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------------------------- | :------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **flowConfiguration** | bắt buộc | Cung cấp một đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow. Dùng phương thức `AdaptyUI.getFlowConfiguration(flow)` để tải nó. Xem thêm tại chủ đề [Fetch the view configuration](android-get-pb-paywalls#fetch-the-view-configuration). | | **products** | tùy chọn | Cung cấp một mảng `AdaptyPaywallProduct` để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền `null`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **eventListener** | tùy chọn | Cung cấp một `AdaptyFlowEventListener` để lắng nghe các sự kiện của flow. Nên dùng `AdaptyFlowDefaultEventListener` để tiện hơn. Xem thêm tại chủ đề [Handle flow & paywall events](android-handling-events). | | **insets** | tùy chọn |

Insets là khoảng trống xung quanh flow giúp các phần tử có thể nhấn không bị ẩn sau thanh hệ thống.

Mặc định: `Unspecified` — Adapty sẽ tự động điều chỉnh insets, phù hợp với các flow hiển thị toàn màn hình (edge-to-edge).

Nếu flow của bạn không phải edge-to-edge, bạn có thể muốn đặt insets tùy chỉnh. Xem hướng dẫn tại phần [Change flow insets](android-present-paywalls#change-flow-insets) bên dưới.

| | **customAssets** | tùy chọn | Truyền một đối tượng `AdaptyCustomAssets` để thay thế hình ảnh và video trong flow hoặc paywall của bạn lúc runtime. Xem thêm tại [Customize assets](android-get-pb-paywalls#customize-assets). | | **tagResolver** | tùy chọn | Dùng `AdaptyUiTagResolver` để xử lý các thẻ tùy chỉnh trong văn bản của flow. Resolver này nhận một tham số tag và chuyển đổi nó thành chuỗi tương ứng. Xem thêm tại chủ đề Custom tags in Paywall Builder. | | **timerResolver** | tùy chọn | Truyền resolver vào đây nếu bạn muốn dùng chức năng hẹn giờ tùy chỉnh. | :::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. ::: ## Thay đổi inset của flow \{#change-flow-insets\} Inset là khoảng cách xung quanh flow, giúp các phần tử có thể nhấn không bị che khuất bởi thanh hệ thống. Mặc định, Adapty sẽ tự động điều chỉnh inset, phù hợp với các flow hiển thị toàn màn hình (edge-to-edge). Nếu flow của bạn không phải edge-to-edge, bạn có thể muốn đặt inset tùy chỉnh: - Nếu cả thanh trạng thái lẫn thanh điều hướng đều không chồng lên `AdaptyFlowView`, hãy dùng `AdaptyFlowInsets.None`. - Với các trường hợp phức tạp hơn, ví dụ như flow chồng lên thanh trạng thái phía trên nhưng không chồng lên phần dưới, bạn có thể chỉ đặt `bottomInset` về `0` như ví dụ dưới đây: ```kotlin showLineNumbers //create extension function fun View.onReceiveSystemBarsInsets(action: (insets: Insets) -> Unit) { ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) ViewCompat.setOnApplyWindowInsetsListener(this, null) action(systemBarInsets) insets } } //and then use it with the view flowView.onReceiveSystemBarsInsets { insets -> val flowInsets = AdaptyFlowInsets.vertical(insets.top, 0) flowView.showFlow( flowConfiguration, products, eventListener, flowInsets, customAssets, tagResolver, timerResolver, ) } ``` ```java showLineNumbers ... ViewCompat.setOnApplyWindowInsetsListener(flowView, (view, insets) -> { Insets systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); ViewCompat.setOnApplyWindowInsetsListener(flowView, null); AdaptyFlowInsets flowInsets = AdaptyFlowInsets.vertical(systemBarInsets.top, 0); flowView.showFlow(flowConfiguration, products, eventListener, flowInsets); return insets; }); ``` ## Sử dụng timer do nhà phát triển định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer do nhà phát triển định nghĩa trong ứng dụng mobile của bạn, hãy tạo một đối tượng `timerResolver`—một dictionary hoặc map liên kết các timer tùy chỉnh với các giá trị chuỗi sẽ thay thế chúng khi flow được render. Đây là ví dụ: ```kotlin showLineNumbers ... val customTimers = mapOf( "CUSTOM_TIMER_NY" to Calendar.getInstance(TimeZone.getDefault()).apply { set(2025, 0, 1) }.time, // New Year 2025 ) val timerResolver = AdaptyUiTimerResolver { timerId -> customTimers.getOrElse(timerId, { Date(System.currentTimeMillis() + 3600 * 1000L) /* in 1 hour */ } ) } ``` ```java showLineNumbers ... Map customTimers = new HashMap<>(); customTimers.put( "CUSTOM_TIMER_NY", new Calendar.Builder().setTimeZone(TimeZone.getDefault()).setDate(2025, 0, 1).build().getTime() ); AdaptyUiTimerResolver timerResolver = new AdaptyUiTimerResolver() { @NonNull @Override public Date timerEndAtDate(@NonNull String timerId) { Date date = customTimers.get(timerId); return date != null ? date : new Date(System.currentTimeMillis() + 3600 * 1000L); /* in 1 hour */ } }; ``` Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của timer do lập trình viên tự định nghĩa, được thiết lập trong Adapty dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật timer động với giá trị chính xác—chẳng hạn như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, ví dụ như Ngày đầu năm mới, trừ đi thời điểm hiện tại). ## Sử dụng custom tag \{#use-custom-tags\} Để sử dụng custom tag trong ứng dụng mobile, hãy tạo một đối tượng `tagResolver`—một dictionary hoặc map liên kết các custom tag với giá trị chuỗi sẽ thay thế chúng khi flow được render. Ví dụ: ```kotlin showLineNumbers val customTags = mapOf("USERNAME" to "John") val tagResolver = AdaptyUiTagResolver { tag -> customTags[tag] } ``` ```java showLineNumbers Map customTags = new HashMap<>(); customTags.put("USERNAME", "John"); AdaptyUiTagResolver tagResolver = customTags::get; ``` Trong ví dụ này, `USERNAME` là custom tag bạn đã nhập trong Adapty dashboard dưới dạng ``. `tagResolver` đảm bảo rằng ứng dụng của bạn tự động thay thế custom tag này bằng giá trị được chỉ định—ví dụ như `John`. Chúng tôi khuyến nghị tạo và điền dữ liệu vào `tagResolver` ngay trước khi hiển thị flow của bạn. Sau khi sẵn sàng, hãy truyền nó vào phương thức AdaptyUI mà bạn dùng để hiển thị flow. ## Thay đổi màu chỉ báo tải của flow \{#change-flow-loading-indicator-color\} Bạn có thể ghi đè màu mặc định của chỉ báo tải theo cách sau: ```xml showLineNumbers title = "XML" ```
Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này chỉ dành cho **paywall dùng Paywall Builder mới** yêu cầu SDK v3.0. Quy trình hiển thị paywall sẽ khác nhau tùy theo phiên bản Paywall Builder được dùng để thiết kế, paywall dùng Remote Config, và [chế độ Observer](observer-vs-full-mode). - Để hiển thị **paywall dùng Remote Config**, xem [Render paywall được thiết kế bằng Remote Config](present-remote-config-paywalls). - Để hiển thị **paywall ở chế độ Observer**, xem [Android - Hiển thị paywall Paywall Builder trong chế độ Observer](android-present-paywall-builder-paywalls-in-observer-mode) ::: Để lấy đối tượng `viewConfiguration` được dùng bên dưới, xem [Tải paywall Paywall Builder và cấu hình của chúng](android-get-pb-paywalls). Để hiển thị paywall trực quan trên màn hình thiết bị, trước tiên bạn cần cấu hình nó. Để làm điều này, hãy gọi phương thức `AdaptyUI.getPaywallView()` hoặc tạo `AdaptyPaywallView` trực tiếp: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver, ) ``` ```kotlin showLineNumbers val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { showPaywall( viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver ); ``` ```java showLineNumbers AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.showPaywall(viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver); ``` ```xml showLineNumbers ``` Sau khi view được tạo thành công, bạn có thể thêm nó vào view hierarchy và hiển thị lên màn hình thiết bị. Nếu bạn nhận được `AdaptyPaywallView` _không_ thông qua việc gọi `AdaptyUI.getPaywallView()`, bạn cũng cần gọi thêm phương thức `.showPaywall()`. Để hiển thị paywall trực quan lên màn hình thiết bị, trước tiên bạn cần cấu hình nó. Sử dụng composable function sau: ```kotlin showLineNumbers AdaptyPaywallScreen( viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver, ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------------------------- | :------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **viewConfiguration** | bắt buộc | Cung cấp đối tượng `AdaptyUI.LocalizedViewConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `Adapty.getViewConfiguration(paywall)` để tải nó. Xem thêm tại chủ đề [Lấy cấu hình hiển thị của paywall](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). | | **products** | tùy chọn | Cung cấp một mảng `AdaptyPaywallProduct` để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền `null`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | | **eventListener** | tùy chọn | Cung cấp `AdaptyUiEventListener` để lắng nghe các sự kiện của paywall. Nên kế thừa `AdaptyUiDefaultEventListener` để dễ sử dụng hơn. Xem thêm tại chủ đề [Xử lý sự kiện paywall](android-handling-events). | | **insets** | tùy chọn |

Insets là khoảng trống xung quanh paywall giúp các phần tử có thể nhấn không bị ẩn sau thanh hệ thống.

Mặc định: `UNSPECIFIED` — Adapty sẽ tự động điều chỉnh insets, phù hợp với các paywall hiển thị toàn màn hình (edge-to-edge).

Nếu paywall của bạn không phải edge-to-edge, bạn có thể muốn đặt insets tùy chỉnh. Cách thực hiện xem tại phần [Thay đổi insets của paywall](android-present-paywalls#change-paywall-insets) bên dưới.

| | **personalizedOfferResolver** | tùy chọn | Để chỉ định giá cá nhân hóa ([đọc thêm](https://developer.android.com/google/play/billing/integrate#personalized-price)), hãy implement `AdaptyUiPersonalizedOfferResolver` và truyền logic của bạn để ánh xạ `AdaptyPaywallProduct` thành `true` nếu giá sản phẩm được cá nhân hóa, ngược lại là `false`. | | **tagResolver** | tùy chọn | Sử dụng `AdaptyUiTagResolver` để xử lý các thẻ tùy chỉnh trong văn bản của paywall. Resolver này nhận tham số là một thẻ và chuyển đổi nó thành chuỗi tương ứng. Xem thêm tại chủ đề Custom tags in Paywall Builder. | | **timerResolver** | tùy chọn | Truyền resolver vào đây nếu bạn muốn sử dụng chức năng hẹn giờ tùy chỉnh. | :::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. ::: ## Thay đổi insets của paywall \{#change-paywall-insets\} Insets là khoảng trống xung quanh paywall, giúp các phần tử có thể nhấn không bị ẩn sau thanh hệ thống. Theo mặc định, Adapty sẽ tự động điều chỉnh insets, phù hợp với các paywall hiển thị toàn màn hình (edge-to-edge). Nếu paywall của bạn không hiển thị toàn màn hình, bạn có thể muốn đặt insets tùy chỉnh: - Nếu cả thanh trạng thái lẫn thanh điều hướng đều không chồng lên `AdaptyPaywallView`, hãy dùng `AdaptyPaywallInsets.NONE`. - Với các thiết lập tùy chỉnh hơn, chẳng hạn khi paywall chồng lên thanh trạng thái phía trên nhưng không chồng lên phần dưới, bạn có thể chỉ đặt `bottomInset` thành `0` như ví dụ dưới đây: ```kotlin showLineNumbers //create extension function fun View.onReceiveSystemBarsInsets(action: (insets: Insets) -> Unit) { ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) ViewCompat.setOnApplyWindowInsetsListener(this, null) action(systemBarInsets) insets } } //and then use it with the view paywallView.onReceiveSystemBarsInsets { insets -> val paywallInsets = AdaptyPaywallInsets.vertical(insets.top, 0) paywallView.showPaywall( viewConfiguration, products, eventListener, paywallInsets, personalizedOfferResolver, tagResolver, timerResolver, ) } ``` ```java showLineNumbers ... ViewCompat.setOnApplyWindowInsetsListener(paywallView, (view, insets) -> { Insets systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); ViewCompat.setOnApplyWindowInsetsListener(paywallView, null); AdaptyPaywallInsets paywallInsets = AdaptyPaywallInsets.of(systemBarInsets.top, 0); paywallView.showPaywall(paywall, products, viewConfiguration, paywallInsets, productTitleResolver); return insets; }); ``` ## Sử dụng timer do nhà phát triển định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer do nhà phát triển định nghĩa trong ứng dụng của bạn, hãy tạo một đối tượng `timerResolver`—một dictionary hoặc map ánh xạ các timer tùy chỉnh với các giá trị chuỗi sẽ thay thế chúng khi paywall được render. Đây là một ví dụ: ```kotlin showLineNumbers ... val customTimers = mapOf( "CUSTOM_TIMER_NY" to Calendar.getInstance(TimeZone.getDefault()).apply { set(2025, 0, 1) }.time, // New Year 2025 ) val timerResolver = AdaptyUiTimerResolver { timerId -> customTimers.getOrElse(timerId, { Date(System.currentTimeMillis() + 3600 * 1000L) /* in 1 hour */ } ) } ``` ```java showLineNumbers ... Map customTimers = new HashMap<>(); customTimers.put( "CUSTOM_TIMER_NY", new Calendar.Builder().setTimeZone(TimeZone.getDefault()).setDate(2025, 0, 1).build().getTime() ); AdaptyUiTimerResolver timerResolver = new AdaptyUiTimerResolver() { @NonNull @Override public Date timerEndAtDate(@NonNull String timerId) { Date date = customTimers.get(timerId); return date != null ? date : new Date(System.currentTimeMillis() + 3600 * 1000L); /* in 1 hour */ } }; ``` Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của timer do developer định nghĩa mà bạn đã cài đặt trong Adapty dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật timer động với giá trị chính xác—chẳng hạn như `13d 09h 03m 34s` (được tính bằng thời gian kết thúc của timer, như Ngày Đầu Năm Mới, trừ đi thời gian hiện tại). ## Sử dụng custom tag \{#use-custom-tags\} Để sử dụng custom tag trong ứng dụng di động, hãy tạo một đối tượng `tagResolver`—một dictionary hoặc map ánh xạ các custom tag với giá trị chuỗi sẽ thay thế chúng khi paywall được hiển thị. Dưới đây là ví dụ: ```kotlin showLineNumbers val customTags = mapOf("USERNAME" to "John") val tagResolver = AdaptyUiTagResolver { tag -> customTags[tag] } ``` ```java showLineNumbers Map customTags = new HashMap<>(); customTags.put("USERNAME", "John"); AdaptyUiTagResolver tagResolver = customTags::get; ``` Trong ví dụ này, `USERNAME` là custom tag bạn đã nhập trong Adapty dashboard dưới dạng ``. `tagResolver` đảm bảo rằng ứng dụng của bạn sẽ tự động thay thế custom tag này bằng giá trị được chỉ định—ví dụ như `John`. Chúng tôi khuyến nghị bạn tạo và điền dữ liệu vào `tagResolver` ngay trước khi hiển thị paywall. Sau khi đã sẵn sàng, hãy truyền nó vào phương thức AdaptyUI mà bạn sử dụng để hiển thị paywall. ## Thay đổi màu của loading indicator trên paywall \{#change-paywall-loading-indicator-color\} Bạn có thể ghi đè màu mặc định của loading indicator theo cách sau: ```xml showLineNumbers title = "XML" ```
--- # File: android-handle-paywall-actions --- --- title: "Phản hồi các hành động trong flow - Android" description: "Xử lý các hành động nút từ flow và paywall trong ứng dụng Android của bạn." --- Nếu bạn đang xây dựng flow hoặc paywall bằng Adapty Flow Builder hoặc 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 builder](paywall-buttons) và gán cho nó một action có sẵn hoặc tạo một custom action ID. 2. Viết code trong app để xử lý từng action bạn đã gán. Hướng dẫn này trình bày cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Chỉ có các hành động mua hàng, khôi phục, đóng flow/paywall và mở URL được xử lý tự động.** Tất cả các button action khác đều cần được xử lý đúng cách trong code của app. ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall: 1. Trong builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, implement một handler cho hành động `close`. :::info Trong Android SDK, hành động `close` sẽ tự động đóng flow hoặc paywall theo mặc định. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một flow có thể kích hoạt mở một flow khác. ::: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { AdaptyUI.Action.Close -> (context as? Activity)?.onBackPressed() // default behavior } } ``` ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch mua), hãy thêm phần tử **Link** trong builder và xử lý nó giống như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết từ flow hoặc paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng của bạn, triển khai trình xử lý cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong Android SDK, hành động `openUrl` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.OpenUrl -> { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(action.url)) // default behavior context.startActivity(intent) } } } ``` ## Xử lý hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong builder, thêm một nút, gán cho nó hành động **Custom**, và đặt cho nó một ID. 2. Trong code của ứng dụng, triển khai handler cho ID hành động bạn đã tạo. Ví dụ: nếu bạn có thêm một bộ ưu đãi đăng ký hoặc sản phẩm mua một lần, bạn có thể thêm một nút sẽ hiển thị một flow hoặc paywall khác: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.Custom -> { if (action.customId == "openNewPaywall") { // Display another flow or paywall } } } } ``` 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 bấm đúng cách là rất quan trọng: 1. Thêm [nút trong paywall builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo ID hành động tùy chỉnh. 2. Viết code trong ứng dụng để xử lý từng hành động bạn đã gán. Hướng dẫn này trình bày cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn. :::warning **Chỉ các hành động mua hàng, khôi phục, đóng paywall và mở URL được xử lý tự động.** Tất cả các hành động nút khác đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: ## Đó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ó action **Close**. 2. Trong code của ứng dụng, hãy implement một handler cho action `close` để đóng paywall. :::info Trong Android SDK, action `close` sẽ tự động kích hoạt việc đóng paywall theo mặc định. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một paywall có thể kích hoạt mở một paywall khác. ::: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { AdaptyUI.Action.Close -> (context as? Activity)?.onBackPressed() // default behavior } } ``` ## Mở URL từ paywall \{#open-urls-from-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch), hãy thêm phần tử **Link** trong paywall builder và xử lý nó giống như các nút có hành động **Open URL**. ::: Để thêm một nút mở liên kết từ paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong paywall builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng, triển khai một handler cho action `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong Android SDK, hành động `openUrl` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.OpenUrl -> { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(action.url)) // default behavior context.startActivity(intent) } } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút đăng nhập cho người dùng vào ứng dụng của bạn: 1. Trong paywall builder, thêm một nút và gán cho nó hành động **Login**. 2. Trong code ứng dụng, triển khai một handler cho hành động `login` để xác định người dùng của bạn. ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { AdaptyUI.Action.Login -> { val intent = Intent(context, LoginActivity::class.java) context.startActivity(intent) } } } ``` ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và gán cho nó một ID. 2. Trong code ứng dụng của bạn, triển khai một handler cho action ID mà bạn vừa tạo. Ví dụ: nếu bạn có một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút để hiển thị paywall khác: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.Custom -> { if (action.customId == "openNewPaywall") { // Display another paywall } } } } ``` --- # File: android-handling-events --- --- title: "Xử lý sự kiện flow & paywall - Android" description: "Xử lý các sự kiện flow và paywall trong ứng dụng Android của bạn." --- :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các lần mua hàng, khôi phục, chọn sản phẩm và hiển thị flow. Bạn cũng cần triển khai xử lý nút bấm (đóng flow, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](android-handle-paywall-actions) của chúng tôi để biết thêm chi tiết. ::: Flow và paywall được cấu hình bằng [Flow Builder](adapty-flow-builder) hoặc [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. Tìm hiểu cách phản hồi các sự kiện này bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, 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. ::: Nếu bạn cần kiểm soát hoặc theo dõi các quy trình diễn ra trên màn hình thanh toán, hãy triển khai các phương thức `AdaptyFlowEventListener`. Nếu bạn muốn giữ nguyên hành vi mặc định trong một số trường hợp, bạn có thể extend `AdaptyFlowDefaultEventListener` và chỉ override những method bạn muốn thay đổi. Dưới đây là các giá trị mặc định từ `AdaptyFlowDefaultEventListener`. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### 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: ```kotlin showLineNumbers title="Kotlin" public override fun onProductSelected( product: AdaptyPaywallProduct, context: Context, ) {} ```
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" } } ```
#### Bắt đầu mua hàng \{#started-purchase\} Nếu người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseStarted( product: AdaptyPaywallProduct, context: Context, ) {} ```
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" } } ```
Phương thức này sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [Android - Hiển thị paywall Paywall Builder trong chế độ Observer](android-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Mua thành công, đã hủy hoặc đang chờ xử lý \{#successful-canceled-or-pending-purchase\} Nếu giao dịch mua thành công, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseFinished( purchaseResult: AdaptyPurchaseResult, product: AdaptyPaywallProduct, context: Context, ) { if (purchaseResult !is AdaptyPurchaseResult.UserCanceled) context.getActivityOrNull()?.onBackPressed() } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript // Successful purchase { "purchaseResult": { "type": "Success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // Cancelled purchase { "purchaseResult": { "type": "UserCanceled" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // Pending purchase { "purchaseResult": { "type": "Pending" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
Chúng tôi khuyến nghị đóng màn hình trong trường hợp đó. Phương thức này sẽ không được gọi trong Observer mode. Tham khảo chủ đề [Android - Hiển thị paywall Paywall Builder trong Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Giao dịch mua thất bại \{#failed-purchase\} Nếu một giao dịch thất bại do lỗi, phương thức này sẽ được gọi. Điều này bao gồm các lỗi Google Play Billing (hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng), lỗi xác minh giao dịch và lỗi hệ thống. Lưu ý rằng khi người dùng hủy, `onPurchaseFinished` sẽ được gọi với kết quả đã hủy thay thế, và các giao dịch đang chờ xử lý không kích hoạt phương thức này. ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseFailure( error: AdaptyError, product: AdaptyPaywallProduct, context: Context, ) {} ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
Phương thức này sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [Android - Hiển thị paywall Paywall Builder trong chế độ Observer](android-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Hoàn tất điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi có một lần thử mở [web paywall](web-paywall) cho một sản phẩm cụ thể. Điều này bao gồm cả các lần điều hướng thành công lẫn thất bại: ```kotlin showLineNumbers title="Kotlin" public override fun onFinishWebPaymentNavigation( product: AdaptyPaywallProduct?, error: AdaptyError?, context: Context, ) {} ``` **Tham số:** | Tham số | Mô tả | |:------------|:---------------------------------------------------------------------------------------------------------------| | **product** | Một `AdaptyPaywallProduct` mà web paywall đã được mở. Có thể là `null`. | | **error** | Một đối tượng `AdaptyError` nếu điều hướng web paywall thất bại; `null` nếu điều hướng thành công. |
Ví dụ về 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" } } } ```
#### Khôi phục thành công \{#successful-restore\} Nếu việc khôi phục giao dịch mua thành công, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreSuccess( profile: AdaptyProfile, context: Context, ) {} ```
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ý](android-listen-subscription-changes) để tìm hiểu cách kiểm tra. #### Khôi phục thất bại \{#failed-restore\} Nếu `Adapty.restorePurchases()` thất bại, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreFailure( error: AdaptyError, context: Context, ) {} ```
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" } } } ```
#### Nâng cấp gói đăng ký \{#upgrade-subscription\} Khi người dùng cố mua một gói đăng ký mới trong khi đã có gói đăng ký đang hoạt động, bạn có thể kiểm soát cách xử lý giao dịch mới bằng cách ghi đè phương thức này. Bạn có hai lựa chọn: 1. **Thay thế gói đăng ký hiện tại** bằng gói mới: ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyFlowEventListener.PurchaseParamsCallback, ): AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived( AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) .build() ) return AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` 2. **Giữ cả hai gói đăng ký** (thêm gói mới riêng biệt): ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyFlowEventListener.PurchaseParamsCallback, ): AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived(AdaptyPurchaseParameters.Empty) return AdaptyFlowEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` :::note Nếu bạn không override phương thức này, hành vi mặc định là giữ nguyên cả hai gói đăng ký (tương đương với việc sử dụng `AdaptyPurchaseParameters.Empty`). ::: Bạn cũng có thể đặt thêm các tham số mua hàng nếu cần: ```kotlin AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) // optional - for replacing current subscription .withOfferPersonalized(true) // optional - if using personalized pricing .build() ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "product": { "vendorProductId": "premium_yearly", "localizedTitle": "Premium Yearly", "localizedDescription": "Premium subscription for 1 year", "localizedPrice": "$99.99", "price": 99.99, "currencyCode": "USD" }, "subscriptionUpdateParams": { "replacementMode": "with_time_proration" } } ```
### 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 sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ server. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức sau: ```kotlin showLineNumbers title="Kotlin" public override fun onLoadingProductsFailure( error: AdaptyError, context: Context, ): Boolean = false ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
Nếu bạn trả về `true`, AdaptyUI sẽ lặp lại yêu cầu sau 2 giây. #### Lỗi kết xuất giao diện \{#rendering-errors\} Nếu xảy ra lỗi trong quá trình kết xuất giao diện, lỗi đó sẽ được báo cáo thông qua phương thức này: ```kotlin showLineNumbers title="Kotlin" public override fun onError( error: AdaptyError, context: Context, ) {} ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render flow interface", "details": { "underlyingError": "Invalid flow configuration" } } } ```
Trong điều kiện bình thường, các lỗi này sẽ không xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết. ### Điều hướng \{#navigation\} #### Nút back của hệ thống \{#system-back-button\} Theo mặc định, người dùng không thể thoát khỏi một flow bằng nút back của hệ thống hoặc thao tác vuốt để quay lại — họ thoát thông qua một đường dẫn bạn định nghĩa, chẳng hạn như nút **Close** hoặc một hành động `on_device_back` trong builder. Nếu bạn muốn nút back của hệ thống có thể đóng flow, hãy override `onBackPressed` và trả về `false` để activity hoặc fragment host xử lý sự kiện nhấn đó: ```kotlin showLineNumbers title="Kotlin" public override fun onBackPressed(context: Context): Boolean { return false // let the host handle the back press (e.g. finish the activity or pop the fragment) } ``` Callback này chỉ được gọi khi không có hành động `on_device_back` nào được cấu hình cho màn hình hiện tại — hành động đã cấu hình sẽ được ưu tiên và xử lý nội bộ. Trả về `true` để tiêu thụ sự kiện nhấn (hành vi mặc định), hoặc `false` để cho phép logic xử lý back của host tự chạy. ### Các sự kiện dành riêng \{#reserved-events\} `AdaptyFlowEventListener` khai báo một số callback cho các tính năng mà flow chưa sử dụng. Bạn không cần triển khai chúng — `AdaptyFlowDefaultEventListener` đã cung cấp sẵn các giá trị mặc định không làm gì cả. | Phương thức | Mô tả | |:-------|:------------| | **onAnalyticEvent** | Dành riêng cho các sự kiện phân tích tùy chỉnh từ một flow. Hiện tại các flow chưa phát ra sự kiện này tới code của bạn, nên bạn chưa cần implement. | | **onShowAppRate** | Dành riêng cho các yêu cầu đánh giá ứng dụng từ một flow. Hiện tại các flow chưa kích hoạt yêu cầu đánh giá ứng dụng, nên bạn chưa cần implement. | | **onShowRequestPermission** | Dành riêng cho các yêu cầu cấp quyền hệ thống (như thông báo đẩy hoặc quyền truy cập camera) từ một flow. Hiện tại các flow chưa kích hoạt yêu cầu cấp quyền, nên bạn chưa cần implement. |
:::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, lựa chọn sản phẩm và hiển thị 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](android-handle-paywall-actions) của chúng tôi để biết thêm chi tiết. ::: Paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm các thao tác nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện trên paywall. Hãy 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. ::: :::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. ::: Nếu bạn cầ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 mua hàng, hãy implement các phương thức của `AdaptyUiEventListener`. Nếu bạn muốn giữ nguyên hành vi mặc định trong một số trường hợp, bạn có thể extend `AdaptyUiDefaultEventListener` và chỉ override những phương thức bạn muốn thay đổi. Dưới đây là các giá trị mặc định từ `AdaptyUiDefaultEventListener`. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### 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: ```kotlin showLineNumbers title="Kotlin" public override fun onProductSelected( product: AdaptyPaywallProduct, context: Context, ) {} ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Bắt đầu mua hàng \{#started-purchase\} Nếu người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseStarted( product: AdaptyPaywallProduct, context: Context, ) {} ```
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" } } ```
Phương thức này sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Mua hàng thành công, đã hủy hoặc đang chờ xử lý \{#successful-canceled-or-pending-purchase\} Nếu mua hàng thành công, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseFinished( purchaseResult: AdaptyPurchaseResult, product: AdaptyPaywallProduct, context: Context, ) { if (purchaseResult !is AdaptyPurchaseResult.UserCanceled) context.getActivityOrNull()?.onBackPressed() } ```
Ví dụ về sự kiện (Nhấn để mở rộng) ```javascript // Successful purchase { "purchaseResult": { "type": "Success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // Cancelled purchase { "purchaseResult": { "type": "UserCanceled" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // Pending purchase { "purchaseResult": { "type": "Pending" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
Chúng tôi khuyến nghị bạn đóng màn hình trong trường hợp đó. Phương thức này sẽ không được gọi trong Observer mode. Tham khảo chủ đề [Android - Hiển thị paywall Paywall Builder trong Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Mua hàng thất bại \{#failed-purchase\} Nếu một giao dịch thất bại do lỗi, phương thức này sẽ được gọi. Điều này bao gồm các lỗi Google Play Billing (hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng), lỗi xác minh giao dịch, và lỗi hệ thống. Lưu ý rằng khi người dùng hủy giao dịch, `onPurchaseFinished` sẽ được gọi với kết quả đã hủy thay thế, và các khoản thanh toán đang chờ xử lý sẽ không kích hoạt phương thức này. ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseFailure( error: AdaptyError, product: AdaptyPaywallProduct, context: Context, ) {} ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
Phương thức này sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [Android - Hiển thị paywall Paywall Builder trong chế độ Observer](android-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Hoàn tất điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi có một lần thử mở [web paywall](web-paywall) cho một sản phẩm cụ thể. Điều này bao gồm cả các lần điều hướng thành công lẫn thất bại: ```kotlin showLineNumbers title="Kotlin" public override fun onFinishWebPaymentNavigation( product: AdaptyPaywallProduct?, error: AdaptyError?, context: Context, ) {} ``` **Tham số:** | Tham số | Mô tả | |:------------|:---------------------------------------------------------------------------------------------------------------| | **product** | Một `AdaptyPaywallProduct` mà web paywall được mở cho sản phẩm đó. Có thể là `null`. | | **error** | Một đối tượng `AdaptyError` nếu điều hướng web paywall thất bại; `null` nếu điều hướng thành công. |
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" } } } ```
#### Khôi phục thành công \{#successful-restore\} Nếu việc khôi phục giao dịch thành công, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreSuccess( profile: AdaptyProfile, context: Context, ) {} ```
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 tài liệu [Trạng thái gói đăng ký](android-listen-subscription-changes) để biết cách kiểm tra. #### Khôi phục thất bại \{#failed-restore\} Nếu `Adapty.restorePurchases()` thất bại, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreFailure( error: AdaptyError, context: Context, ) {} ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ```
#### Nâng cấp gói đăng ký \{#upgrade-subscription\} Khi người dùng cố gắng mua một gói đăng ký mới trong khi đã có một gói đăng ký khác đang hoạt động, bạn có thể kiểm soát cách xử lý giao dịch mua mới đó bằng cách override phương thức này. Bạn có hai lựa chọn: 1. **Thay thế gói đăng ký hiện tại** bằng gói mới: ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived( AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) .build() ) return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` 2. **Giữ cả hai gói đăng ký** (thêm gói mới riêng biệt): ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived(AdaptyPurchaseParameters.Empty) return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` :::note Nếu bạn không override phương thức này, hành vi mặc định là giữ cả hai gói đăng ký hoạt động (tương đương với việc sử dụng `AdaptyPurchaseParameters.Empty`). ::: Bạn cũng có thể đặt các tham số mua hàng bổ sung nếu cần: ```kotlin AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) // optional - for replacing current subscription .withOfferPersonalized(true) // optional - if using personalized pricing .build() ``` Nếu một gói đăng ký mới được mua trong khi gói khác vẫn còn hoạt động, hãy override phương thức này để thay thế gói hiện tại bằng gói mới. Nếu gói đăng ký đang hoạt động vẫn phải được giữ nguyên và gói mới được thêm riêng, hãy gọi `onSubscriptionUpdateParamsReceived(null)`: ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingSubscriptionUpdateParams( product: AdaptyPaywallProduct, context: Context, onSubscriptionUpdateParamsReceived: SubscriptionUpdateParamsCallback, ) { onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters(...)) } ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "product": { "vendorProductId": "premium_yearly", "localizedTitle": "Premium Yearly", "localizedDescription": "Premium subscription for 1 year", "localizedPrice": "$99.99", "price": 99.99, "currencyCode": "USD" }, "subscriptionUpdateParams": { "replacementMode": "with_time_proration" } } ```
### 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 sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ server. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức sau: ```kotlin showLineNumbers title="Kotlin" public override fun onLoadingProductsFailure( error: AdaptyError, context: Context, ): Boolean = false ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
Nếu bạn trả về `true`, AdaptyUI sẽ thực hiện lại yêu cầu sau 2 giây. #### Lỗi rendering \{#rendering-errors\} Nếu xảy ra lỗi trong quá trình render giao diện, lỗi đó sẽ được báo cáo qua phương thức này: ```kotlin showLineNumbers title="Kotlin" public override fun onRenderingError( error: AdaptyError, context: Context, ) {} ```
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 trường hợp bình thường, những lỗi này không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết.
--- # File: android-use-fallback-paywalls --- --- title: "Android - 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ợ bởi Android 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. Di chuyển file cấu hình dự phòng vào thư mục `assets` hoặc `res/raw` của dự án Android. 2. Gọi phương thức `.setFallback` **trước khi** bạn fetch flow, paywall hoặc onboarding mục tiêu. ```kotlin showLineNumbers //if you put the 'android_fallback.json' file to the 'assets' directory val location = FileLocation.fromAsset("android_fallback.json") //or `FileLocation.fromAsset("/android_fallback.json")` if you placed it in a child folder of 'assets') //if you put the 'android_fallback.json' file to the 'res/raw' directory val location = FileLocation.fromResId(context, R.raw.android_fallback) //you can also pass a file URI val fileUri: Uri = //get Uri for the file with fallback paywalls val location = FileLocation.fromFileUri(fileUri) //pass the file location Adapty.setFallback(location, callback) ``` ```java showLineNumbers //if you put the 'android_fallback.json' file to the 'assets' directory FileLocation location = FileLocation.fromAsset("android_fallback.json"); //or `FileLocation.fromAsset("/android_fallback.json");` if you placed it in a child folder of 'assets') //if you put the 'android_fallback.json' file to the 'res/raw' directory FileLocation location = FileLocation.fromResId(context, R.raw.android_fallback); //you can also pass a file URI Uri fileUri = //get Uri for the file with fallback paywalls FileLocation location = FileLocation.fromFileUri(fileUri); //pass the file location Adapty.setFallback(location, callback); ``` | Tham số | Mô tả | | :----------- | :----------------------------------------------------------- | | **location** | Đối tượng [FileLocation](https://android.adapty.io/adapty/com.adapty.utils/-file-location/-companion/) cho file cấu hình paywall dự phòng | --- # File: android-localizations-and-locale-codes --- --- title: "Sử dụng localizations và locale codes trong Android SDK" description: "Quản lý localizations và locale codes của ứng dụng để tiếp cận người dùng toàn cầu (Android)." --- ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một vài trường hợp mà locale codes phát huy tác dụng — ví dụ, khi bạn cần lấy đúng paywall cho localization hiện tại của ứng dụng. Vì locale codes khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi sử dụng một chuẩn nội bộ thống nhất cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng localization — như vậy bạn sẽ luôn nhận được kết quả như mong đợi. ## Chuẩn locale code tại Adapty \{#locale-code-standard-at-adapty\} Đối với locale codes, Adapty sử dụng phiên bản điều chỉnh nhẹ của [chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi code gồm các subtag viết thường, phân cách bằng dấu gạch ngang. 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 locale code \{#locale-code-matching\} Khi Adapty nhận được lời gọi từ SDK phía client kèm theo locale code và bắt đầu tìm kiếm localization tương ứng của một 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. Chúng tôi tìm kiếm localization có locale code khớp hoàn toàn 3. Nếu không tìm thấy, chúng tôi lấy phần chuỗi trước dấu gạch ngang đầu tiên (`pt` với `pt-br`) và tìm kiếm localization khớp 4. Nếu vẫn không tìm thấy, chúng tôi trả về localization mặc định `en` Nhờ đó, 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. ## Cách triển khai localizations được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang quan tâm đến localizations, rất có thể bạn đã làm việc với các file chuỗi đã được dịch trong dự án. Trong trường hợp đó, chúng tôi khuyến nghị bạn thêm một cặp key-value chứa Adapty locale code tương ứng vào từng file localization. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như sau: ```kotlin showLineNumbers // 1. Modify your strings.xml files /* strings.xml - Spanish */ es /* strings.xml - Portuguese (Brazil) */ pt-br // 2. Extract and use the locale code val localeCode = context.getString(R.string.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 tải về cho từng người dùng trong ứng dụng của bạn. ## Cách triển khai localizations 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 tường minh locale codes cho từng localization. Cách này có nghĩa là trích xuất locale code từ các đối tượng mà nền tảng của bạn cung cấp, như sau: ```kotlin showLineNumbers val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) context.resources.configuration.locales[0] else context.resources.configuration.locale val localeCode = locale.toLanguageTag() // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Lưu ý rằng chúng tôi không khuyến nghị cách tiếp cận này vì khó dự đoán chính xác server của Adapty sẽ nhận được gì. Nếu bạn vẫn quyết định sử dụng cách này — hãy đảm bảo bạn đã xử lý tất cả các trường hợp liên quan. --- # File: android-web-paywall --- --- title: "Triển khai web paywall trong Android 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 Play 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.15 trở lên. ::: ## Mở web paywall \{#open-web-paywalls\} Nếu bạn đang làm việc với paywall do bạn 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 giú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. :::note Sau khi người dùng quay lại ứng dụng, hãy làm mới giao diện để phản ánh các cập nhật từ hồ sơ người dùng. Adapty sẽ nhận và xử lý các sự kiện cập nhật hồ sơ người dùng. ::: ```kotlin showLineNumbers Adapty.openWebPaywall( activity = activity, product = product, ) { error -> if (error == null) { // the web paywall was opened successfully } else { // handle the error } } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall(product)` — tạo URL theo paywall và đồng thời thêm dữ liệu sản phẩm vào URL. 2. `openWebPaywall(paywall)` — tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng phiên bản này khi các sản phẩm trong Adapty paywall khác với các sản phẩm trong web paywall. ::: ## Mở web paywall trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} 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 ngay bên trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt tham số `presentation` thành `AdaptyWebPresentation.InAppBrowser`: ```kotlin showLineNumbers Adapty.openWebPaywall( activity = activity, product = product, presentation = AdaptyWebPresentation.InAppBrowser, ) { error -> if (error == null) { // the web paywall was opened successfully } else { // handle the error val adaptyError = error } } ``` --- # File: android-troubleshoot-paywall-builder --- --- title: "Xử lý sự cố Paywall Builder trong Android SDK" description: "Xử lý sự cố Paywall Builder trong Android SDK" --- Hướng dẫn này giúp bạn giải quyết các vấn đề thường gặp khi sử dụng paywall được thiết kế trong Adapty Paywall Builder với Android SDK. ## Không lấy được cấu hình paywall \{#getting-a-paywall-configuration-fails\} **Vấn đề**: Phương thức `getViewConfiguration` không lấy được cấu hình paywall. **Nguyên nhân**: Paywall chưa được bật để hiển thị trên thiết bị trong Paywall Builder. **Giải pháp**: Bật toggle **Show on device** trong Paywall Builder. ## Số lượt xem paywall quá lớn \{#the-paywall-view-number-is-too-big\} **Vấn đề**: Số lượt xem paywall hiển thị gấp đôi so với con số mong đợi. **Nguyên nhân**: Bạn có thể đang gọi `logShowFlow` (Android SDK v4+) / `logShowPaywall` trong code, khiến số lượt xem bị tính trùng nếu bạn đang dùng Paywall Builder hoặc Flow Builder. Với các flow và paywall được xây dựng bằng các công cụ này, analytics đã được theo dõi tự động, nên bạn không cần gọi phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowFlow` (Android SDK v4+) / `logShowPaywall` trong code nếu đang dùng Paywall Builder hoặc Flow Builder. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn gặp phải các sự cố liên quan đến Paywall Builder không được đề cập ở trên. **Giải pháp**: Migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](android-sdk-migration-guides) nếu cần. Nhiều sự cố đã được giải quyết trong các phiên bản SDK mới hơn. --- # File: android-quickstart-manual --- --- title: "Kích hoạt mua hàng trong paywall tùy chỉnh của bạn trên Android SDK" description: "Tích hợp Adapty SDK vào các paywall Android tùy chỉnh của bạn để kích hoạt in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Bạn vẫn toàn quyền kiểm soát việc triển khai paywall, trong khi Adapty SDK lo việc lấy sản phẩm, xử lý các 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 developer đ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 Flow Builder](android-quickstart-paywalls). Với Flow Builder, bạn tạo flow trong trình chỉnh sửa trực quan không cần code, Adapty xử lý toàn bộ logic mua hàng tự động, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để kích hoạt in-app purchase, bạn cần nắm rõ ba khái niệm quan trọng: - [**Sản phẩm**](product) – bất kỳ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – cấu hình xác định sản phẩm nào sẽ được hiển thị. 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. - [**Placements**](placements) – vị trí và thời điểm 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 đó gọi chúng theo 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 từng nhóm người dùng. Hãy đảm bảo bạn hiểu các khái niệm này ngay cả khi bạn dùng paywall tự tạo. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng. Để triển khai paywall tự tạo, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy danh sách sản phẩm. Để hiểu những gì cần làm trong dashboard, hãy xem hướng dẫn quickstart [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc có hoặc không có xác thực backend ở phía mình. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo các cách khác nhau. Đọc [hướng dẫn khởi đầu nhanh về nhận dạng người dùng](android-quickstart-identify) để hiểu rõ sự khác biệt và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy danh sách sản phẩm \{#step-1-get-products\} Để lấy danh sách sản phẩm cho paywall tùy chỉnh của bạn, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền ID [placement](placements) vào phương thức `getFlow`. 2. Lấy mảng sản phẩm cho flow này bằng phương thức `getPaywallProducts`. ```kotlin showLineNumbers fun loadPaywall() { Adapty.getFlow("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val flow = result.value Adapty.getPaywallProducts(flow) { productResult -> when (productResult) { is AdaptyResult.Success -> { val products = productResult.value // Use products to build your custom paywall UI } is AdaptyResult.Error -> { val error = productResult.error // Handle the error } } } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } } ``` ```java showLineNumbers public void loadPaywall() { Adapty.getFlow("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) result).getValue(); Adapty.getPaywallProducts(flow, productResult -> { if (productResult instanceof AdaptyResult.Success) { List products = ((AdaptyResult.Success>) productResult).getValue(); // Use products to build your custom paywall UI } else if (productResult instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) productResult).getError(); // Handle the error } }); } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); } ``` ## Bước 2. Xử lý giao dịch mua \{#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. ```kotlin showLineNumbers fun purchaseProduct(activity: Activity, product: AdaptyPaywallProduct) { Adapty.makePurchase(activity, product) { result -> when (result) { is AdaptyResult.Success -> { when (val purchaseResult = result.value) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // Purchase successful, profile updated } is AdaptyPurchaseResult.UserCanceled -> { // User canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Purchase is pending (e.g., user will pay offline with cash) } } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } } ``` ```java showLineNumbers public void purchaseProduct(Activity activity, AdaptyPaywallProduct product) { Adapty.makePurchase(activity, product, null, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchaseResult purchaseResult = ((AdaptyResult.Success) result).getValue(); if (purchaseResult instanceof AdaptyPurchaseResult.Success) { AdaptyProfile profile = ((AdaptyPurchaseResult.Success) purchaseResult).getProfile(); // Mua hàng thành công, hồ sơ người dùng đã được cập nhật } else if (purchaseResult instanceof AdaptyPurchaseResult.UserCanceled) { // Người dùng đã hủy giao dịch } else if (purchaseResult instanceof AdaptyPurchaseResult.Pending) { // Giao dịch đang chờ xử lý (ví dụ: người dùng sẽ thanh toán offline bằng tiền mặt) } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Xử lý lỗi } }); } ``` ## Bước 3. Khôi phục giao dịch mua \{#step-3-restore-purchases\} Google Play và các cửa hàng ứng dụng khác yêu cầu tất cả ứng dụng có gói đăng ký phải cung cấp cách để người dùng khôi phục giao dịch mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Thao tác này sẽ đồng bộ lịch sử giao dịch mua của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```kotlin showLineNumbers fun restorePurchases() { Adapty.restorePurchases { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // Restore successful, profile updated } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } } ``` ```java showLineNumbers public void restorePurchases() { Adapty.restorePurchases(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // Restore successful, profile updated } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); } ``` ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. [Kiểm tra giao dịch mua hàng trên Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử từ paywall. Để xem cách hoạt động trong một triển khai thực tế, hãy xem [ProductListFragment.kt](https://github.com/adaptyteam/AdaptySDK-Android/blob/master/app/src/main/java/com/adapty/example/ProductListFragment.kt) 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 hàng với xử lý lỗi phù hợp, phản hồi giao diện người dùng và quản lý gói đăng ký. Tiếp theo, [kiểm tra xem người dùng đã hoàn tất giao dịch mua chưa](android-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-android --- --- title: "Lấy paywall và sản phẩm cho paywall remote config trong Android SDK" description: "Lấy paywall và sản phẩm trong Adapty Android SDK để tối ưu hóa doanh thu người dùng." --- Trước khi hiển thị Remote Config và paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và paywall tùy chỉnh. Để được hướng dẫn về cách lấy flow hoặc paywall được tùy chỉnh trong **Flow Builder** hoặc **Paywall Builder**, vui lòng tham khảo [Lấy flow & paywall](android-get-pb-paywalls). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. :::
Trước khi bắt đầu lấy flow và sản phẩm trong ứng dụng di động của bạn (nhấn để mở rộng) 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow hoặc paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow hoặc paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-android) trong ứng dụng di động của bạn.
## Lấy thông tin flow \{#fetch-flow-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các flow và paywall, cho phép bạn trình bày chúng tại các placement cụ thể trong ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một `AdaptyFlow` từ một trong các [placement](placements) của bạn bằng phương thức `getFlow`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Flow được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có sẵn có thể thay đổi bất cứ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay flow trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```kotlin showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val flow = result.value // the requested flow } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) result).getValue(); // the requested flow } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo một placement trong Adapty Dashboard. || **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

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

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

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

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

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

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

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

| Đừng hardcode ID sản phẩm! Vì các flow được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những trường hợp này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị đúng 2 sản phẩm đó. 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 cần hardcode là placement ID. Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, một mảng `remoteConfigs` (một mục cho mỗi ngôn ngữ đã cấu hình), và cờ `hasViewConfiguration`. Để lấy sản phẩm cho flow, gọi `getPaywallProducts(flow)`. | :::note Trong v4, tham số `locale` đã được chuyển ra khỏi `getFlow` và đặt vào `getFlowConfiguration` (chỉ dùng khi render với AdaptyUI). Đối với các paywall tùy chỉnh, tất cả các locale có sẵn đều được trả về cùng nhau trong `flow.remoteConfigs` — hãy chọn locale phù hợp với thiết bị của người dùng hoặc cài đặt trong ứng dụng của bạn. ::: ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có flow, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```kotlin showLineNumbers Adapty.getPaywallProducts(flow) { result -> when (result) { is AdaptyResult.Success -> { val products = result.value // the requested products } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywallProducts(flow, result -> { if (result instanceof AdaptyResult.Success) { List products = ((AdaptyResult.Success>) result).getValue(); // the requested products } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/) với: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký, và một số thuộc tính khác. | Khi triển khai thiết kế flow của riêng bạn, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/). Dưới đây là các thuộc tính được sử dụng phổ biến nhất, nhưng hãy tham khảo tài liệu được liên kết để biết đầy đủ chi tiết về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề của sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, chứ không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, dùng `product.price.localizedString`. Việc bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể lấy giá dưới dạng số thông qua `product.price.amount`. Giá trị sẽ được tính theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm,...), dùng `product.subscriptionDetails?.localizedSubscriptionPeriod`. Việc bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo cách lập trình, dùng `product.subscriptionDetails?.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là DAY, WEEK, MONTH, YEAR, hoặc UNKNOWN). Giá trị `numberOfUnits` sẽ cho bạn biết số lượng đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `MONTH` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ báo cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionDetails?.introductoryOfferPhases`. Đâ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ị `FREE_TRIAL`, `PAY_AS_YOU_GO`, `PAY_UPFRONT` và `UNKNOWN`. Dùng thử miễn phí sẽ có kiểu `FREE_TRIAL`.
• `price`: Giá giảm dưới dạng số. Với dùng thử miễn phí, giá trị này là `0`.
• `localizedNumberOfPeriods`: một chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.
• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy từng chi tiết riêng lẻ của chu kỳ ưu đãi thông qua thuộc tính này. Cách hoạt động tương tự như phần trước đã mô tả.
• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo ngôn ngữ của người dùng. | ## Tăng tốc tải flow với flow đối tượng mặc định \{#speed-up-flow-fetching-with-default-audience-flow\} Thông thường, flow được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow bằng phương thức `getFlow`, như đã trình bày chi tiết trong phần [Lấy thông tin flow](fetch-paywalls-and-products-android#fetch-flow-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị dùng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa targeting (bao gồm dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy dùng `getFlow` đã mô tả [ở trên](fetch-paywalls-and-products-android#fetch-flow-information). ::: ```kotlin showLineNumbers Adapty.getFlowForDefaultAudience("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val flow = result.value // the requested flow } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getFlowForDefaultAudience("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) result).getValue(); // the requested flow } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. || **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

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

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

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

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

tùy chọn

mặc định: `en`

|

Mã định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con phân cá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).

Xem [Bản địa hóa và mã locale](android-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 thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn bất kể kết nối internet có chập chờn đến mức nào. Cache được cập nhật thường xuyên nên an toàn khi dùng trong suốt phiên làm việc để tránh các yêu cầu mạng.

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

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

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

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

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

| Đừng hardcode ID sản phẩm! Vì các paywall được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (chẳng hạn như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những 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 của bạn 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 của bạn 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. Các tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) chứa: danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy 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ó: ```kotlin showLineNumbers Adapty.getPaywallProducts(paywall) { result -> when (result) { is AdaptyResult.Success -> { val products = result.value // the requested products } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywallProducts(paywall, result -> { if (result instanceof AdaptyResult.Success) { List products = ((AdaptyResult.Success>) result).getValue(); // the requested products } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi tự triển khai thiết kế paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/). Dưới đây là các thuộc tính được dùng phổ biến nhất — tham khảo tài liệu được liên kết để xem đầy đủ tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng nội địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được nội địa hóa, dùng `product.price.localizedString`. Nội địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể lấy giá dưới dạng số qua `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`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscriptionDetails?.localizedSubscriptionPeriod`. Nội địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo cách lập trình, dùng `product.subscriptionDetails?.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy đơn vị thời gian (DAY, WEEK, MONTH, YEAR hoặc UNKNOWN). Giá trị `numberOfUnits` cho biết số đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `MONTH` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ báo khác cho thấy gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionDetails?.introductoryOfferPhases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:
• `paymentMode`: một enum với các giá trị `FREE_TRIAL`, `PAY_AS_YOU_GO`, `PAY_UPFRONT` và `UNKNOWN`. Dùng thử miễn phí sẽ có kiểu `FREE_TRIAL`.
• `price`: Giá ưu đãi dưới dạng số. Với dùng thử miễn phí, giá trị sẽ là `0`.
• `localizedNumberOfPeriods`: một chuỗi được nội địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.
• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết riêng lẻ về chu kỳ ưu đãi qua thuộc tính này. Cách hoạt động tương tự như phần trước đã mô tả cho các ưu đãi.
• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký được định dạng theo ngôn ngữ của người dùng. | ## Tăng tốc tải paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong các trường hợp bạn có nhiều đối tượng và paywall, và người dùng có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này 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ư đã trình bày chi tiết trong phần [Lấy thông tin Paywall](fetch-paywalls-and-products-android#fetch-paywall-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không hiển thị được. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng targeting cá nhân hóa (bao gồm dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` được mô tả [ở trên](fetch-paywalls-and-products-android#fetch-paywall-information). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", locale = "en") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", "en", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Android SDK phiên bản 2.11.3 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 subtag phân cách bằng dấu trừ (**-**). Subtag đầu tiên là mã ngôn ngữ, subtag thứ hai là mã vùng.

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

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

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

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

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

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

|
--- # File: present-remote-config-paywalls-android --- --- title: "Render paywall designed by remote config in Android SDK" description: "Khám phá cách hiển thị paywall Remote Config trong Adapty Android SDK để cá nhân hóa trải nghiệm người dùng." --- Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng có thể thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn quyết định nội dung hiển thị và giao diện của paywall. Adapty cung cấp phương thức để lấy cấu hình Remote Config, giúp bạn chủ động xây dựng paywall tùy chỉnh của mình. ## Lấy Remote Config của flow và hiển thị nó \{#get-flow-remote-config-and-present-it\} Trong v4, một flow chứa một mục `AdaptyRemoteConfig` cho mỗi locale đã cấu hình trong mảng `remoteConfigs`. Chọn locale phù hợp với tùy chọn của người dùng, sau đó đọc các giá trị bạn cần. ```kotlin showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val flow = result.value val config = flow.remoteConfigs.firstOrNull { it.locale == "en" } ?: flow.remoteConfigs.firstOrNull() val headerText = config?.dataMap?.get("header_text") as? String } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyFlow flow = ((AdaptyResult.Success) result).getValue(); AdaptyRemoteConfig config = null; for (AdaptyRemoteConfig remoteConfig : flow.getRemoteConfigs()) { if ("en".equals(remoteConfig.getLocale())) { config = remoteConfig; break; } } if (config == null && !flow.getRemoteConfigs().isEmpty()) { config = flow.getRemoteConfigs().get(0); } if (config != null && config.getDataMap().get("header_text") instanceof String) { String headerText = (String) config.getDataMap().get("header_text"); } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Đến đây, 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 lại thành một trang có giao diện hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng hiển thị khác nhau trên điện thoại di động, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên nhiều thiết bị. :::warning Hãy nhớ [ghi lại sự kiện xem paywall](present-remote-config-paywalls-android#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin phục vụ cho funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ flow của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](android-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](android-use-fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các flow và paywall. Dữ liệu mua hàng được thu thập tự động, nhưng việc ghi nhận lượt xem cần bạn thực hiện thủ công vì chỉ bạn mới biết khi nào người dùng nhìn thấy một flow. Để ghi nhận sự kiện xem, chỉ cần gọi `.logShowFlow(flow)`, và nó sẽ được phản ánh trong các chỉ số của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowFlow(flow)` nếu bạn đang hiển thị các flow hoặc paywall được render bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder). Adapty tự động theo dõi lượt xem trong những trường hợp đó. ::: ```kotlin showLineNumbers Adapty.logShowFlow(flow) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :-------- | :------- |:-----------------------------------------------------------------------------------------| | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` nhận được qua `Adapty.getFlow`. | Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự implement phần hiển thị trong code của ứng dụng để người dùng có thể thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn toàn quyền quyết định nội dung hiển thị và giao diện của paywall. Chúng tôi cung cấp một phương thức để lấy cấu hình remote, giúp bạn chủ động hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## Lấy Remote Config của paywall và hiển thị nó \{#get-paywall-remote-config-and-present-it\} Để lấy Remote Config của một paywall, truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value val headerText = paywall.remoteConfig?.dataMap?.get("header_text") as? String } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); AdaptyPaywall.RemoteConfig remoteConfig = paywall.getRemoteConfig(); if (remoteConfig != null) { if (remoteConfig.getDataMap().get("header_text") instanceof String) { String headerText = (String) remoteConfig.getDataMap().get("header_text"); } } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và tổng hợp chúng thành một trang trông đẹp mắt. Hãy đảm bảo rằng thiết kế phù hợp với nhiều màn hình điện thoại và hướng hiển thị 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 các thiết bị khác nhau. :::warning Hãy đảm bảo [ghi lại sự kiện xem paywall](present-remote-config-paywalls-android#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy tiếp tục thiết lập luồng mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](android-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](android-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 trường hợp đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi chúng tôi tự động thu thập dữ liệu về các giao dịch mua, việc ghi lại lượt xem paywall cần có sự hỗ trợ từ phí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ị các paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```kotlin showLineNumbers Adapty.logShowPaywall(paywall) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:------------------------------------------------------------------------------------------------------------| | **paywall** | bắt buộc | Đối tượng [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | --- # File: android-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng Android SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng là bước quan trọng để 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ợ thanh toán nếu bạn dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall. Nếu không dùng Paywall Builder, bạn phải sử dụng một phương thức riêng là `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung. Phương thức này là cổng để người dùng tương tác với paywall và thực hiện giao dịch mong 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 mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm thanh toán. :::warning Lưu ý rằng ưu đãi giới thiệu chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh tính đủ điều kiện của người dùng cho ưu đãi giới thiệu 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ỏ sót 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 hàng. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang dùng [Paywall Builder](adapty-paywall-builder)?** Các giao dịch mua hàng được xử lý tự động — bạn có thể bỏ qua bước này. **Cần hướng dẫn từng bước?** Xem [hướng dẫn quickstart](android-implement-paywalls-manually) để biết cách triển khai đầy đủ từ đầu đến cuối. ::: ```kotlin showLineNumbers Adapty.makePurchase(activity, product, null) { result -> when (result) { is AdaptyResult.Success -> { when (val purchaseResult = result.value) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // Grant access to the paid features } } is AdaptyPurchaseResult.UserCanceled -> { // Handle the case where the user canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Handle deferred purchases (e.g., the user will pay offline with cash) } } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } ``` ```java showLineNumbers Adapty.makePurchase(activity, product, null, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchaseResult purchaseResult = ((AdaptyResult.Success) result).getValue(); if (purchaseResult instanceof AdaptyPurchaseResult.Success) { AdaptyProfile profile = ((AdaptyPurchaseResult.Success) purchaseResult).getProfile(); AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); if (premium != null && premium.isActive()) { // Grant access to the paid features } } else if (purchaseResult instanceof AdaptyPurchaseResult.UserCanceled) { // Handle the case where the user canceled the purchase } else if (purchaseResult instanceof AdaptyPurchaseResult.Pending) { // Handle deferred purchases (e.g., the user will pay offline with cash) } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // 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://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/) 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://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) cung cấp thông tin toàn diện về mức độ truy cập, gói đăng ký và các sản phẩm mua một lần của người dùng trong ứng dụng.

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

| :::warning **Lưu ý:** nếu bạn vẫn đang dùng StoreKit của Apple phiên bản thấp hơn v2.0 và Adapty SDK phiên bản thấp hơn v2.9.0, bạn cần cung cấp [Apple App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple ngừng hỗ trợ. ::: ## Thay đổi gói đăng ký khi mua hàng \{#change-subscription-when-making-a-purchase\} Khi người dùng chọn 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. Với Google Play, gói đăng ký không được cập nhật tự động. Bạn cần xử lý việc chuyển đổi trong code của ứng dụng như mô tả bên dưới. Để thay thế gói đăng ký bằng gói khác trên Android, hãy gọi phương thức `.makePurchase()` với tham số bổ sung: ```kotlin showLineNumbers Adapty.makePurchase( activity, product, AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(subscriptionUpdateParams) .build() ) { result -> when (result) { is AdaptyResult.Success -> { when (val purchaseResult = result.value) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // successful cross-grade } is AdaptyPurchaseResult.UserCanceled -> { // user canceled the purchase flow } is AdaptyPurchaseResult.Pending -> { // the purchase has not been finished yet, e.g. user will pay offline by cash } } } is AdaptyResult.Error -> { val error = result.error // 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 [`AdaptySubscriptionUpdateParameters`](https://android.adapty.io/adapty/com.adapty.models/-adapty-subscription-update-parameters/). | ```java showLineNumbers Adapty.makePurchase( activity, product, new AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(subscriptionUpdateParams) .build(), result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchaseResult purchaseResult = ((AdaptyResult.Success) result).getValue(); if (purchaseResult instanceof AdaptyPurchaseResult.Success) { AdaptyProfile profile = ((AdaptyPurchaseResult.Success) purchaseResult).getProfile(); // successful cross-grade } else if (purchaseResult instanceof AdaptyPurchaseResult.UserCanceled) { // user canceled the purchase flow } else if (purchaseResult instanceof AdaptyPurchaseResult.Pending) { // the purchase has not been finished yet, e.g. user will pay offline by cash } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // 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 [`AdaptySubscriptionUpdateParameters`](https://android.adapty.io/adapty/com.adapty.models/-adapty-subscription-update-parameters/). | 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ị của 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 để 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ự chỉ xảy ra khi chu kỳ thanh toán của gói đăng ký hiện tại kết thúc. ### Quản lý gói trả trước \{#manage-prepaid-plans\} 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 đang chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withEnablePendingPrepaidPlans(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withEnablePendingPrepaidPlans(true) .build(); ``` --- # File: android-restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng Android SDK" description: "Tìm hiểu cách khôi phục giao dịch mua trong Adapty để đảm bảo trải nghiệm người dùng liền mạch." --- Khôi phục giao dịch mua là tính năng cho phép người dùng lấy lại quyền truy cập vào nội dung đã mua trước đó — chẳng hạn như các gói đăng ký hoặc in-app purchase — mà không bị tính tiền thêm lần nữa. Tính năng này đặc biệt hữu ích cho những người đã gỡ cài đặt rồi cài lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập lại nội dung đã mua mà không cần thanh toán 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()`: ```kotlin showLineNumbers Adapty.restorePurchases { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // successful access restore } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.restorePurchases(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); if (profile != null) { AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); if (premium != null && premium.isActive()) { // successful access restore } } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Tham số phản hồi: | Tham số | Mô tả | |---------|-----------| | **Profile** |

Đối tượng [`AdaptyProfile`](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/). 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-android --- --- title: "Triển khai Observer mode trong Android SDK" description: "Triển khai Observer mode trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong Android SDK." --- Nếu bạn đã có hạ tầng mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể khám phá [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp phân tích nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics. Nếu điều này đáp ứng nhu cầu của bạn, bạn chỉ cần: 1. Bật tính năng này khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. Làm theo hướng dẫn cài đặt cho [Android](sdk-installation-android#activate-adapty-module-of-adapty-sdk). 2. [Báo cáo giao dịch](report-transactions-observer-mode-android) từ hạ tầng mua hàng hiện có của bạn sang Adapty. ## Thiết lập Observer mode \{#observer-mode-setup\} Bật Observer mode nếu bạn tự xử lý việc mua hàng và trạng thái gói đăng ký, đồng thời sử dụng Adapty để gửi các sự kiện gói đăng ký và analytics. :::important Khi chạy ở Observer mode, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều đó. ::: ```kotlin showLineNumbers class MyApplication : Application() { override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(true) //default false .build() ) } ``` ```java showLineNumbers public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(true) //default false .build() ); } ``` Các tham số: | Tham số | Mô tả | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean kiểm soát [Observer mode](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thêm một số thiết lập trong Observer mode. Đây là những gì bạn cần làm ngoài các bước trên: 1. Hiển thị paywall như bình thường đối với [remote config paywalls](present-remote-config-paywalls-android). Đối với paywall dùng Paywall Builder, hãy làm theo hướng dẫn cài đặt cụ thể cho [Android](android-present-paywall-builder-paywalls-in-observer-mode). 3. [Liên kết paywall](report-transactions-observer-mode-android) với các giao dịch mua hàng. --- # File: report-transactions-observer-mode-android --- --- title: "Báo cáo giao dịch trong Observer Mode trên Android SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trên Android SDK." --- Trong Observer Mode, SDK không thể tự động theo dõi các giao dịch mua hàng được thực hiện qua hệ thống mua hàng hiện tại của bạn. Bạn cần báo cáo các giao dịch từ app store. Đ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 từng giao dịch một cách tường minh để Adapty nhận diện. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận diện được giao dịch, 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 đưa `variationId` vào 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. ```kotlin showLineNumbers val transactionInfo = TransactionInfo.fromPurchase(purchase) Adapty.reportTransaction(transactionInfo, variationId) { result -> if (result is AdaptyResult.Success) { // success } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | --------------- | -------- | ------------------------------------------------------------ | | transactionInfo | bắt buộc | TransactionInfo từ 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 | Định danh chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | ```java showLineNumbers TransactionInfo transactionInfo = TransactionInfo.fromPurchase(purchase); Adapty.reportTransaction(transactionInfo, variationId, result -> { if (result instanceof AdaptyResult.Success) { // success } }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | | --------------- | -------- | ------------------------------------------------------------ | | transactionInfo | bắt buộc | TransactionInfo từ 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 | Định danh chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | Trong Observer Mode, SDK không thể tự động theo dõi các giao dịch mua hàng được thực hiện qua hệ thống mua hàng hiện tại của bạn. Bạn cần báo cáo các giao dịch từ app store 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 `restorePurchases` để báo cáo giao dịch cho Adapty. :::warning **Đừng bỏ qua việc khôi phục giao dịch mua hàng!** Nếu bạn không gọi `restorePurchases`, Adapty sẽ không nhận diện được 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 liên kết giao dịch với paywall đã dẫn đến giao dịch mua hàng bằng phương thức `setVariationId`. Điều này đảm bảo giao dịch mua hàng được quy đúng về paywall kích hoạt để analytics chính xác. Bước này chỉ cần thiết nếu bạn đang sử dụng paywall của Adapty. ```kotlin showLineNumbers Adapty.restorePurchases { result -> if (result is AdaptyResult.Success) { // success } } Adapty.setVariationId(transactionId, variationId) { error -> if (error == null) { // success } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | Định danh 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 | bắt buộc | Định danh chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | ```java showLineNumbers Adapty.restorePurchases(result -> { if (result instanceof AdaptyResult.Success) { // success } }); Adapty.setVariationId(transactionId, variationId, error -> { if (error == null) { // success } }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | Định danh 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 | bắt buộc | Định danh chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | **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](android-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 diện được giao dịch, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: **Liên kết paywall với giao dịch** SDK 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 có ý đị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ừ app store với paywall tương ứng trong code ứng dụng của bạn. Điều này quan trọng để thiết lập đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến sai sót trong analytics. ```kotlin Adapty.setVariationId(transactionId, variationId) { error -> if (error == null) { // success } } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | Định danh 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 | bắt buộc | Định danh chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | ```java Adapty.setVariationId(transactionId, variationId, error -> { if (error == null) { // success } }); ``` | Tham số | Bắt buộc | Mô tả | | ------------------------------------------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | Định danh 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 | bắt buộc | Định danh chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | --- # File: android-present-paywall-builder-paywalls-in-observer-mode --- --- title: "Hiển thị paywall được tạo bằng Paywall Builder trong Observer mode trên Android SDK" description: "Tìm hiểu cách hiển thị paywall trong observer mode bằng Paywall Builder của Adapty." --- Nếu bạn đã tạo một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị nó. :::warning Phần này chỉ dành cho [Observer mode](observer-vs-full-mode). Nếu bạn không làm việc ở Observer mode, hãy tham khảo chủ đề [Android - Hiển thị flows & paywalls](android-present-paywalls) thay thế. :::
Trước khi bắt đầu hiển thị flows (Nhấn để mở rộng) 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo từng framework [cho Android](sdk-installation-android). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình flow hoặc paywall trong các builder](create-paywall) và gán sản phẩm vào chúng. 5. [Tạo placement và gán flow hoặc paywall vào chúng](create-placement) trong Adapty Dashboard. 6. [Lấy flow và cấu hình của chúng](android-get-pb-paywalls) trong code ứng dụng di động của bạn.

1. Triển khai `AdaptyUiObserverModeHandler`. Sự kiện `onPurchaseInitiated` sẽ thông báo cho bạn biết người dùng đã khởi tạo một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình để phản hồi callback này: ```kotlin showLineNumbers val observerModeHandler = AdaptyUiObserverModeHandler { product, flow, flowView, onStartPurchase, onFinishPurchase -> onStartPurchase() yourBillingClient.makePurchase( product, onSuccess = { purchase -> onFinishPurchase() //handle success }, onError = { onFinishPurchase() //handle error }, onCancel = { onFinishPurchase() //handle cancel } ) } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = (product, flow, flowView, onStartPurchase, onFinishPurchase) -> { onStartPurchase.invoke(); yourBillingClient.makePurchase( product, purchase -> { onFinishPurchase.invoke(); //handle success }, error -> { onFinishPurchase.invoke(); //handle error }, () -> { //cancellation onFinishPurchase.invoke(); //handle cancel } ); }; ``` Để xử lý việc khôi phục giao dịch trong Observer mode, hãy override `getRestoreHandler()`. Mặc định nó trả về `null`, tức là sử dụng flow `Adapty.restorePurchases()` có sẵn của Adapty. Để cung cấp cách xử lý khôi phục của riêng bạn: ```kotlin showLineNumbers val observerModeHandler = object : AdaptyUiObserverModeHandler { // onPurchaseInitiated implementation (see above) override fun getRestoreHandler() = AdaptyUiObserverModeHandler.RestoreHandler { onStartRestore, onFinishRestore -> onStartRestore() yourBillingClient.restorePurchases( onSuccess = { restoredPurchases -> onFinishRestore() //handle successful restore }, onError = { onFinishRestore() //handle error } ) } } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = new AdaptyUiObserverModeHandler() { // onPurchaseInitiated implementation (see above) @Override public RestoreHandler getRestoreHandler() { return (onStartRestore, onFinishRestore) -> { onStartRestore.invoke(); yourBillingClient.restorePurchases( restoredPurchases -> { onFinishRestore.invoke(); //handle successful restore }, error -> { onFinishRestore.invoke(); //handle error } ); }; } }; ``` Hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về quá trình mua hàng hoặc khôi phục. Điều này cần thiết để flow hoạt động đúng, chẳng hạn như hiển thị loader: | Callback | Mô tả | | :----------------- |:----------------------------------------------------------------------------------------------| | onStartPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã bắt đầu. | | onFinishPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã hoàn tất. | | onStartRestore() | Tùy chọn. Callback có thể được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã bắt đầu. | | onFinishRestore() | Tùy chọn. Callback có thể được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã hoàn tất. | 2. Để hiển thị flow trực quan lên màn hình thiết bị, bạn cần cấu hình nó trước. Để làm điều này, hãy gọi phương thức `AdaptyUI.getFlowView()` hoặc tạo trực tiếp `AdaptyFlowView`: ```kotlin showLineNumbers val flowView = AdaptyUI.getFlowView( activity, flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, observerModeHandler, ) ``` ```kotlin showLineNumbers val flowView = AdaptyFlowView(activity) // or retrieve it from xml ... with(flowView) { showFlow( flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, observerModeHandler, ) } ``` ```java showLineNumbers AdaptyFlowView flowView = AdaptyUI.getFlowView( activity, flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, observerModeHandler ); ``` ```java showLineNumbers AdaptyFlowView flowView = new AdaptyFlowView(activity); //add to the view hierarchy if needed, or you receive it from xml ... flowView.showFlow(flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, observerModeHandler); ``` ```xml showLineNumbers ``` Sau khi view được tạo thành công, bạn có thể thêm nó vào hệ thống phân cấp view và hiển thị nó. Để thực hiện điều này, hãy sử dụng composable function sau: ```kotlin showLineNumbers AdaptyFlowScreen( flowConfiguration, products, eventListener, insets, customAssets, tagResolver, timerResolver, observerModeHandler, ) ``` Các tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **flowConfiguration** | bắt buộc | Cung cấp đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow. Sử dụng phương thức `AdaptyUI.getFlowConfiguration(flow)` để tải nó. Tham khảo chủ đề [Fetch the view configuration](android-get-pb-paywalls#fetch-the-view-configuration) để biết thêm chi tiết. | | **products** | tùy chọn | Cung cấp một mảng `AdaptyPaywallProduct` để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền vào `null`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | | **eventListener** | tùy chọn | Cung cấp `AdaptyFlowEventListener` để theo dõi các sự kiện của flow. Nên kế thừa `AdaptyFlowDefaultEventListener` để dễ sử dụng hơn. Tham khảo chủ đề [Handle flow & paywall events](android-handling-events) để biết thêm chi tiết. | | **insets** | tùy chọn | Insets là khoảng cách xung quanh flow giúp các phần tử có thể nhấn không bị ẩn sau thanh hệ thống. Mặc định: `Unspecified`, cho phép Adapty tự động điều chỉnh insets. Xem [Change flow insets](android-present-paywalls#change-flow-insets). | | **customAssets** | tùy chọn | Truyền đối tượng `AdaptyCustomAssets` để thay thế hình ảnh và video trong flow hoặc paywall của bạn lúc chạy. Tham khảo [Customize assets](android-get-pb-paywalls#customize-assets) để biết thêm chi tiết. | | **tagResolver** | tùy chọn | Sử dụng `AdaptyUiTagResolver` để phân giải các custom tag trong văn bản của flow. Resolver này nhận tham số là một tag và phân giải nó thành chuỗi tương ứng. Tham khảo chủ đề Custom tags in Paywall Builder để biết thêm chi tiết. | | **observerModeHandler** | bắt buộc khi dùng Observer mode | `AdaptyUiObserverModeHandler` mà bạn đã triển khai ở bước trước. | :::warning Đừng quên [liên kết paywall với giao dịch mua hàng](report-transactions-observer-mode-android). Nếu không, Adapty sẽ không xác định được flow nguồn của giao dịch mua hàng. :::
Trước khi bắt đầu hiển thị paywall (Nhấn để mở rộng) 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android) và [với App Store](initial_ios). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo từng framework [cho Android](sdk-installation-android). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình paywall, gán sản phẩm vào paywall](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 5. [Tạo placement và gán paywall vào placement](create-placement) trong Adapty Dashboard. 6. [Lấy paywall Paywall Builder và cấu hình của chúng](android-get-pb-paywalls) trong code ứng dụng mobile của bạn.

1. Implement the `AdaptyUiObserverModeHandler`. Sự kiện `onPurchaseInitiated` sẽ thông báo cho bạn khi người dùng đã bắt đầu một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình trong callback này: ```kotlin showLineNumbers val observerModeHandler = AdaptyUiObserverModeHandler { product, paywall, paywallView, onStartPurchase, onFinishPurchase -> onStartPurchase() yourBillingClient.makePurchase( product, onSuccess = { purchase -> onFinishPurchase() //handle success }, onError = { onFinishPurchase() //handle error }, onCancel = { onFinishPurchase() //handle cancel } ) } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = (product, paywall, paywallView, onStartPurchase, onFinishPurchase) -> { onStartPurchase.invoke(); yourBillingClient.makePurchase( product, purchase -> { onFinishPurchase.invoke(); //handle success }, error -> { onFinishPurchase.invoke(); //handle error }, () -> { //cancellation onFinishPurchase.invoke(); //handle cancel } ); }; ``` Để xử lý khôi phục trong Observer mode, hãy override `getRestoreHandler()`. Mặc định nó trả về `null`, tức là sử dụng flow `Adapty.restorePurchases()` tích hợp sẵn của Adapty. Để cung cấp cách triển khai khôi phục của riêng bạn: ```kotlin showLineNumbers val observerModeHandler = object : AdaptyUiObserverModeHandler { // onPurchaseInitiated implementation (see above) override fun getRestoreHandler() = AdaptyUiObserverModeHandler.RestoreHandler { onStartRestore, onFinishRestore -> onStartRestore() yourBillingClient.restorePurchases( onSuccess = { restoredPurchases -> onFinishRestore() //handle successful restore }, onError = { onFinishRestore() //handle error } ) } } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = new AdaptyUiObserverModeHandler() { // onPurchaseInitiated implementation (see above) @Override public RestoreHandler getRestoreHandler() { return (onStartRestore, onFinishRestore) -> { onStartRestore.invoke(); yourBillingClient.restorePurchases( restoredPurchases -> { onFinishRestore.invoke(); //handle successful restore }, error -> { onFinishRestore.invoke(); //handle error } ); }; } }; ``` Hãy nhớ gọi các callback sau để thông báo cho AdaptyUI biết về quá trình mua hàng hoặc khôi phục. Điều này cần thiết để paywall hoạt động đúng cách, chẳng hạn như hiển thị loader: | Callback | Mô tả | | :----------------- |:----------------------------------------------------------------------------------------------| | onStartPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua đã bắt đầu. | | onFinishPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua đã hoàn thành. | | onStartRestore() | Tùy chọn. Callback có thể được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã bắt đầu. | | onFinishRestore() | Tùy chọn. Callback có thể được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã hoàn thành. | 2. Để hiển thị paywall trực quan trên màn hình thiết bị, bạn cần cấu hình nó trước. Để làm điều này, gọi phương thức `AdaptyUI.getPaywallView()` hoặc tạo trực tiếp `AdaptyPaywallView`: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler, ) ``` ```kotlin showLineNumbers val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { showPaywall( viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler ); ``` ```java showLineNumbers AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.showPaywall(viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler); ``` ```xml showLineNumbers ``` Sau khi view được tạo thành công, bạn có thể thêm nó vào view hierarchy và hiển thị. Để làm điều này, hãy sử dụng composable function sau: ```kotlin showLineNumbers AdaptyPaywallScreen( viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **Products** | tùy chọn | Truyền vào một mảng `AdaptyPaywallProduct` để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền `null`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | | **ViewConfiguration** | bắt buộc | Truyền vào đối tượng `AdaptyViewConfiguration` chứa thông tin hiển thị của paywall. Dùng phương thức `Adapty.getViewConfiguration(paywall)` để tải nó. Xem thêm tại phần [Tải cấu hình hiển thị của paywall](#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). | | **EventListener** | tùy chọn | Truyền vào `AdaptyUiEventListener` để lắng nghe các sự kiện của paywall. Khuyến nghị kế thừa `AdaptyUiDefaultEventListener` để dễ sử dụng hơn. Xem thêm tại phần [Xử lý sự kiện paywall](android-handling-events). | | **PersonalizedOfferResolver** | tùy chọn | Để chỉ định giá cá nhân hóa ([đọc thêm](https://developer.android.com/google/play/billing/integrate#personalized-price)), hãy implement `AdaptyUiPersonalizedOfferResolver` và truyền vào logic của bạn — ánh xạ `AdaptyPaywallProduct` thành `true` nếu giá của sản phẩm được cá nhân hóa, ngược lại là `false`. | | **TagResolver** | tùy chọn | Dùng `AdaptyUiTagResolver` để xử lý các thẻ tùy chỉnh trong văn bản của paywall. Resolver này nhận một tham số tag và chuyển đổi nó thành chuỗi tương ứng. Xem thêm tại phần Custom tags in Paywall Builder. | | **ObserverModeHandler** | bắt buộc khi dùng Observer mode | `AdaptyUiObserverModeHandler` bạn đã implement ở bước trước. | | **variationId** | bắt buộc | Chuỗi định danh của biến thể. Bạn có thể lấy nó qua thuộc tính `variationId` của đối tượng [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | | **transaction** | bắt buộc |

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

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

Với Android: Chuỗi định danh (`purchase.getOrderId()`) của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) thuộc thư viện billing.

|
Trước khi bắt đầu hiển thị paywall (Nhấp để mở rộng) 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android) và [với App Store](initial_ios). 2. Cài đặt và cấu hình SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo từng framework [cho Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk), và [Unity](sdk-installation-unity#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình paywall, gán sản phẩm cho chúng](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 5. [Tạo placement và gán paywall cho chúng](create-placement) trong Adapty Dashboard. 6. [Tải paywall từ Paywall Builder và cấu hình của chúng](android-get-pb-paywalls) trong code ứng dụng di động của bạn.
1. Triển khai `AdaptyUiObserverModeHandler`. Callback của `AdaptyUiObserverModeHandler` (`onPurchaseInitiated`) thông báo cho bạn biết khi người dùng bắt đầu mua hàng. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình để phản hồi callback này như sau: ```kotlin showLineNumbers val observerModeHandler = AdaptyUiObserverModeHandler { product, paywall, paywallView, onStartPurchase, onFinishPurchase -> onStartPurchase() yourBillingClient.makePurchase( product, onSuccess = { purchase -> onFinishPurchase() //handle success }, onError = { onFinishPurchase() //handle error }, onCancel = { onFinishPurchase() //handle cancel } ) } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = (product, paywall, paywallView, onStartPurchase, onFinishPurchase) -> { onStartPurchase.invoke(); yourBillingClient.makePurchase( product, purchase -> { onFinishPurchase.invoke(); //handle success }, error -> { onFinishPurchase.invoke(); //handle error }, () -> { //cancellation onFinishPurchase.invoke(); //handle cancel } ); }; ``` Ngoài ra, hãy nhớ gọi các callback này đến AdaptyUI. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị loader, v.v.: | Callback trong Kotlin | Callback trong Java | Mô tả | | :-------------------- | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------ | | onStartPurchase() | onStartPurchase.invoke() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình mua đã bắt đầu. | | onFinishPurchase() | onFinishPurchase.invoke() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình mua đã hoàn tất (thành công hoặc thất bại), hoặc đã bị hủy. | 2. Để hiển thị paywall trực quan, bạn cần khởi tạo nó trước. Để làm điều này, hãy gọi phương thức `AdaptyUI.getPaywallView()` hoặc tạo `AdaptyPaywallView` trực tiếp: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), eventListener, personalizedOfferResolver, tagResolver, observerModeHandler, ) //======= OR ======= val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { setEventListener(eventListener) setObserverModeHandler(observerModeHandler) showPaywall( viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), personalizedOfferResolver, tagResolver, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), eventListener, personalizedOfferResolver, tagResolver, observerModeHandler ); //======= OR ======= AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.setEventListener(eventListener); paywallView.setObserverModeHandler(observerModeHandler); paywallView.showPaywall(viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), personalizedOfferResolver); ``` ```xml showLineNumbers ``` Sau khi view được tạo thành công, bạn có thể thêm nó vào view hierarchy và hiển thị nó. Các tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Products** | tùy chọn | Cung cấp một mảng `AdaptyPaywallProduct` để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền `null`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | | **ViewConfiguration** | bắt buộc | Cung cấp đối tượng `AdaptyViewConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `Adapty.getViewConfiguration(paywall)` để tải. Tham khảo chủ đề [Tải cấu hình hiển thị của paywall](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) để biết thêm chi tiết. | | **Insets** | bắt buộc | Xác định đối tượng `AdaptyPaywallInsets` chứa thông tin về vùng bị các thanh hệ thống che phủ, tạo khoảng cách dọc cho nội dung. Nếu cả thanh trạng thái lẫn thanh điều hướng đều không che phủ `AdaptyPaywallView`, hãy truyền `AdaptyPaywallInsets.NONE`. Với chế độ toàn màn hình khi các thanh hệ thống che một phần giao diện, hãy lấy insets như hướng dẫn bên dưới bảng. | | **EventListener** | tùy chọn | Cung cấp `AdaptyUiEventListener` để lắng nghe các sự kiện của paywall. Nên kế thừa `AdaptyUiDefaultEventListener` để dễ sử dụng. Tham khảo chủ đề [Xử lý sự kiện paywall](android-handling-events) để biết thêm chi tiết. | | **PersonalizedOfferResolver** | tùy chọn | Để chỉ định giá cá nhân hóa ([đọc thêm](https://developer.android.com/google/play/billing/integrate#personalized-price)), hãy triển khai `AdaptyUiPersonalizedOfferResolver` và truyền logic của bạn để ánh xạ `AdaptyPaywallProduct` thành `true` nếu giá của sản phẩm được cá nhân hóa, ngược lại là `false`. | | **TagResolver** | tùy chọn | Sử dụng `AdaptyUiTagResolver` để xử lý các thẻ tùy chỉnh trong văn bản paywall. Resolver này nhận tham số tag và chuyển đổi nó thành chuỗi tương ứng. Tham khảo chủ đề Custom tags in Paywall Builder để biết thêm chi tiết. | | **ObserverModeHandler** | bắt buộc với Observer mode | `AdaptyUiObserverModeHandler` mà bạn đã triển khai ở bước trước. | | **variationId** | bắt buộc | Chuỗi định danh của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/). | | **transaction** | bắt buộc |

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

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

Với Android: Chuỗi định danh (`purchase.getOrderId()`) của giao dịch mua, trong đó purchase là một thực thể của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.

| Đối với chế độ toàn màn hình khi các thanh hệ thống che một phần giao diện của bạn, hãy lấy insets theo cách sau: ```kotlin showLineNumbers import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat //create extension function fun View.onReceiveSystemBarsInsets(action: (insets: Insets) -> Unit) { ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) ViewCompat.setOnApplyWindowInsetsListener(this, null) action(systemBarInsets) insets } } //and then use it with the view paywallView.onReceiveSystemBarsInsets { insets -> val paywallInsets = AdaptyPaywallInsets.of(insets.top, insets.bottom) paywallView.setEventListener(eventListener) paywallView.setObserverModeHandler(observerModeHandler) paywallView.showPaywall(viewConfig, products, paywallInsets, personalizedOfferResolver, tagResolver) } ``` ```java showLineNumbers import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; ... ViewCompat.setOnApplyWindowInsetsListener(paywallView, (view, insets) -> { Insets systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); ViewCompat.setOnApplyWindowInsetsListener(paywallView, null); AdaptyPaywallInsets paywallInsets = AdaptyPaywallInsets.of(systemBarInsets.top, systemBarInsets.bottom); paywallView.setEventListener(eventListener); paywallView.setObserverModeHandler(observerModeHandler); paywallView.showPaywall(viewConfiguration, products, paywallInsets, personalizedOfferResolver, tagResolver); return insets; }); ``` Kết quả trả về: | Đối tượng | Mô tả | | :------------------ | :------------------------------------------------- | | `AdaptyPaywallView` | object đại diện cho màn hình paywall được yêu cầu. | :::warning Đừng quên [liên kết paywall với giao dịch mua hàng](report-transactions-observer-mode-android). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua hàng. :::
--- # File: android-troubleshoot-purchases --- --- title: "Xử lý sự cố mua hàng trong Android SDK" description: "Xử lý sự cố mua hàng trong Android 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 Android 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 thành 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**: Thường là do thiết lập Google Play Store chưa đầy đủ 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 bị 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 UI 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 trong chế độ observer \{#adaptyelrorcantmakepayments-in-observer-mode\} **Vấn đề**: Bạn nhận được `AdaptyError.cantMakePayments` khi sử dụng `makePurchase` trong chế độ observer. **Nguyên nhân**: Trong chế độ observer, bạn nên tự xử lý các giao dịch mua ở phía mình, không dùng phương thức `makePurchase` của Adapty. **Giải pháp**: Nếu bạn dùng `makePurchase` để thực hiện mua hàng, hãy tắt chế độ observer. Bạn cần chọn một trong hai: dùng `makePurchase` hoặc tự xử lý mua hàng ở phía mình trong chế độ observer. Xem [Triển khai chế độ Observer](implement-observer-mode-android) để 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 của 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**: Thường liên quan đến các vấn đề khi kiểm thử trong môi trường sandbox. **Giải pháp**: Tạo 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ề purchase completion handler trong môi trường sandbox. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn gặp phải các vấn đề liên quan đến mua hàng khác chưa được đề cập ở trên. **Giải pháp**: Nếu cần, hãy migrate SDK lên phiên bản mới nhất theo [hướng dẫn migration](android-sdk-migration-guides). Nhiều vấn đề đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: android-identifying-users --- --- title: "Xác định người dùng trong Android SDK" description: "Xác định người dùng trong Adapty để cải thiện trải nghiệm gói đăng ký được cá nhân hóa (Android)." --- 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), ID này sẽ được gửi đến tất cả các tích hợp. ### Đặt customer user ID khi khởi tạo \{#setting-customer-user-id-on-configuration\} Nếu bạn có user ID trong quá trình khởi tạo, chỉ cần truyền nó làm tham số `customerUserId` vào phương thức `.activate()`: ```kotlin showLineNumbers Adapty.activate(applicationContext, "PUBLIC_SDK_KEY", customerUserId = "YOUR_USER_ID") ``` :::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 khởi tạo \{#setting-customer-user-id-after-configuration\} Nếu bạn chưa có user ID khi khởi tạo 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. ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> if (error == null) { // successful identify } } ``` ```java showLineNumbers Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` Các 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 trường hợp này, Adapty SDK sẽ tự động chuyển sang làm việc với người dùng mới. Nếu bạn đã truyền dữ liệu cho người dùng ẩn danh, chẳng hạn như các 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, cần lưu ý rằng bạn nên yêu cầu 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()`: ```kotlin showLineNumbers Adapty.logout { error -> if (error == null) { // successful logout } } ``` ```java showLineNumbers Adapty.logout(error -> { if (error == null) { // successful logout } }); ``` Sau đó, bạn có thể đăng nhập người dùng bằng phương thức `.identify()`. ### Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} 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: android-setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong Android SDK" description: "Tìm hiểu cách thiết lập thuộc tính người dùng trong Adapty để phân khúc đối tượng hiệu quả hơn." --- Bạn có thể thiết lập các thuộc tính tùy chọn như email, số điện thoại, v.v. cho người dùng trong ứng dụng của mình. Sau đó, bạn có thể sử dụng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM. ### Thiết lập thuộc tính người dùng \{#setting-user-attributes\} Để thiết lập thuộc tính người dùng, hãy gọi phương thức `.updateProfile()`: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() .withEmail("email@email.com") .withPhoneNumber("+18888888888") .withFirstName("John") .withLastName("Appleseed") .withGender(AdaptyProfile.Gender.OTHER) .withBirthday(AdaptyProfile.Date(1970, 1, 3)) Adapty.updateProfile(builder.build()) { error -> if (error != null) { // handle the error } } ``` ```java showLineNumbers AdaptyProfileParameters.Builder builder = new AdaptyProfileParameters.Builder() .withEmail("email@email.com") .withPhoneNumber("+18888888888") .withFirstName("John") .withLastName("Appleseed") .withGender(AdaptyProfile.Gender.OTHER) .withBirthday(new AdaptyProfile.Date(1970, 1, 3)); Adapty.updateProfile(builder.build(), error -> { if (error != null) { // handle the error } }); ``` Lưu ý rằng các thuộc tính bạn đã thiết lập trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các 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ị được phép: `female`, `male`, `other` | | birthday | Date | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể 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ụ, đối với ứng dụng thể dục, có thể là số buổi tập mỗi tuần; đối với ứng dụng học ngôn ngữ, có thể 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 sử dụng trong analytics để tìm ra các chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```kotlin showLineNumbers builder.withCustomAttribute("key1", "value1") ``` ```java showLineNumbers builder.withCustomAttribute("key1", "value1"); ``` Để xóa key hiện có, hãy dùng phương thức `.withRemoved(customAttributeForKey:)`: ```kotlin showLineNumbers builder.withRemovedCustomAttribute("key2") ``` ```java showLineNumbers builder.withRemovedCustomAttribute("key2"); ``` Đô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 sử dụng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không còn cập nhật, vì 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, nên 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: android-listen-subscription-changes --- --- title: "Kiểm tra trạng thái gói đăng ký trong Android 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 việc giữ chân khách hàng trong ứng dụng Android của bạn." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải nhập thủ công các 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 gói đăng ký, hãy thiết lập [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). ## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là các thuộc tính của đối tượng [AdaptyProfile](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/). 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 [xác định người dùng](android-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ể sử dụng đối tượng profile mà không cần phải gọi lại liên tục. Để nhận thông báo khi profile được cập nhật, hãy lắng nghe các thay đổi của profile như mô tả trong phần [Lắng nghe cập nhật profile, bao gồm mức độ truy cập](android-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, sử dụng phương thức `.getProfile()`: ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Tham số phản hồi: | Tham số | Mô tả | | --------- | ------------------------------------------------------------ | | Profile |

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

Phương thức `.getProfile` cung cấp kết quả mới nhất vì nó luôn cố gắng truy vấn API. 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ề. Cần lưu ý rằng Adapty SDK cập nhật cache `AdaptyProfile` định kỳ để đảm bảo thông tin luôn được cập nhật nhất có thể.

| Phương thức `.getProfile()` cung cấp cho bạn hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Ví dụ, nếu bạn có ứng dụng báo và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết các trường hợp, bạn chỉ cần một mức độ truy cập duy nhất — khi đó, bạn có thể dùng mức độ truy cập mặc định là "premium". Dưới đây là ví dụ kiểm tra mức độ truy cập "premium" mặc định: ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value if (profile.accessLevels["premium"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("premium"); if (premium != null && premium.isActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận tin nhắn từ Adapty, bạn cần thực hiện một số cấu hình bổ sung: ```kotlin showLineNumbers Adapty.setOnProfileUpdatedListener { profile -> // handle any changes to subscription state } ``` ```java showLineNumbers t Adapty.setOnProfileUpdatedListener(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 qua. ### Cache trạng thái gói đăng ký \{#subscription-status-cache\} Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi server không khả dụng, dữ liệu trong cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ người dùng. Tuy nhiên, cần lưu ý rằng bạn 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 một lần để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ người dùng. Nếu có bất kỳ thay đổi nào — chẳng hạn như giao dịch mới hoặc các cập nhật khác — chúng sẽ được đồng bộ vào dữ liệu cache để giữ cho cache luôn đồng bộ với server. --- # File: kids-mode-android --- --- title: "Chế Độ Trẻ Em trong Android SDK" description: "Dễ dàng bật Chế Độ Trẻ Em để tuân thủ chính sách của Google. Không thu thập GAID hay dữ liệu quảng cáo trong Android SDK." --- Nếu ứng dụng Android của bạn dành cho trẻ em, bạn phải tuân theo chính sách của [Google](https://support.google.com/googleplay/android-developer/answer/9893335). Nếu bạn đang dùng Adapty SDK, chỉ cần vài bước đơn giản là có thể cấu hình để đá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. ## Cần làm gì? \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách thận trọng. User ID có định dạng `` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc dùng email. Đối với Chế Độ Trẻ Em, cách tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã được hash hoặc UUID do thiết bị tạo ra) để đả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. Để làm điều này, hãy 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ủ các chính sách, bạn cần tắt việc thu thập Android Advertising ID (AAID/GAID) và địa chỉ IP khi khởi tạo Adapty SDK: **Kotlin:** ```kotlin showLineNumbers override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") // highlight-start .withAdIdCollectionDisabled(true) // set to `true` .withIpAddressCollectionDisabled(true) // set to `true` // highlight-end .build() ) } ``` **Java:** ```java showLineNumbers @Override public void onCreate() { super.onCreate(); Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") // highlight-start .withAdIdCollectionDisabled(true) // set to `true` .withIpAddressCollectionDisabled(true) // set to `true` // highlight-end .build() ); } ``` ### Cập nhật trong Android manifest \{#updates-in-your-android-manifest\} :::note Nếu ứng dụng của bạn chỉ nhắm đến trẻ em và biên dịch với Android 13 (API 33) trở lên, Google Play yêu cầu bạn không được yêu cầu quyền `AD_ID`. Một SDK khác trong ứng dụng của bạn (analytics, attribution, hoặc quảng cáo) có thể thêm quyền này thông qua manifest merging. Việc đặt `withAdIdCollectionDisabled(true)` sẽ ngăn Adapty thu thập ID, nhưng không xóa được quyền do SDK khác khai báo. ::: Để xóa quyền đó, hãy thêm đoạn sau vào bên trong phần tử `` của `app/src/main/AndroidManifest.xml`. Phần tử `` phải khai báo `xmlns:tools="http://schemas.android.com/tools"`. ```xml showLineNumbers title="AndroidManifest.xml" ``` --- # File: android-get-onboardings --- --- title: "Lấy onboarding trong Android SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho Android." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flow](android-get-pb-paywalls) như một giải pháp thay thế mạnh mẽ hơn cho onboarding. Khác với onboarding chạy trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện Android nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](android-get-pb-paywalls) và [Hiển thị flow & paywall](android-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Android của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement và cấu hình view của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Android SDK](sdk-installation-android) 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 trình xây dựng no-code của chúng tôi, nó được lưu trữ dưới dạng một container chứa cấu hình mà ứng dụng của bạn cần lấy về và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung hiển thị, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập từ form). Container cũng tự động theo dõi các sự kiện analytics, nên bạn không cần tự triển khai tính năng theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để hình ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy một onboarding, sử dụng phương thức `getOnboarding`: ```kotlin showLineNumbers Adapty.getOnboarding("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val onboarding = result.value // the requested onboarding } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

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

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

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

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

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

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

Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng 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 sử 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 hạn chế.

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

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

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

Đố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, hãy dùng `TimeInterval.INFINITE`.

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

tùy chọn

mặc định: `en`

|

Mã định danh của bản địa hóa onboarding. Tham số này phải là mã ngôn ngữ gồm một hoặc hai thẻ con, phân tách bằng dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.

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

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

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

Theo mặc định, SDK sẽ cố tải dữ liệu từ server 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 thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối có bấp bênh đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên để tránh các yêu cầu mạng.

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

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

| --- # File: android-present-onboardings --- --- title: "Hiển thị onboarding trong Android SDK" description: "Tìm hiểu cách hiển thị onboarding trên Android để tăng hiệu quả tương tác với người dùng." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flows](android-get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboardings. Khác với onboardings chạy trong WebView, flows render trực tiếp trên thiết bị — giúp hoạt ảnh mượt mà hơn, giao diện Android nhất quán hơn, tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](android-get-pb-paywalls) và [Hiển thị flows & paywalls](android-present-paywalls) để bắt đầu. ::: Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Android SDK](sdk-installation-android) 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). Nếu bạn đã tùy chỉnh onboarding bằng Onboarding Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Onboarding như vậy đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị nó. Để hiển thị onboarding trực quan trên màn hình thiết bị, bạn phải cấu hình nó trước. Để làm điều này, hãy gọi phương thức `AdaptyUI.getOnboardingView()` hoặc tạo `OnboardingView` trực tiếp: ```kotlin val onboardingView = AdaptyUI.getOnboardingView( activity = this, viewConfig = onboardingConfig, eventListener = eventListener ) ``` ```kotlin val onboardingView = AdaptyOnboardingView(activity) onboardingView.show( viewConfig = onboardingConfig, delegate = eventListener ) ``` ```java AdaptyOnboardingView onboardingView = AdaptyUI.getOnboardingView( activity, onboardingConfig, eventListener ); ``` ```java AdaptyOnboardingView onboardingView = new AdaptyOnboardingView(activity); onboardingView.show(onboardingConfig, eventListener); ``` ```xml ``` Sau khi view được tạo thành công, bạn có thể thêm nó vào view hierarchy và hiển thị trên màn hình thiết bị. Các tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :-------- | :------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **viewConfig** | bắt buộc | Cấu hình onboarding lấy từ `AdaptyUI.getOnboardingConfiguration()` | | **eventListener** | bắt buộc | Một implementation của `AdaptyOnboardingEventListener` để xử lý các sự kiện onboarding. Xem [Xử lý sự kiện onboarding](android-handle-onboarding-events) để biết thêm chi tiết. | ## Thay đổi màu chỉ báo tải \{#change-loading-indicator-color\} Bạn có thể ghi đè màu mặc định của chỉ báo tải theo cách sau: ```xml ``` ## Thêm hiệu ứng chuyển tiếp mượt mà giữa splash screen và onboarding \{#add-smooth-transitions-between-the-splash-screen-and-onboarding\} Mặc định, giữa splash screen và onboarding, bạn sẽ thấy màn hình loading cho đến khi onboarding tải xong. Tuy nhiên, nếu muốn quá trình chuyển tiếp mượt mà hơn, bạn có thể tùy chỉnh bằng cách kéo dài splash screen hoặc hiển thị nội dung khác. Để làm điều này, hãy tạo file `adapty_onboarding_placeholder_view.xml` trong thư mục `res/layout` và định nghĩa một placeholder (nội dung sẽ hiển thị trong khi onboarding đang tải) ở đó. Nếu bạn định nghĩa một placeholder, onboarding sẽ được tải trong nền và tự động hiển thị ngay khi sẵn sàng. ## Vô hiệu hóa safe area paddings \{#disable-safe-area-paddings\} Theo mặc định, màn hình onboarding sẽ tự động áp dụng safe area paddings để tránh các thành phần giao diện hệ thống như status bar và navigation bar. Tuy nhiên, nếu bạn muốn vô hiệu hóa 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 đặt tham số `safeAreaPaddings` thành `false`. ```kotlin val onboardingView = AdaptyUI.getOnboardingView( activity = this, viewConfig = onboardingConfig, eventListener = eventListener, safeAreaPaddings = false ) ``` ```kotlin val onboardingView = AdaptyOnboardingView(activity) onboardingView.show( viewConfig = onboardingConfig, delegate = eventListener, safeAreaPaddings = false ) ``` ```java AdaptyOnboardingView onboardingView = AdaptyUI.getOnboardingView( activity, onboardingConfig, eventListener, false ); ``` ```java AdaptyOnboardingView onboardingView = new AdaptyOnboardingView(activity); onboardingView.show(onboardingConfig, eventListener, false); ``` Ngoài ra, bạn có thể kiểm soát hành vi này ở mức toàn cục bằng cách thêm một boolean resource vào ứng dụng: ```xml false ``` Khi `safeAreaPaddings` được đặt thành `false`, onboarding sẽ mở rộng toàn màn hình mà không có bất kỳ điều chỉnh padding tự động nào, giúp bạn kiểm soát hoàn toàn bố cục và cho phép nội dung onboarding sử dụng toàn bộ không gian màn hình. ## Tùy chỉnh cách mở liên kết trong onboardings \{#customize-how-links-open-in-onboardings\} :::important Tính năng tùy chỉnh cách mở liên kết trong onboardings được hỗ trợ từ Adapty SDK v3.15.1 trở lên. ::: Theo mặc định, các liên kết trong onboardings sẽ mở trong trình duyệt nội bộ của ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị trang web ngay trong ứng dụng, giúp họ xem nội dung mà không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết bằng 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`: ```kotlin val onboardingConfig = AdaptyUI.getOnboardingConfiguration( onboarding = onboarding, externalUrlsPresentation = AdaptyWebPresentation.ExternalBrowser // default – InAppBrowser ) ``` ```java AdaptyOnboardingConfiguration onboardingConfig = AdaptyUI.getOnboardingConfiguration( onboarding, AdaptyWebPresentation.ExternalBrowser // default – InAppBrowser ); ``` --- # File: android-handle-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong Android SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong Android bằng Adapty." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flows](android-get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboarding. Khác với onboarding chạy bên trong WebView, flows được render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện Android nhất quán, tốc độ tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](android-get-pb-paywalls) và [Hiển thị flows & paywalls](android-present-paywalls) để bắt đầu. ::: Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Android SDK](sdk-installation-android) 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể xử lý. Hãy xem cách xử lý các sự kiện đó bên dưới. Để kiểm soát hoặc theo dõi các tiến trình diễn ra trên màn hình onboarding trong ứng dụng Android của bạn, hãy implement interface `AdaptyOnboardingEventListener`. ## Hành động tùy chỉnh \{#custom-actions\} Trong builder, bạn có thể thêm hành động **custom** cho một nút và gán cho nó một ID. Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. Ví dụ, nếu người dùng nhấn vào một nút tuỳ chỉnh như **Login** hoặc **Allow notifications**, phương thức delegate `onCustomAction` sẽ được kích hoạt với action ID từ builder. Bạn có thể tự tạo ID theo ý muốn, ví dụ như "allowNotifications". ```kotlin showLineNumbers class YourActivity : AppCompatActivity() { private val eventListener = object : AdaptyOnboardingEventListener { override fun onCustomAction(action: AdaptyOnboardingCustomAction, context: Context) { when (action.actionId) { "allowNotifications" -> { // Request notification permissions } } } override fun onError(error: AdaptyOnboardingError, context: Context) { // Handle errors } // ... other required delegate methods } } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
## Đóng onboarding \{#closing-onboarding\} Onboarding được coi là đã đóng khi người dùng nhấn vào một nút có hành động **Close** được gán. Bạn cần xử lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ: :::important Bạn cần xử lý những gì xảy ra khi người dùng đóng onboarding. Chẳng hạn, bạn cần dừng hiển thị chính onboarding đó. ::: Ví dụ: ```kotlin override fun onCloseAction(action: AdaptyOnboardingCloseAction, context: Context) { // Dismiss the onboarding screen (context as? Activity)?.onBackPressed() } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
## Mở một paywall \{#opening-a-paywall\} :::tip Xử lý sự kiện này để mở paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi nó đóng lại, có một cách đơn giản hơn – xử lý [`AdaptyOnboardingCloseAction`](#closing-onboarding) và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall. Như vậy, sau sự kiện `AdaptyOnboardingOpenPaywallAction`, bạn có thể dùng placement ID để lấy và mở paywall ngay lập tức: ```kotlin override fun onOpenPaywallAction(action: AdaptyOnboardingOpenPaywallAction, context: Context) { // Get the paywall using the placement ID from the action Adapty.getPaywall(placementId = action.actionId) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // Get the paywall configuration AdaptyUI.getViewConfiguration(paywall) { result -> when(result) { is AdaptyResult.Success -> { val paywallConfig = result.value // Create and present the paywall val paywallView = AdaptyUI.getPaywallView( activity = this, viewConfig = paywallConfig, products, eventListener = paywallEventListener ) // Add the paywall view to your layout binding.container.addView(paywallView) } is AdaptyResult.Error -> { val error = result.error // handle the error } } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
## Hoàn thành tải onboarding \{#finishing-loading-onboarding\} Khi onboarding hoàn tất quá trình tải, phương thức này sẽ được gọi: ```kotlin override fun onFinishLoading(action: AdaptyOnboardingLoadedAction, context: Context) { // Handle loading completion } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
## Sự kiện điều hướng \{#navigation-events\} Phương thức `onAnalyticsEvent` được gọi khi các sự kiện analytics khác nhau xảy ra trong quá trình onboarding. Đối tượng `event` có thể là một trong các kiểu sau: |Kiểu | Mô tả | |------------|-------------| | `OnboardingStarted` | Khi onboarding đã được tải | | `ScreenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `ScreenCompleted` | Khi một màn hình được hoàn thành. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn thành) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `SecondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `UserEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `OnboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy gán ID `final` cho màn hình cuối cùng. | | `Unknown` | Dành cho bất kỳ kiểu sự kiện nào không được nhận dạng. 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 đều chứa thông tin `meta` bao gồm: | 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 | | `totalScreens` | Tổng số màn hình trong flow | Dưới đây là ví dụ về cách sử dụng các sự kiện analytics để theo dõi: ```kotlin override fun onAnalyticsEvent(event: AdaptyOnboardingAnalyticsEvent, context: Context) { when (event) { is AdaptyOnboardingAnalyticsEvent.OnboardingStarted -> { // Track onboarding start trackEvent("onboarding_started", event.meta) } is AdaptyOnboardingAnalyticsEvent.ScreenPresented -> { // Track screen presentation trackEvent("screen_presented", event.meta) } is AdaptyOnboardingAnalyticsEvent.ScreenCompleted -> { // Track screen completion with user response trackEvent("screen_completed", event.meta, event.elementId, event.reply) } is AdaptyOnboardingAnalyticsEvent.OnboardingCompleted -> { // Track successful onboarding completion trackEvent("onboarding_completed", event.meta) } is AdaptyOnboardingAnalyticsEvent.Unknown -> { // Handle unknown events trackEvent(event.name, event.meta) } // Handle other cases as needed } } ```
Ví dụ về sự kiện (Nhấp để mở rộng) ```javascript // OnboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // ScreenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // ScreenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // SecondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // UserEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // OnboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: android-onboarding-input --- --- title: "Xử lý dữ liệu từ onboardings trong Android SDK" description: "Lưu và sử dụng dữ liệu từ onboardings trong ứng dụng Android của bạn với Adapty SDK." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flows](android-get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboardings. Khác với onboardings chạy trong WebView, flows render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện Android nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](android-get-pb-paywalls) và [Hiển thị flows & paywalls](android-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời một câu hỏi quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Ví dụ: ```kotlin override fun onStateUpdatedAction(action: AdaptyOnboardingStateUpdatedAction, context: Context) { // Store user preferences or responses when (val params = action.params) { is AdaptyOnboardingStateUpdatedParams.Select -> { // Handle single selection } is AdaptyOnboardingStateUpdatedParams.MultiSelect -> { // Handle multiple selections } is AdaptyOnboardingStateUpdatedParams.Input -> { // Handle text input } is AdaptyOnboardingStateUpdatedParams.DatePicker -> { // Handle date selection } } } ``` Xem định dạng action [tại đây](https://android.adapty.io/adapty-ui/com.adapty.ui.onboardings.actions/-adapty-onboarding-state-updated-action/).
Ví dụ về dữ liệu đã lưu (định dạng có thể khác trong quá trình 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 } } } ```
## Trường hợp sử dụng \{#use-cases\} ### Enrich user profiles with data \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi lại cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](android-setting-user-attributes) với dữ liệu đầu vào đó khi xử lý action. Ví dụ: bạn yêu cầu người dùng nhập tên của họ vào trường văn bản có ID là `name`, và bạn muốn đặt giá trị của trường này làm tên 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 của bạn, nó có thể trông như thế này: ```kotlin showLineNumbers override fun onStateUpdatedAction(action: AdaptyOnboardingStateUpdatedAction, context: Context) { // Store user preferences or responses when (val params = action.params) { is AdaptyOnboardingStateUpdatedParams.Input -> { // Handle text input val builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field when (action.elementId) { "name" -> { when (val inputParams = params.params) { is AdaptyOnboardingInputParams.Text -> { builder.withFirstName(inputParams.value) } } } "email" -> { when (val inputParams = params.params) { is AdaptyOnboardingInputParams.Email -> { builder.withEmail(inputParams.value) } } } } Adapty.updateProfile(builder.build()) { error -> if (error != null) { // handle the error } } } } } ``` ### Tuỳ chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Khi dùng quiz trong onboarding, bạn cũng có thể tuỳ 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 thể thao của họ và hiển thị các CTA và sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) vào onboarding builder và gán ID có ý nghĩa cho các lựa chọn. 2. Xử lý các câu trả lời quiz dựa trên ID của chúng và [đặt thuộc tính tuỳ chỉnh](android-setting-user-attributes) cho người dùng. ```kotlin showLineNumbers override fun onStateUpdatedAction(action: AdaptyOnboardingStateUpdatedAction, context: Context) { // Handle quiz responses and set custom attributes when (val params = action.params) { is AdaptyOnboardingStateUpdatedParams.Select -> { // Handle quiz selection val builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes when (action.elementId) { "experience" -> { // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.withCustomAttribute("experience", params.params.value) } } Adapty.updateProfile(builder.build()) { error -> if (error != null) { // handle the error } } } } } ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho từng phân khúc bạn đã tạo. 5. [Hiển thị paywall](android-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 hành động của nút này](android-handle-onboarding-events#opening-a-paywall). --- # File: android-sdk-call-order --- --- title: "Thứ tự gọi trong Android SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution, và lỗi ADAPTY_NOT_INITIALIZED gián đoạn 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 hoàn tất, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` đều thất bại với lỗi [`ADAPTY_NOT_INITIALIZED`](android-sdk-error-handling). 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()` vào thời điểm đó. Không gọi các phương thức liên quan đến hành động người dùng cho đến khi callback hoàn thành của `identify` được kích hoạt. Các lệnh gọi chạy đua với nó sẽ trả về lỗi trong callback, hoặc sẽ tác động lên hồ sơ người dùng ẩn danh được tạo khi kích hoạt. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu lượt cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo quy tắc tương tự. 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ẽ được gán vào một hồ sơ người dùng ẩn danh tồn tại trong thời gian ngắn và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Để biết chi tiết về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Con đường thực hiện phụ thuộc vào hai yếu tố: khi nào bạn biết customer user ID, và liệu 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ỉ cần thiết nếu bạn tích hợp SDK MMP hoặc analytics (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ cần thiết nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn đã có customer user ID khi khởi chạy ứng dụng, hãy truyền nó vào `AdaptyConfig.Builder` trước khi gọi `activate()` (bước 2a). Con đường này không bao giờ tạo hồ sơ người dùng ẩ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(context, AdaptyConfig.Builder("KEY").withCustomerUserId(...).build())` | Khởi chạy ứng dụng, sau bước 1, nếu bạn đã có customer user ID | Khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty.activate(context, AdaptyConfig.Builder("KEY").build())` không có `customerUserId` | 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 bao giờ thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. | | 3 | `Adapty.setIntegrationIdentifier("appsflyer_id", uid)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi hành động người dùng nào | Cần thiết để MMP ID được gán vào đúng hồ sơ người dùng. | | 4 | `Adapty.identify("YOUR_USER_ID") { error -> ... }` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên con đường 2b có xác thực | Sử dụng completion callback. Các lệnh gọi đồng thời trong quá trình `identify` có thể tác động lên hồ sơ người dùng ẩn danh. | | 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này sẽ dẫn đến mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về cho sai đối tượng. ::: ## Cài đặt Web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua trên 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ơ người dùng ẩn danh mới. Hồ sơ người dùng này không được liên kết với hồ sơ người dùng 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 nó vào `AdaptyConfig.Builder` trực tiếp. 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 đó là `restorePurchases`. Để biết metadata cần gửi kèm với mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: android-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong Android SDK" description: "Tải paywall Adapty đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho Android." --- Một lần tải paywall đáng tin cậy trên Android cần đảm bảo ba điều: hiển thị nhanh, trả về paywall đúng với đối tượng mục tiêu, và xử lý dự phòng linh hoạt khi mạng chậm. Các quy tắc dưới đây đề cập đến thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định rằng `Adapty.activate()` và `Adapty.identify()` đã hoàn thành. Xem [Thứ tự gọi trong Android SDK](android-sdk-call-order). ::: ## Quy tắc và lưu ý \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement mà bạn sắp hiển thị. | Tải trước tất cả các placement cùng lúc 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 suốt thời gian đó. | | Gọi `getPaywall` sau khi attribution đã có cơ hội xử lý xong — ví dụ, 1–2 giây sau `activate` hoặc sau khi `setOnProfileUpdatedListener` kích hoạt. | Gọi `getPaywall` trong `Application.onCreate()`. | Attribution chưa được xử lý xong. Paywall sẽ được phân giải theo đối tượng mặc định và bỏ qua phân khúc cũng như ASA personalization mà không có cảnh báo. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` vô thời hạn. | Nếu không có timeout, người dùng ở 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 thoát ứng dụng. | Xem [Tải paywalls và sản phẩm](fetch-paywalls-and-products-android) để tham khảo tham số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn đúng placement. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối kém thường xuyên (khu vực nông thôn, phương tiện giao thông công cộng, vùng bị ảnh hưởng bởi routing): - Đặt `fetchPolicy` thành `AdaptyPlacementFetchPolicy.ReturnCacheDataElseLoad` cho mọi lần tả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 để việc hiển thị paywall phụ thuộc vào `getProfile`. Gọi `getPaywall` độc lập để hồ sơ người dùng chậm không làm trì hoãn giao diện. --- # File: android-test --- --- title: "Test & release in Android SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Android của bạn với Adapty." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng Android của mình, bạn sẽ muốn kiểm tra xem mọi thứ đã được thiết lập đúng cách và các giao dịch mua hoạt động như mong đợi. Quá trình này bao gồm 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 Google Play. ## Kiểm thử ứng dụng của bạn \{#test-your-app\} Để kiểm thử toàn diện các in-app purchase, bao gồm kiểm thử sandbox và xác thực closed track, hãy xem [hướng dẫn kiểm thử](testing-on-android) của chúng tôi. ## Chuẩn bị để phát hành \{#prepare-for-release\} Trước khi gửi ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối cửa hàng và thông báo máy chủ đã được cấu hình - Các giao dịch mua hoàn tất và được báo cáo cho Adapty - Quyền 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à đánh giá đã được đáp ứng --- # File: android-sdk-error-handling --- --- title: "Xử lý lỗi trong Android SDK" description: "Xử lý lỗi Android SDK hiệu quả với hướng dẫn khắc phục sự cố của Adapty." --- Mọi lỗi được trả về bởi SDK đều có dạng `AdaptyError`. :::tip **Bật verbose logs trước khi debug.** Hầu hết các `AdaptyError` đều bọc một lỗi bên dưới từ Play Billing, mạng, hoặc backend. Khi bật verbose logs (`Adapty.logLevel = AdaptyLogLevel.VERBOSE` — xem [Logging](sdk-installation-android#logging)), lỗi được bọc đó sẽ được in ra console, thường cho biết nguyên nhân thực sự. ::: :::important Nếu các giải pháp này không khắc phục được vấn đề của bạn, hãy xem [Các vấn đề khác](#other-issues) để biết các bước cần thực hiện trước khi liên hệ hỗ trợ, giúp chúng tôi hỗ trợ bạn hiệu quả hơn. ::: | Lỗi | Giải pháp | |----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | UNKNOWN | Lỗi này cho biết đã xảy ra một lỗi không xác định hoặc không mong đợi. | | [ITEM_UNAVAILABLE](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_UNAVAILABLE()) | Lỗi này thường xảy ra trong giai đoạn kiểm thử. Nguyên nhân có thể là các sản phẩm chưa có trên môi trường production, hoặc người dùng không thuộc nhóm Testers trong Google Play. | | ADAPTY_NOT_INITIALIZED | Adapty SDK chưa được kích hoạt.
Thường gặp khi màn hình splash hoặc một UI hook sớm gọi các phương thức Adapty trước khi `Adapty.activate` hoàn tất. Triệu chứng này không ổn định và có thể không tái hiện trên máy ảo vì thời gian thực thi trên thiết bị thật khác nhau. Hãy chờ `Adapty.activate` hoàn tất trước khi gọi bất kỳ SDK nào khác. Xem [Thứ tự gọi trong Android SDK](android-sdk-call-order) để biết trình tự đầy đủ. Bạn cũng cần [cấu hình Adapty SDK](sdk-installation-android#activate-adapty-module-of-adapty-sdk) đúng cách bằng phương thức `Adapty.activate`. | | PROFILE_WAS_CHANGED | Hồ sơ người dùng đã thay đổi trong quá trình thực hiện thao tác.
Điều này xảy ra khi một phương thức được gọi trong khi `Adapty.identify` vẫn đang chạy — lệnh gọi đang xử lý đó rơi vào một hồ sơ sắp bị thay thế, và SDK từ chối nó. Hãy chờ `Adapty.identify` hoàn tất trước khi gọi các SDK khác. Xem [Thứ tự gọi trong Android SDK](android-sdk-call-order). | | PRODUCT_NOT_FOUND | Lỗi này cho biết sản phẩm được yêu cầu mua không có sẵn trong cửa hàng. | | INVALID_JSON |

JSON của paywall dự phòng cục bộ không hợp lệ.

Hãy sửa paywall tiếng Anh mặc định trước, sau đó thay thế các paywall cục bộ không hợp lệ. Tham khảo chủ đề [Tùy chỉnh paywall bằng Remote Config](customize-paywall-with-remote-config) để biết cách sửa paywall, và [Định nghĩa paywall dự phòng cục bộ](fallback-paywalls) để biết cách thay thế các paywall cục bộ.

| |

CURRENT_SUBSCRIPTION_TO_UPDATE

\_NOT_FOUND_IN_HISTORY

| Không tìm thấy gói đăng ký gốc cần thay thế trong danh sách gói đăng ký đang hoạt động. | | [BILLING_SERVICE_TIMEOUT](https://developer.android.com/google/play/billing/errors#service_timeout_error_code_-3) | Lỗi này cho biết yêu cầu đã vượt quá thời gian chờ tối đa trước khi Google Play có thể phản hồi. Nguyên nhân có thể là do thao tác được yêu cầu bởi lệnh gọi Play Billing Library bị trễ. | | [FEATURE_NOT_SUPPORTED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#FEATURE_NOT_SUPPORTED()) | Tính năng được yêu cầu không được Play Store hỗ trợ trên thiết bị hiện tại. | | [BILLING_SERVICE_DISCONNECTED](https://developer.android.com/google/play/billing/errors#service_disconnected_error_code_-1) | Lỗi này cho biết kết nối của ứng dụng khách đến dịch vụ Google Play Store thông qua `BillingClient` đã bị ngắt. | | [BILLING_SERVICE_UNAVAILABLE](https://developer.android.com/google/play/billing/errors#service_unavailable_error_code_2) | Lỗi này cho biết dịch vụ Google Play Billing hiện không khả dụng. Trong hầu hết các trường hợp, điều này có nghĩa là có sự cố kết nối mạng ở đâu đó giữa thiết bị khách và các dịch vụ Google Play Billing. | | [BILLING_UNAVAILABLE](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) |

Lỗi này cho biết đã xảy ra sự cố thanh toán trong quá trình mua hàng. Các nguyên nhân có thể bao gồm:

1. Ứng dụng Play Store trên thiết bị của người dùng bị thiếu hoặc đã lỗi thời.

2. Người dùng ở quốc gia không được hỗ trợ.

3. Người dùng thuộc tài khoản doanh nghiệp mà quản trị viên đã tắt tính năng mua hàng.

4. Google Play không thể tính phí phương thức thanh toán của người dùng (ví dụ: thẻ tín dụng đã hết hạn).

5. Người dùng chưa đăng nhập vào ứng dụng Play Store.

| | [DEVELOPER_ERROR](https://developer.android.com/google/play/billing/errors#developer_error) | Lỗi này cho biết bạn đang sử dụng API không đúng cách. | | [BILLING_ERROR](https://developer.android.com/google/play/billing/errors#error_error_code_6) | Lỗi này cho biết có sự cố nội bộ với chính Google Play. | | [ITEM_ALREADY_OWNED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_ALREADY_OWNED()) | Sản phẩm đã được mua trước đó. | | [ITEM_NOT_OWNED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_NOT_OWNED()) | Lỗi này cho biết thao tác được yêu cầu trên mặt hàng thất bại vì người dùng chưa sở hữu nó. | | [BILLING_NETWORK_ERROR](https://developer.android.com/google/play/billing/errors#network_error_error_code_12) | Lỗi này cho biết có sự cố với kết nối mạng giữa thiết bị và hệ thống của Play. | | NO_PRODUCT_IDS_FOUND |

Lỗi này cho biết không có sản phẩm nào trong paywall khả dụng trong cửa hàng.

Nếu bạn gặp lỗi này, hãy làm theo các bước dưới đây để khắc phục:

  1. Kiểm tra xem tất cả các sản phẩm đã được thêm vào Adapty Dashboard chưa.
  2. Đảm bảo rằng **Package name** của ứng dụng khớp với tên trong Google Play Console.
  3. Xác minh rằng các mã định danh sản phẩm từ cửa hàng ứng dụng khớp với những gì bạn đã thêm vào Dashboard. Lưu ý rằng các mã định danh không nên chứa Bundle ID, trừ khi nó đã được bao gồm trong cửa hàng.
  4. Xác nhận rằng trạng thái thanh toán của ứng dụng là **Active** trong cài đặt thuế Google của bạn. Đảm bảo thông tin thuế của bạn được cập nhật và chứng chỉ còn hiệu lực.
  5. Kiểm tra xem tài khoản ngân hàng đã được liên kết với ứng dụng chưa để ứng dụng đủ điều kiện kiếm tiền.
  6. Kiểm tra xem các sản phẩm có khả dụng ở khu vực của bạn không.
  7. Đảm bảo ứng dụng của bạn đang ở một trong các track kiểm thử. Track **Internal testing** là lựa chọn dễ nhất vì không yêu cầu xét duyệt và giữ ứng dụng ẩn với khách hàng.
| | NO_PURCHASES_TO_RESTORE | Lỗi này cho biết Google Play không tìm thấy giao dịch mua để khôi phục. | | AUTHENTICATION_ERROR | Bạn cần [cấu hình Adapty SDK](sdk-installation-android#activate-adapty-module-of-adapty-sdk) đúng cách bằng phương thức `Adapty.activate`. | | BAD_REQUEST | Yêu cầu không hợp lệ.
Đảm bảo bạn đã hoàn thành tất cả các bước cần thiết để [tích hợp với Google Play](google-play-store-connection-configuration). | | SERVER_ERROR | Lỗi máy chủ. | | REQUEST_FAILED | Lỗi này cho biết có sự cố mạng không thể xác định cụ thể. | | DECODING_FAILED | Chúng tôi không thể giải mã phản hồi.
Hãy xem lại code của bạn và đảm bảo rằng các tham số bạn gửi đi là hợp lệ. Ví dụ, lỗi này có thể cho biết bạn đang sử dụng API key không hợp lệ. | | ANALYTICS_DISABLED | Chúng tôi không thể xử lý các sự kiện analytics vì bạn đã [tắt tính năng này](analytics-integration#disabling-external-analytics-for-a-specific-customer). | | WRONG_PARAMETER | Lỗi này cho biết một số tham số của bạn không đúng: để trống khi không được phép để trống, hoặc sai kiểu dữ liệu, v.v. | ## Các vấn đề khác \{#other-issues\} Nếu bạn vẫn chưa tìm được giải pháp, các bước tiếp theo có thể là: - **Nâng cấp SDK lên phiên bản mới nhất**: Chúng tôi luôn khuyến nghị nâng cấp lên các phiên bản SDK mới nhất vì chúng ổn định hơn và bao gồm các bản sửa lỗi đã biết. - **Liên hệ đội ngũ hỗ trợ hoặc nhờ trợ giúp từ các nhà phát triển khác** trong [diễn đàn hỗ trợ](https://adapty.featurebase.app/). - **Liên hệ đội ngũ hỗ trợ qua [support@adapty.io](mailto:support@adapty.io) hoặc qua chat**: Nếu bạn chưa sẵn sàng nâng cấp SDK hoặc việc nâng cấp không giúp được, hãy liên hệ đội ngũ hỗ trợ của chúng tôi. Lưu ý rằng vấn đề của bạn sẽ được giải quyết nhanh hơn nếu bạn [bật verbose logging](sdk-installation-android#logging) và chia sẻ logs với đội ngũ. Bạn cũng có thể đính kèm các đoạn code liên quan. --- # File: migration-to-android-sdk-v4 --- --- title: "Migrate Adapty Android SDK lên v. 4.0" description: "Migrate lên Adapty Android SDK v4.0 bằng cách thay thế các API paywall bằng API flow, tương thích với cả Flow Builder và Paywall Builder." --- Adapty Android SDK 4.0 giới thiệu flows và đổi tên các API paywall cho phù hợp. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi gì trên Adapty Dashboard. ## Tham chiếu nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `Adapty.getPaywall(placementId, locale)` | `Adapty.getFlow(placementId)` | | `Adapty.getPaywallForDefaultAudience(placementId, locale)` | `Adapty.getFlowForDefaultAudience(placementId)` | | `AdaptyUI.getViewConfiguration(paywall)` | `AdaptyUI.getFlowConfiguration(flow, locale)` | | `AdaptyUI.LocalizedViewConfiguration` | `AdaptyUI.FlowConfiguration` | | `Adapty.getPaywallProducts(paywall)` | `Adapty.getPaywallProducts(flow)` | | `Adapty.logShowPaywall(paywall)` | `Adapty.logShowFlow(flow)` | | `AdaptyPaywall` | `AdaptyFlow` | | `AdaptyUI.getPaywallView(...)` | `AdaptyUI.getFlowView(...)` | | `AdaptyPaywallView` | `AdaptyFlowView` | | `AdaptyPaywallScreen` (Compose) | `AdaptyFlowScreen` | | `showPaywall(...)` | `showFlow(...)` | | `AdaptyPaywallInsets` | `AdaptyFlowInsets` | | `AdaptyUiEventListener` | `AdaptyFlowEventListener` | | `AdaptyUiDefaultEventListener` | `AdaptyFlowDefaultEventListener` | | `onPaywallShown` / `onPaywallClosed` | `onFlowShown` / `onFlowClosed` | | `onRenderingError` | `onError` | | `Adapty.updateAttribution(attribution, source)` (`source: String`) | `Adapty.updateAttribution(attribution, source)` (`source: AdaptyAttributionSource`) | | `Adapty.setIntegrationIdentifier(key, value)` | `Adapty.setIntegrationIdentifier(AdaptyIntegrationIdentifier)` | `AdaptyPaywallProduct` giữ nguyên tên — các sản phẩm vẫn thuộc về một flow, và `getPaywallProducts` giờ nhận vào một `AdaptyFlow`. Các phương thức khác của `AdaptyFlowEventListener` (`onProductSelected`, `onPurchaseStarted`, `onPurchaseFinished`, `onPurchaseFailure`, `onRestoreSuccess`, `onRestoreFailure`, `onActionPerformed`, `onAwaitingPurchaseParams`, `onLoadingProductsFailure`, v.v.) giữ nguyên tên và chữ ký. ## Cài đặt \{#installation\} Đặt phiên bản `adapty-bom` thành `4.0.0` (hoặc mới hơn) và đồng bộ hóa dự án. BOM sẽ tự động phân giải các phiên bản `android-sdk` và `android-ui` tương ứng cho bạn. Xem [Cài đặt Adapty SDK](sdk-installation-android) để biết cách khai báo dependency. ## Các API đã bị xóa và không còn được hỗ trợ \{#removed-and-deprecated-apis\} - **`Adapty.makePurchase(activity, product, subscriptionUpdateParams, isOfferPersonalized, callback)`** — đã bị xóa. Overload này đã bị deprecated từ v3. Thay vào đó, hãy truyền các tùy chọn tương tự thông qua `AdaptyPurchaseParameters`: ```diff showLineNumbers - Adapty.makePurchase(activity, product, subscriptionUpdateParams, isOfferPersonalized) { result -> /* ... */ } + val params = AdaptyPurchaseParameters.Builder() + .withSubscriptionUpdateParams(subscriptionUpdateParams) + .withOfferPersonalized(isOfferPersonalized) + .build() + Adapty.makePurchase(activity, product, params) { result -> /* ... */ } ``` - **Onboardings đã bị deprecated.** `AdaptyUI.getOnboardingView` và `AdaptyUI.getOnboardingConfiguration` được đánh dấu `@Deprecated` trong 4.0 — hãy migrate các onboarding sang flow được xây dựng trong [Flow Builder](adapty-flow-builder). ## Tải flows \{#fetching-flows\} ### getPaywall + getViewConfiguration → getFlow + getFlowConfiguration Kiểu trả về của lệnh fetch thay đổi từ `AdaptyPaywall` sang `AdaptyFlow`, và trình tải cấu hình được đổi tên từ `AdaptyUI.getViewConfiguration` thành `AdaptyUI.getFlowConfiguration` (trả về `AdaptyUI.FlowConfiguration` thay vì `AdaptyUI.LocalizedViewConfiguration`). Tham số `locale` được chuyển ra khỏi lệnh fetch và đưa vào `getFlowConfiguration`: ```diff showLineNumbers - Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en") { result -> + Adapty.getFlow("YOUR_PLACEMENT_ID") { result -> if (result is AdaptyResult.Success) { - val paywall = result.value - if (!paywall.hasViewConfiguration) return@getPaywall - AdaptyUI.getViewConfiguration(paywall) { configResult -> + val flow = result.value + if (!flow.hasViewConfiguration) return@getFlow + AdaptyUI.getFlowConfiguration(flow, locale = "en") { configResult -> if (configResult is AdaptyResult.Success) { val flowConfiguration = configResult.value } } } } ``` ### getPaywallProducts(paywall) → getPaywallProducts(flow) `getPaywallProducts` hiện nhận một `AdaptyFlow` được trả về bởi `Adapty.getFlow`: ```diff showLineNumbers - Adapty.getPaywallProducts(paywall) { result -> /* products */ } + Adapty.getPaywallProducts(flow) { result -> /* products */ } ``` ## Theo dõi lượt xem flow \{#tracking-flow-views\} ### logShowPaywall → logShowFlow `logShowPaywall` được đổi tên thành `logShowFlow` và bây giờ nhận vào một `AdaptyFlow` thay vì `AdaptyPaywall`. Sự kiện vẫn được ghi lại dựa trên cùng một biến thể, vì vậy các chỉ số funnel và A/B test hiện tại vẫn hoạt động bình thường mà không cần thay đổi gì trên dashboard. ```diff showLineNumbers - Adapty.logShowPaywall(paywall) + Adapty.logShowFlow(flow) ``` Giống như trong v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được dựng bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi những lượt xem đó. ## Hiển thị flow \{#displaying-flows\} ### getPaywallView / AdaptyPaywallView → getFlowView / AdaptyFlowView \{#getpaywallview--adaptypayallview--getflowview--adaptyflowview\} Đổi tên phương thức factory và kiểu view, đồng thời truyền vào `AdaptyUI.FlowConfiguration`: ```diff showLineNumbers - val paywallView = AdaptyUI.getPaywallView( - activity, - viewConfiguration, - products, - eventListener, - ) + val flowView = AdaptyUI.getFlowView( + activity, + flowConfiguration, + products, + eventListener, + ) ``` Nếu bạn tạo view trực tiếp, phương thức show cũng được đổi tên: ```diff showLineNumbers - val paywallView = AdaptyPaywallView(activity) - paywallView.showPaywall(viewConfiguration, products, eventListener) + val flowView = AdaptyFlowView(activity) + flowView.showFlow(flowConfiguration, products, eventListener) ``` Trong XML layouts, cập nhật thẻ view: ```diff showLineNumbers - + ``` Tham số tùy chọn `personalizedOfferResolver` đã bị xóa khỏi `getFlowView` / `showFlow` / `AdaptyFlowScreen`. Để chỉ định giá cá nhân hóa, hãy thiết lập theo từng sản phẩm thông qua `onAwaitingPurchaseParams` (`AdaptyPurchaseParameters.Builder().withOfferPersonalized(true)`). Một tham số tùy chọn mới `customAssets` cho phép bạn ghi đè hình ảnh và video tại runtime — xem [Tùy chỉnh assets](android-get-pb-paywalls#customize-assets). ### AdaptyPaywallScreen → AdaptyFlowScreen Trong Jetpack Compose, đổi tên composable và cập nhật tham số cấu hình: ```diff showLineNumbers - AdaptyPaywallScreen( - viewConfiguration, + AdaptyFlowScreen( + flowConfiguration, products, eventListener, ) ``` ## Xử lý sự kiện \{#handling-events\} Event listener được đổi tên từ `AdaptyUiEventListener` thành `AdaptyFlowEventListener` (và `AdaptyUiDefaultEventListener` thành `AdaptyFlowDefaultEventListener`). Hầu hết các tên phương thức không thay đổi; các callback vòng đời và rendering được đổi tên: ```diff showLineNumbers - class YourListener : AdaptyUiDefaultEventListener() { + class YourListener : AdaptyFlowDefaultEventListener() { - override fun onPaywallShown(context: Context) {} - override fun onPaywallClosed() {} + override fun onFlowShown(context: Context) {} + override fun onFlowClosed() {} - override fun onRenderingError(error: AdaptyError, context: Context) {} + override fun onError(error: AdaptyError, context: Context) {} } ``` Phần thân handler hiện có không cần thay đổi code — chỉ cần đổi tên kiểu và các override. `onError` sẽ kích hoạt cho cùng các lỗi render như `onRenderingError` đã làm, cộng thêm các lỗi runtime không liên quan đến mua hàng. Xem [Xử lý sự kiện flow & paywall](android-handling-events) để biết danh sách đầy đủ các callback. v4 cũng bổ sung callback `onBackPressed(context): Boolean`, và giá trị mặc định của nó thay đổi cách nút back hệ thống hoạt động. Trước đây, nút back (hoặc cử chỉ vuốt back) sẽ được chuyển xuống activity hoặc fragment của bạn, thường là đóng paywall lại. Trong v4, implementation mặc định sẽ tiêu thụ sự kiện đó, vì vậy **nút back hệ thống không còn tự đóng flow nữa** — giống với iOS, nơi flow không thể bị đóng bằng cử chỉ hệ thống. Hãy cung cấp cho người dùng một cách thoát rõ ràng (nút **Close** hoặc action `on_device_back`), hoặc override `onBackPressed` trả về `false` để khôi phục hành vi cũ. Xem [Nút back hệ thống](android-handling-events#system-back-button) để biết thêm chi tiết. Handler mua hàng mặc định cũng không còn tự động đóng màn hình nữa. Trong v3, `onPurchaseFinished` mặc định sẽ đóng paywall sau bất kỳ giao dịch mua hàng nào đã hoàn thành mà không phải do người dùng hủy (giao dịch thành công hoặc đang chờ xử lý). Trong v4, nó là no-op, vì vậy **flow vẫn mở sau khi mua hàng cho đến khi bạn tự đóng nó** — điều này cũng tương tự như iOS. Nếu bạn đang dựa vào tính năng tự động đóng đó, hãy tự đóng màn hình sau khi giao dịch mua hàng hoàn tất. Xem [Mua hàng thành công, bị hủy hoặc đang chờ xử lý](android-handling-events#successful-canceled-or-pending-purchase) để xem ví dụ. ## Mã định danh attribution và tích hợp \{#attribution-and-integration-identifiers\} ### updateAttribution Tham số `source` thay đổi từ `String` sang kiểu `AdaptyAttributionSource` mới, và `attribution` hiện là `Map` (cũng có thể dùng overload `String` dạng JSON). Sử dụng một trong các nguồn được định nghĩa sẵn: ```diff showLineNumbers - Adapty.updateAttribution(attribution, "appsflyer") { error -> /* handle the error */ } + Adapty.updateAttribution(attribution, AdaptyAttributionSource.APPSFLYER) { error -> /* handle the error */ } ``` Các nguồn được định sẵn: `AdaptyAttributionSource.APPLE_ADS`, `.ADJUST`, `.APPSFLYER`, `.BRANCH`, `.TENJIN`. Với bất kỳ nguồn nào khác, hãy tạo từ một chuỗi: `AdaptyAttributionSource("your_source")`. ### setIntegrationIdentifier `setIntegrationIdentifier(key, value)` được thay thế bằng một phương thức nhận một hoặc nhiều giá trị `AdaptyIntegrationIdentifier`. Tạo từng identifier bằng convenience method thay vì truyền trực tiếp chuỗi key: ```diff showLineNumbers - Adapty.setIntegrationIdentifier("appsflyer_id", appsFlyerId) { error -> /* handle the error */ } + Adapty.setIntegrationIdentifier(AdaptyIntegrationIdentifier.appsflyerId(appsFlyerId)) { error -> /* handle the error */ } ``` Bạn có thể đặt nhiều identifier trong một lần gọi: ```kotlin showLineNumbers Adapty.setIntegrationIdentifier( listOf( AdaptyIntegrationIdentifier.appsflyerId(appsFlyerId), AdaptyIntegrationIdentifier.adjustDeviceId(adjustDeviceId), ) ) { error -> /* handle the error */ } ``` Thay thế mỗi chuỗi key cũ bằng phương thức tiện lợi tương ứng: | Khóa v3 | Phương thức `AdaptyIntegrationIdentifier` v4 | |---|---| | `"adjust_device_id"` | `adjustDeviceId(value)` | | `"airbridge_device_id"` | `airbridgeDeviceId(value)` | | `"amplitude_user_id"` | `amplitudeUserId(value)` | | `"amplitude_device_id"` | `amplitudeDeviceId(value)` | | `"appmetrica_device_id"` | `appmetricaDeviceId(value)` | | `"appmetrica_profile_id"` | `appmetricaProfileId(value)` | | `"appsflyer_id"` | `appsflyerId(value)` | | `"branch_id"` | `branchId(value)` | | `"facebook_anonymous_id"` | `facebookAnonymousId(value)` | | `"firebase_app_instance_id"` | `firebaseAppInstanceId(value)` | | `"mixpanel_user_id"` | `mixpanelUserId(value)` | | `"one_signal_subscription_id"` | `oneSignalSubscriptionId(value)` | | `"one_signal_player_id"` | `oneSignalPlayerId(value)` | | `"posthog_distinct_user_id"` | `posthogDistinctUserId(value)` | | `"pushwoosh_hwid"` | `pushwooshHWID(value)` | | `"tenjin_analytics_installation_id"` | `tenjinAnalyticsInstallationId(value)` | Đối với khóa không có trong danh sách này, hãy tạo mã định danh trực tiếp từ `Key` tùy chỉnh: `AdaptyIntegrationIdentifier(AdaptyIntegrationIdentifier.Key("custom"), customValue)`. --- # File: migration-to-android-312 --- --- title: "Migrate Adapty Android SDK sang v3.12" description: "Migrate lên Adapty Android SDK v3.12 để có hiệu suất tốt hơn và các tính năng kiếm tiền mới." --- Trong Adapty SDK 3.12.0, chúng tôi đã xóa phương thức `logShowOnboarding` khỏi SDK. Nếu bạn đang sử dụng phương thức này, nó sẽ không còn khả dụng khi bạn nâng cấp SDK lên phiên bản 3.12 trở lên. Thay vào đó, bạn có thể [tạo onboarding trong trình tạo onboarding no-code của Adapty](onboardings). Dữ liệu analytics cho các onboarding này được theo dõi tự động, và bạn có nhiều tùy chọn tùy chỉnh. --- # File: migration-to-android-310 --- --- title: "Hướng dẫn migration lên Android 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. `AdaptyUiPersonalizedOfferResolver` đã bị loại bỏ. Nếu bạn đang sử dụng nó, hãy truyền vào callback `onAwaitingPurchaseParams`. 2. Cập nhật chữ ký phương thức `onAwaitingSubscriptionUpdateParams` cho các paywall dùng Paywall Builder. ## Cập nhật callback tham số mua hàng \{#update-purchase-parameters-callback\} Phương thức `onAwaitingSubscriptionUpdateParams` đã được đổi tên thành `onAwaitingPurchaseParams` và hiện sử dụng `AdaptyPurchaseParameters` thay vì `AdaptySubscriptionUpdateParameters`. Điều này cho phép bạn chỉ định các tham số thay thế gói đăng ký (crossgrade) và xác định liệu giá có được cá nhân hóa hay không ([đọc thêm](https://developer.android.com/google/play/billing/integrate#personalized-price)), cùng với các tham số mua hàng khác. ```diff showLineNumbers - override fun onAwaitingSubscriptionUpdateParams( - product: AdaptyPaywallProduct, - context: Context, - onSubscriptionUpdateParamsReceived: SubscriptionUpdateParamsCallback, - ) { - onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters(...)) - } + override fun onAwaitingPurchaseParams( + product: AdaptyPaywallProduct, + context: Context, + onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, + ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { + onPurchaseParamsReceived( + AdaptyPurchaseParameters.Builder() + .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) + .withOfferPersonalized(true) + .build() + ) + return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked + } ``` Nếu không cần thêm tham số nào, bạn có thể dùng đơn giản như sau: ```kotlin showLineNumbers + override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived(AdaptyPurchaseParameters.Empty) return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` --- # File: migration-to-android-sdk-34 --- --- title: "Migrate Adapty Android SDK to v3.4" description: "Migrate sang Adapty Android SDK v3.4 để cải thiện hiệu suất và các tính năng kiếm tiền mới." --- Adapty SDK 3.4.0 là một bản phát hành lớn, giới thiệu các cải tiến yêu cầu bạn thực hiện các bước migration. ## Cập nhật 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](android-use-fallback-paywalls) bằng các file mới. ## Cập nhật cài đặt 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 cài đặt của nó. Trong các phiên bản trước, bạn phải khôi phục giao dịch mua để Adapty có thể nhận diện các giao dịch được thực hiện qua cơ sở hạ tầng riêng của bạn, vì Adapty không có quyền truy cập trực tiếp vào chúng trong Observer Mode. Nếu bạn sử dụng paywall, bạn cũng cần liên kết thủ công từng giao dịch với paywall đã khởi tạo nó. Trong phiên bản mới, bạn phải báo cáo rõ ràng từng giao dịch để Adapty nhận diện. Nếu bạn sử dụng paywall, bạn cũng cần truyền variation ID để liên kết giao dịch với paywall đã được sử dụng. :::warning **Đừng bỏ qua bướ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 hiển thị trong analytics và sẽ không được gửi đến các tích hợp. ::: ```diff showLineNumbers - Adapty.restorePurchases { result -> - if (result is AdaptyResult.Success) { - // success - } - } - - Adapty.setVariationId(transactionId, variationId) { error -> - if (error == null) { - // success - } - } + val transactionInfo = TransactionInfo.fromPurchase(purchase) + + Adapty.reportTransaction(transactionInfo, variationId) { result -> + if (result is AdaptyResult.Success) { + // success + } + } ``` ```diff showLineNumbers - Adapty.restorePurchases(result -> { - if (result instanceof AdaptyResult.Success) { - // success - } - }); - - Adapty.setVariationId(transactionId, variationId, error -> { - if (error == null) { - // success - } - }); + TransactionInfo transactionInfo = TransactionInfo.fromPurchase(purchase); + + Adapty.reportTransaction(transactionInfo, variationId, result -> { + if (result instanceof AdaptyResult.Success) { + // success + } + }); ``` --- # File: migration-to-android330 --- --- title: "Migrate Adapty Android SDK sang v3.3" description: "Migrate sang Adapty Android SDK v3.3 để cải thiện hiệu suất và có thêm 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 đến 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 cách xử lý thanh toán trong các paywall không được tạo bằng Paywall Builder. Dừng xử lý các mã lỗi `USER_CANCELED` và `PENDING_PURCHASE`. Giao dịch bị hủy không còn được coi là lỗi nữa và sẽ xuất hiện trong kết quả mua hàng thành công (non-error). 2. Thay thế các sự kiện `onPurchaseCanceled` và `onPurchaseSuccess` bằng sự kiện `onPurchaseFinished` mới cho các paywall được tạo bằng Paywall Builder. Thay đổi này vì cùng lý do: giao dịch bị hủy không còn bị coi là lỗi và sẽ được đưa vào kết quả mua hàng thành công. 3. Thay đổi chữ ký phương thức `onAwaitingSubscriptionUpdateParams` cho các paywall của Paywall Builder. 4. Cập nhật phương thức dùng để cung cấp paywall dự phòng nếu bạn truyền URI file trực tiếp. 5. Cập nhật cấu hình tích hợp cho Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase và Google Analytics, Mixpanel, OneSignal, Pushwoosh. ## Cập nhật thực hiện mua hàng \{#update-making-purchase\} Trước đây, giao dịch bị hủy và đang chờ xử lý được coi là lỗi và trả về mã `USER_CANCELED` và `PENDING_PURCHASE` tương ứng. Giờ đây, một lớp `AdaptyPurchaseResult` mới được sử dụng để biểu thị các trạng thái: đã hủy, thành công và đang chờ xử lý. Cập nhật code xử lý mua hàng như sau: ~~~diff Adapty.makePurchase(activity, product) { result -> when (result) { is AdaptyResult.Success -> { - val info = result.value - val profile = info?.profile - - if (profile?.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true) { - // Grant access to the paid features - } + when (val purchaseResult = result.value) { + is AdaptyPurchaseResult.Success -> { + val profile = purchaseResult.profile + if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { + // Grant access to the paid features + } + } + + is AdaptyPurchaseResult.UserCanceled -> { + // Handle the case where the user canceled the purchase + } + + is AdaptyPurchaseResult.Pending -> { + // Handle deferred purchases (e.g., the user will pay offline with cash + } + } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } ~~~ Để xem ví dụ code đầy đủ, hãy xem trang [Thực hiện mua hàng trong ứng dụng mobile](android-making-purchases#make-purchase). ## Chỉnh sửa sự kiện mua hàng trong Paywall Builder \{#modify-paywall-builder-purchase-events\} 1. Thêm sự kiện `onPurchaseFinished`: ```diff showLineNumbers + public override fun onPurchaseFinished( + purchaseResult: AdaptyPurchaseResult, + product: AdaptyPaywallProduct, + context: Context, + ) { + when (purchaseResult) { + is AdaptyPurchaseResult.Success -> { + // Grant access to the paid features + } + is AdaptyPurchaseResult.UserCanceled -> { + // Handle the case where the user canceled the purchase + } + is AdaptyPurchaseResult.Pending -> { + // Handle deferred purchases (e.g., the user will pay offline with cash) + } + } + } ``` Để xem ví dụ code đầy đủ, hãy xem phần [Mua hàng thành công, bị hủy hoặc đang chờ xử lý](android-handling-events#successful-canceled-or-pending-purchase) và mô tả sự kiện. 2. Xóa phần xử lý sự kiện `onPurchaseCancelled`: ```diff showLineNumbers - public override fun onPurchaseCanceled( - product: AdaptyPaywallProduct, - context: Context, - ) {} ``` 3. Xóa `onPurchaseSuccess`: ```diff showLineNumbers - public override fun onPurchaseSuccess( - profile: AdaptyProfile?, - product: AdaptyPaywallProduct, - context: Context, - ) { - // Your logic on successful purchase - } ``` ## Thay đổi chữ ký của phương thức onAwaitingSubscriptionUpdateParams \{#change-the-signature-of--onawaitingsubscriptionupdateparams-method\} Bây giờ, nếu một gói đăng ký mới được mua trong khi gói đăng ký khác vẫn còn hoạt động, hãy gọi `onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters...))` nếu gói đăng ký mới sẽ thay thế gói đang hoạt động, hoặc `onSubscriptionUpdateParamsReceived(null)` nếu gói đang hoạt động vẫn tiếp tục và gói mới sẽ được thêm riêng biệt: ```diff showLineNumbers - public override fun onAwaitingSubscriptionUpdateParams( - product: AdaptyPaywallProduct, - context: Context, - ): AdaptySubscriptionUpdateParameters? { - return AdaptySubscriptionUpdateParameters(...) - } + public override fun onAwaitingSubscriptionUpdateParams( + product: AdaptyPaywallProduct, + context: Context, + onSubscriptionUpdateParamsReceived: SubscriptionUpdateParamsCallback, + ) { + onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters(...)) + } ``` Xem phần tài liệu [Nâng cấp gói đăng ký](android-handling-events#upgrade-subscription) để xem ví dụ code hoàn chỉnh. ## Cập nhật cung cấp paywall dự phòng \{#update-providing-fallback-paywalls\} Nếu bạn truyền URI file để cung cấp paywall dự phòng, hãy cập nhật cách thực hiện như sau: ```diff showLineNumbers val fileUri: Uri = // Get the URI for the file with fallback paywalls - Adapty.setFallbackPaywalls(fileUri, callback) + Adapty.setFallbackPaywalls(FileLocation.fromFileUri(fileUri), callback) ``` ```diff showLineNumbers Uri fileUri = // Get the URI for the file with fallback paywalls - Adapty.setFallbackPaywalls(fileUri, callback); + Adapty.setFallbackPaywalls(FileLocation.fromFileUri(fileUri), callback); ``` ## 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 Android SDK 3.3.0 trở lên, hãy cập nhật cấu hình SDK cho các tích hợp sau theo hướng dẫn trong các phần bên dưới. ### Adjust \{#adjust\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Adjust](adjust#connect-your-app-to-adjust). ```diff showLineNumbers - Adjust.getAttribution { attribution -> - if (attribution == null) return@getAttribution - - Adjust.getAdid { adid -> - if (adid == null) return@getAdid - - Adapty.updateAttribution(attribution, AdaptyAttributionSource.ADJUST, adid) { error -> - // Handle the error - } - } - } + Adjust.getAdid { adid -> + if (adid == null) return@getAdid + + Adapty.setIntegrationIdentifier("adjust_device_id", adid) { error -> + if (error != null) { + // Handle the error + } + } + } + + Adjust.getAttribution { attribution -> + if (attribution == null) return@getAttribution + + Adapty.updateAttribution(attribution, "adjust") { error -> + if (error != null) { + // Handle the error + } + } + } ``` ```diff showLineNumbers val config = AdjustConfig(context, adjustAppToken, environment) config.setOnAttributionChangedListener { attribution -> attribution?.let { attribution -> - Adapty.updateAttribution(attribution, AdaptyAttributionSource.ADJUST) { error -> + Adapty.updateAttribution(attribution, "adjust") { error -> if (error != null) { // Handle the error } } } } Adjust.onCreate(config) ``` ### AirBridge \{#airbridge\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp AirBridge](airbridge#connect-your-app-to-airbridge). ```diff showLineNumbers Airbridge.getDeviceInfo().getUUID(object: AirbridgeCallback.SimpleCallback() { override fun onSuccess(result: String) { - val params = AdaptyProfileParameters.Builder() - .withAirbridgeDeviceId(result) - .build() - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("airbridge_device_id", result) { error -> + if (error != null) { + // Handle the error + } + } } override fun onFailure(throwable: Throwable) { } }) ``` ### Amplitude \{#amplitude\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Amplitude](amplitude#sdk-configuration). ```diff showLineNumbers // For Amplitude maintenance SDK (obsolete) val amplitude = Amplitude.getInstance() val amplitudeDeviceId = amplitude.getDeviceId() val amplitudeUserId = amplitude.getUserId() //for actual Amplitude Kotlin SDK val amplitude = Amplitude( Configuration( apiKey = AMPLITUDE_API_KEY, context = applicationContext ) ) val amplitudeDeviceId = amplitude.store.deviceId val amplitudeUserId = amplitude.store.userId // - val params = AdaptyProfileParameters.Builder() - .withAmplitudeDeviceId(amplitudeDeviceId) - .withAmplitudeUserId(amplitudeUserId) - .build() - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("amplitude_user_id", amplitudeUserId) { error -> + if (error != null) { + // Handle the error + } + } + Adapty.setIntegrationIdentifier("amplitude_device_id", amplitudeDeviceId) { error -> + if (error != null) { + // Handle the error + } + } ``` ### AppMetrica \{#appmetrica\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp AppMetrica](appmetrica#sdk-configuration). ```diff showLineNumbers val startupParamsCallback = object: StartupParamsCallback { override fun onReceive(result: StartupParamsCallback.Result?) { val deviceId = result?.deviceId ?: return - val params = AdaptyProfileParameters.Builder() - .withAppmetricaDeviceId(deviceId) - .withAppmetricaProfileId("YOUR_ADAPTY_CUSTOMER_USER_ID") - .build() - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("appmetrica_device_id", deviceId) { error -> + if (error != null) { + // Handle the error + } + } + + Adapty.setIntegrationIdentifier("appmetrica_profile_id", "YOUR_ADAPTY_CUSTOMER_USER_ID") { error -> + if (error != null) { + // Handle the error + } + } } override fun onRequestError( reason: StartupParamsCallback.Reason, result: StartupParamsCallback.Result? ) { // Handle the error } } AppMetrica.requestStartupParams(context, startupParamsCallback, listOf(StartupParamsCallback.APPMETRICA_DEVICE_ID)) ``` ### AppsFlyer \{#appsflyer\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp AppsFlyer](appsflyer#connect-your-app-to-appsflyer). ```diff showLineNumbers val conversionListener: AppsFlyerConversionListener = object : AppsFlyerConversionListener { override fun onConversionDataSuccess(conversionData: Map) { - Adapty.updateAttribution( - conversionData, - AdaptyAttributionSource.APPSFLYER, - AppsFlyerLib.getInstance().getAppsFlyerUID(context) - ) { error -> - if (error != null) { - // Handle the error - } - } + val uid = AppsFlyerLib.getInstance().getAppsFlyerUID(context) + Adapty.setIntegrationIdentifier("appsflyer_id", uid) { error -> + if (error != null) { + // Handle the error + } + } + Adapty.updateAttribution(conversionData, "appsflyer") { error -> + if (error != null) { + // Handle the error + } + } } } ``` ### Branch \{#branch\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Branch](branch#connect-your-app-to-branch). ```diff showLineNumbers // Login and update attribution Branch.getAutoInstance(this) .setIdentity("YOUR_USER_ID") { referringParams, error -> referringParams?.let { data -> - Adapty.updateAttribution(data, AdaptyAttributionSource.BRANCH) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.updateAttribution(data, "branch") { error -> + if (error != null) { + // Handle the error + } + } } } // Logout Branch.getAutoInstance(context).logout() ``` ### Facebook Ads \{#facebook-ads\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Facebook Ads](facebook-ads#connect-your-app-to-facebook-ads). ```diff showLineNumbers - val builder = AdaptyProfileParameters.Builder() - .withFacebookAnonymousId(AppEventsLogger.getAnonymousAppDeviceGUID(context)) - - Adapty.updateProfile(builder.build()) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier( + "facebook_anonymous_id", + AppEventsLogger.getAnonymousAppDeviceGUID(context) + ) { error -> + if (error != null) { + // Handle the error + } + } ``` ### Firebase và Google Analytics \{#firebase-and-google-analytics\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Firebase và Google Analytics](firebase-and-google-analytics). ```diff showLineNumbers // After Adapty.activate() FirebaseAnalytics.getInstance(context).appInstanceId.addOnSuccessListener { appInstanceId -> - Adapty.updateProfile( - AdaptyProfileParameters.Builder() - .withFirebaseAppInstanceId(appInstanceId) - .build() - ) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId) { error -> + if (error != null) { + // Handle the error + } + } } ``` ```diff showLineNumbers // After Adapty.activate() - FirebaseAnalytics.getInstance(context).getAppInstanceId().addOnSuccessListener(appInstanceId -> { - AdaptyProfileParameters params = new AdaptyProfileParameters.Builder() - .withFirebaseAppInstanceId(appInstanceId) - .build(); - - Adapty.updateProfile(params, error -> { - if (error != null) { - // Handle the error - } - }); - }); + FirebaseAnalytics.getInstance(context).getAppInstanceId().addOnSuccessListener(appInstanceId -> { + Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId, error -> { + if (error != null) { + // Handle the error + } + }); + }); ``` ### Mixpanel \{#mixpanel\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Mixpanel](mixpanel#sdk-configuration). ```diff showLineNumbers - val params = AdaptyProfileParameters.Builder() - .withMixpanelUserId(mixpanelAPI.distinctId) - .build() - - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelAPI.distinctId) { error -> + if (error != null) { + // Handle the error + } + } ``` ### OneSignal \{#onesignal\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp OneSignal](onesignal#sdk-configuration). ```diff showLineNumbers // SubscriptionID val oneSignalSubscriptionObserver = object: IPushSubscriptionObserver { override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) { - val params = AdaptyProfileParameters.Builder() - .withOneSignalSubscriptionId(state.current.id) - .build() - - Adapty.updateProfile(params) { error -> + Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.current.id) { error -> if (error != null) { // Handle the error } } } } ``` ```diff showLineNumbers // SubscriptionID IPushSubscriptionObserver oneSignalSubscriptionObserver = state -> { - AdaptyProfileParameters params = new AdaptyProfileParameters.Builder() - .withOneSignalSubscriptionId(state.getCurrent().getId()) - .build(); - Adapty.updateProfile(params, error -> { + Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.getCurrent().getId(), error -> { if (error != null) { // Handle the error } }); }; ``` ```diff showLineNumbers // PlayerID val osSubscriptionObserver = OSSubscriptionObserver { stateChanges -> stateChanges?.to?.userId?.let { playerId -> - val params = AdaptyProfileParameters.Builder() - .withOneSignalPlayerId(playerId) - .build() - - Adapty.updateProfile(params) { error -> + Adapty.setIntegrationIdentifier("one_signal_player_id", playerId) { error -> if (error != null) { // Handle the error } - } } } ``` ```diff showLineNumbers // PlayerID OSSubscriptionObserver osSubscriptionObserver = stateChanges -> { OSSubscriptionState to = stateChanges != null ? stateChanges.getTo() : null; String playerId = to != null ? to.getUserId() : null; if (playerId != null) { - AdaptyProfileParameters params1 = new AdaptyProfileParameters.Builder() - .withOneSignalPlayerId(playerId) - .build(); - - Adapty.updateProfile(params1, error -> { + Adapty.setIntegrationIdentifier("one_signal_player_id", playerId, error -> { if (error != null) { // Handle the error } - }); } }; ``` ### Pushwoosh \{#pushwoosh\} Cập nhật code ứng dụng mobile của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Pushwoosh](pushwoosh#sdk-configuration). ```diff showLineNumbers - val params = AdaptyProfileParameters.Builder() - .withPushwooshHwid(Pushwoosh.getInstance().hwid) - .build() - Adapty.updateProfile(params) { error -> + Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().hwid) { error -> if (error != null) { // Handle the error } } ``` ```diff showLineNumbers - AdaptyProfileParameters params = new AdaptyProfileParameters.Builder() - .withPushwooshHwid(Pushwoosh.getInstance().getHwid()) - .build(); - - Adapty.updateProfile(params, error -> { + Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().getHwid(), error -> { if (error != null) { // Handle the error } }); ``` --- # File: migration-to-android-sdk-v3 --- --- title: "Migrate Adapty Android SDK to v3.0" description: "Migrate to Adapty Android SDK v3.0 for better performance and new monetization features." --- Adapty SDK v3.0 hỗ trợ [Adapty Paywall Builder](adapty-paywall-builder) mới — công cụ no-code thân thiện để 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à mang lại doanh thu tốt hơn. Adapty SDK được phân phối dưới dạng BoM (Bill of Materials), đảm bảo phiên bản Adapty SDK và AdaptyUI SDK trong ứng dụng của bạn luôn nhất quán với nhau. Để migrate lên v3.0, hãy cập nhật code như sau: ```diff showLineNumbers dependencies { ... - implementation 'io.adapty:android-sdk:2.11.5' - implementation 'io.adapty:android-ui:2.11.3' + implementation platform('io.adapty:adapty-bom:3.0.4') + implementation 'io.adapty:android-sdk' + implementation 'io.adapty:android-ui' } ``` ```diff showLineNumbers dependencies { ... - implementation("io.adapty:android-sdk:2.11.5") - implementation("io.adapty:android-ui:2.11.3") + implementation(platform("io.adapty:adapty-bom:3.0.4")) + implementation("io.adapty:android-sdk") + implementation("io.adapty:android-ui") } ``` ```diff showLineNumbers //libs.versions.toml [versions] .. - adapty = "2.11.5" - adaptyUi = "2.11.3" + adaptyBom = "3.0.4" [libraries] .. - adapty = { group = "io.adapty", name = "android-sdk", version.ref = "adapty" } - adapty-ui = { group = "io.adapty", name = "android-ui", version.ref = "adaptyUi" } + adapty-bom = { module = "io.adapty:adapty-bom", version.ref = "adaptyBom" } + adapty = { module = "io.adapty:android-sdk" } + adapty-ui = { module = "io.adapty:android-ui" } //module-level build.gradle.kts dependencies { ... + implementation(libs.adapty.bom) implementation(libs.adapty) implementation(libs.adapty.ui) } ``` --- # End of Documentation _Generated on: 2026-07-24T13:01:43.367Z_ _Successfully processed: 41/41 files_ # API - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.369Z Total files: 18 --- # File: developer-cli --- --- title: "Developer CLI" description: "Tổng quan về Adapty Developer CLI." --- **Adapty Developer CLI** là công cụ dòng lệnh giúp bạn quản lý tài khoản Adapty mà không cần mở Dashboard. Công cụ này cung cấp các tính năng cấu hình chính, có thể sử dụng trực tiếp từ terminal hoặc trong các môi trường tự động hóa. **Bạn có thể làm gì với CLI:** - Tạo và cấu hình ứng dụng iOS và Android trong tài khoản Adapty của bạn - Định nghĩa các mức độ truy cập — các cấp gói đăng ký mà ứng dụng kiểm tra tại runtime - Thiết lập sản phẩm và ánh xạ chúng với ID cửa hàng trên App Store và Google Play - Tạo paywall và gán sản phẩm cho chúng - Cấu hình placement để lấy paywall thông qua SDK :::link Bạn đang dùng trợ lý AI hoặc MCP client? [Adapty CLI skill](https://github.com/adaptyteam/adapty-cli/tree/main/skills/adapty-cli) có sẵn để giúp các LLM làm việc với CLI. ::: --- # File: developer-cli-quickstart --- --- title: "Hướng dẫn bắt đầu nhanh với Adapty Developer CLI" description: "Thiết lập tài khoản Adapty từ đầu đến cuối bằng Developer CLI — từ tạo app đến placement hoạt động thực tế, chỉ với vài lệnh." --- :::link Bạn đang dùng trợ lý AI? Có sẵn [Adapty CLI skill](https://github.com/adaptyteam/adapty-cli/tree/main/skills/adapty-cli) để giúp các LLM làm việc với CLI. ::: Adapty CLI cho phép bạn thiết lập toàn bộ cấu hình app từ dòng lệnh. Dùng nó thay thế cho [hướng dẫn bắt đầu trên dashboard](integrate-payments) nếu bạn thích làm việc với terminal hoặc [MCP clients](https://github.com/adaptyteam/adapty-cli/tree/main/skills/adapty-cli). :::note Kết nối Adapty với App Store Connect và Google Play vẫn cần thiết lập một lần trên dashboard — được đề cập ở bước 3. ::: Sau khi hoàn thành, app, mức độ truy cập, sản phẩm, paywall và placement của bạn đều hiển thị trên [Adapty dashboard](https://app.adapty.io). ## 1. Cài đặt CLI \{#1-install-the-cli\} Yêu cầu [Node.js](https://nodejs.org/en/download) phiên bản 18 trở lên. Để cài đặt CLI, chạy lệnh: ```bash npm install -g adapty ``` Hoặc, chạy trực tiếp: ```bash npx adapty auth login ``` ## 2. Xác thực \{#2-authenticate\} Chạy lệnh đăng nhập để kết nối CLI với tài khoản Adapty của bạn. ```bash adapty auth login ``` CLI sẽ mở một tab trình duyệt. Khớp mã hiển thị trong terminal với mã hiển thị trên trình duyệt, sau đó nhấp **Authorize**. Terminal xác nhận khi quá trình xác thực hoàn tất. ## 3. Tạo app của bạn \{#3-create-your-app\} Một app trong Adapty đại diện cho ứng dụng di động của bạn. Một Adapty app kết nối với cả App Store lẫn Google Play — bạn chỉ cần tạo một app, bất kể bạn phát hành trên bao nhiêu cửa hàng. ```bash adapty apps create --title "My App" --platform ios --platform android --apple-bundle-id com.example.app --google-bundle-id com.example.app ``` ```bash adapty apps create --title "My App" --platform ios --apple-bundle-id com.example.app ``` ```bash adapty apps create --title "My App" --platform android --google-bundle-id com.example.app ``` Lệnh trả về một ``. Dùng ID này trong tất cả các lệnh tiếp theo. :::important Trước khi tiếp tục, hãy kết nối app của bạn với App Store Connect và Google Play trên Adapty dashboard. ID sản phẩm từ cả hai cửa hàng là bắt buộc ở bước 5. - [Kết nối App Store Connect](app-store-connection-configuration) - [Kết nối Google Play](google-play-store-connection-configuration) ::: ## 4. Tạo mức độ truy cập (tùy chọn) \{#4-create-an-access-level-optional\} [Mức độ truy cập](access-level) kiểm soát những gì người dùng có thể truy cập sau khi mua hàng. Thay vì kiểm tra xem người dùng đã mua sản phẩm cụ thể nào, app của bạn chỉ cần kiểm tra xem người dùng có mức độ truy cập tương ứng hay không. Cách này giúp tách biệt logic app của bạn khỏi các ID sản phẩm cụ thể. Mức độ truy cập `premium` được tự động tạo cùng với mỗi app mới. **Với hầu hết các app, bạn có thể bỏ qua bước này.** Dùng `premium` làm access level ID ở bước 5. Chỉ chạy lệnh này nếu các sản phẩm khác nhau mở khóa các tính năng khác nhau cho các nhóm người dùng khác nhau — ví dụ, nếu người đăng ký "Basic" và người đăng ký "Pro" có quyền truy cập vào các phần khác nhau của app. ```bash adapty access-levels create --app --sdk-id "pro" --title "Pro" ``` - `--sdk-id` là định danh bạn sẽ dùng trong code app để kiểm tra xem một tính năng có nên khả dụng với người dùng hay không (ví dụ: `if user.hasAccessLevel("pro")`). Nếu bạn bỏ qua bước này và dùng mức độ truy cập mặc định, `--sdk-id` của nó là `premium`. - `--title` là nhãn hiển thị để bạn tham khảo trên Adapty dashboard. Lệnh trả về một ``. ## 5. Tạo sản phẩm \{#5-create-a-product\} Trong Adapty, một [sản phẩm](product) đại diện cho bất kỳ thứ gì app của bạn bán — gói đăng ký hoặc sản phẩm mua một lần. Các mục từ App Store Connect và Google Play có thể được nhóm thành một sản phẩm Adapty duy nhất và quản lý từ một nơi. Bạn sẽ cần ID sản phẩm từ mỗi cửa hàng: Apple product ID từ App Store Connect, và Android product ID cùng base plan ID từ Google Play Console. Xem [Sản phẩm](quickstart-products) để biết chi tiết về nơi tìm chúng. Nếu bạn bỏ qua bước 4, hãy dùng `default_access_level.id` được trả về bởi lệnh `apps create` ở bước 3 làm ``. :::important ID sản phẩm trong cửa hàng mà bạn liên kết ở đây (`--ios-product-id`, `--android-product-id`) không thể thay đổi sau khi tạo. Để dùng các ID sản phẩm khác, hãy tạo một sản phẩm mới. ::: ```bash adapty products create --app --title "My Product" --access-level-id --period monthly --ios-product-id --android-product-id --android-base-plan-id ``` ```bash adapty products create --app --title "My Product" --access-level-id --period monthly --ios-product-id ``` ```bash adapty products create --app --title "My Product" --access-level-id --period monthly --android-product-id --android-base-plan-id ``` Lệnh trả về một ``. ## 6. Tạo paywall \{#6-create-a-paywall\} Một [paywall](paywalls) là nơi chứa các sản phẩm của bạn. Trong Adapty, paywall là cách duy nhất để đưa sản phẩm đến người dùng. Mọi sản phẩm đều phải nằm trong một paywall trước khi có thể xuất hiện trong app của bạn. :::important Khi một paywall đã được liên kết với một placement, các sản phẩm của nó không thể thay đổi. Để dùng các sản phẩm khác, hãy tạo một paywall mới và cập nhật placement để trỏ đến nó. ::: ```bash adapty paywalls create --app --title "My Paywall" --product-id ``` ```bash adapty paywalls create --app --title "My Paywall" --product-id --product-id ``` Lệnh trả về một ``. ## 7. Tạo placement \{#7-create-a-placement\} Một [placement](placements) là điểm trong app của bạn nơi bạn hiển thị paywall. Thứ duy nhất bạn hardcode trong code app là placement ID. Tất cả những thứ còn lại — hiển thị paywall nào và cho người dùng nào — được quản lý trên dashboard mà không cần phát hành phiên bản app mới. `--developer-id` là chuỗi bạn sẽ tham chiếu sau này trong code app khi hỏi Adapty paywall nào cần hiển thị tại điểm này. Chọn tên mô tả vị trí, như `"main"`, `"onboarding"`, hoặc `"settings"`. ```bash adapty placements create --app --title "Main" --developer-id "main" --audiences '[{"segment_ids":[],"paywall_id":"","priority":0}]' ``` Cờ `--audiences` kiểm soát paywall nào được hiển thị cho người dùng nào. Ví dụ trên thiết lập một đối tượng mặc định duy nhất — mọi người dùng tại placement này đều thấy cùng một paywall. ## Tiếp theo là gì \{#whats-next\} Tất cả các thực thể hiện đã hiển thị trên [Adapty dashboard](https://app.adapty.io). Tiếp theo: - [Thiết kế paywall của bạn](adapty-paywall-builder) — dùng Paywall Builder không cần code để thêm hình ảnh, bố cục và nội dung vào paywall bạn vừa tạo. - [Tích hợp Adapty SDK](quickstart-sdk) — thêm SDK vào app của bạn để lấy và hiển thị placement. - Định tuyến các [phân khúc](segments) người dùng khác nhau đến các paywall khác nhau — xem [`placements update`](developer-cli-reference#adapty-placements-update) và [`segments list`](developer-cli-reference#adapty-segments-list) trong tài liệu tham khảo đầy đủ. --- # File: developer-cli-authentication --- --- title: "Xác thực trong Adapty Developer CLI" description: "Cách xác thực với Adapty Developer CLI." --- :::link Đang dùng AI assistant? Có sẵn một [Adapty CLI skill](https://github.com/adaptyteam/adapty-cli/tree/main/skills/adapty-cli) để hỗ trợ LLM làm việc với CLI. ::: CLI yêu cầu xác thực để gọi Adapty API. ## Đăng nhập \{#log-in\} Để đăng nhập: 1. Trong terminal, chạy lệnh: ```bash adapty auth login ``` 2. CLI in ra một mã xác minh theo định dạng `XXXX-XXXX` và mở Adapty Dashboard trong trình duyệt. 3. Trên trang ủy quyền, xác nhận rằng mã khớp với kết quả hiển thị trong terminal. 4. Nhấp vào **Authorize**. Trình duyệt hiển thị "CLI authorized! You can close this tab." 5. Quay lại terminal, CLI xác nhận bạn đã được xác thực. Nếu mã hết hạn trước khi bạn ủy quyền, hoặc nếu bạn nhấp vào **Deny**, hãy chạy lại lệnh sau để khởi động lại flow: ```bash adapty auth login ``` ## Quản lý xác thực \{#manage-authentication\} ### Kiểm tra trạng thái xác thực \{#check-authentication-status\} Để xem trạng thái xác thực hiện tại, chạy: ```bash adapty auth status ``` Khi đã xác thực, kết quả hiển thị email, tiền tố token đã che, và đường dẫn đến tệp cấu hình cục bộ: ``` Email: you@example.com Token: abcd1234**** Config: ~/.config/adapty/config.json ``` Khi chưa xác thực: ``` Not authenticated. Run `adapty auth login`. ``` ### Xác minh token của bạn \{#verify-your-token\} Để xác nhận token hợp lệ và xem thông tin tài khoản, chạy: ```bash adapty auth whoami ``` Khác với `adapty auth status`, lệnh này thực hiện một yêu cầu trực tiếp đến máy chủ để xác minh token. ### Đăng xuất \{#log-out\} Để xóa thông tin đăng nhập đã lưu trữ cục bộ, chạy: ```bash adapty auth logout ``` Lệnh này xóa `~/.config/adapty/config.json`. Token vẫn còn hiệu lực phía máy chủ cho đến khi hết hạn — nếu bạn cần vô hiệu hóa ngay lập tức, hãy dùng `adapty auth revoke` thay thế. ### Thu hồi token \{#revoke-your-token\} Để vô hiệu hóa token trên máy chủ và xóa nó cục bộ, chạy: ```bash adapty auth revoke ``` Dùng lệnh này khi bạn muốn vô hiệu hóa hoàn toàn một token — ví dụ, nếu thông tin đăng nhập của bạn có thể đã bị lộ. Sau khi thu hồi, chạy `adapty auth login` để xác thực lại. ## Lỗi token \{#token-errors\} Nếu token bị thu hồi hoặc không còn hợp lệ, các lệnh CLI trả về lỗi 401. Để xác thực lại, chạy: ```bash adapty auth login ``` --- # File: developer-cli-reference --- --- title: "Tài liệu đầy đủ về Adapty Developer CLI" description: "Tài liệu đầy đủ về tất cả các lệnh của Adapty Developer CLI." --- :::link Đang dùng trợ lý AI? Có sẵn [Adapty CLI skill](https://github.com/adaptyteam/adapty-cli/tree/main/skills/adapty-cli) để giúp các LLM làm việc với CLI. ::: Bài viết này liệt kê tất cả các lệnh Adapty CLI cùng với các đối số, cờ và giá trị được chấp nhận. :::link Để thiết lập xác thực và quản lý token, xem [Xác thực](developer-cli-authentication). ::: ## Cờ toàn cục \{#global-flags\} Các cờ này có thể dùng với tất cả các lệnh. | Cờ | Mô tả | |---|---| | `--json` | Xuất dưới dạng JSON thay vì văn bản định dạng | | `--help` | Hiển thị trợ giúp lệnh | Tất cả lệnh `list` cũng chấp nhận cờ phân trang: | Cờ | Mặc định | Mô tả | |---|---|---| | `--page` | `1` | Số trang | | `--page-size` | `20` | Số mục mỗi trang (tối đa: 100) | ## Apps \{#apps\} Quản lý các ứng dụng trong tài khoản Adapty của bạn. Để cấu hình qua Dashboard, xem [App settings](general). ### adapty apps list \{#adapty-apps-list\} Liệt kê tất cả ứng dụng trong tài khoản Adapty của bạn. ```bash adapty apps list ``` Chấp nhận [cờ phân trang](#global-flags). ### adapty apps get \{#adapty-apps-get\} Lấy thông tin chi tiết của một ứng dụng cụ thể. ```bash adapty apps get ``` | Đối số | Mô tả | |---|---| | `app-id` | App ID (UUID) | ### adapty apps create \{#adapty-apps-create\} Tạo ứng dụng mới. ```bash adapty apps create --title "My App" --platform ios --apple-bundle-id com.example.app ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--title` | Có | Tên ứng dụng | | `--platform` | Có | Nền tảng: `ios` hoặc `android`. Lặp lại để dùng cả hai: `--platform ios --platform android` | | `--apple-bundle-id` | Bắt buộc khi dùng `--platform ios` | Apple bundle ID | | `--google-bundle-id` | Bắt buộc khi dùng `--platform android` | Google bundle ID | ### adapty apps update \{#adapty-apps-update\} Cập nhật ứng dụng hiện có. ```bash adapty apps update --title "New Name" ``` | Đối số | Mô tả | |---|---| | `app-id` | App ID (UUID) | | Cờ | Mô tả | |---|---| | `--title` | Tên ứng dụng mới | | `--apple-bundle-id` | Apple bundle ID mới | | `--google-bundle-id` | Google bundle ID mới | Cần có ít nhất một cờ. `--platform` không thể thay đổi sau khi tạo. ## Mức độ truy cập \{#access-levels\} ### adapty access-levels list \{#adapty-access-levels-list\} Liệt kê tất cả [mức độ truy cập](access-level) của một ứng dụng. ```bash adapty access-levels list --app ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Chấp nhận [cờ phân trang](#global-flags). ### adapty access-levels get \{#adapty-access-levels-get\} Lấy thông tin chi tiết của một [mức độ truy cập](access-level) cụ thể. ```bash adapty access-levels get --app ``` | Đối số | Mô tả | |---|---| | `access-level-id` | Access level ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | ### adapty access-levels create \{#adapty-access-levels-create\} Tạo [mức độ truy cập](access-level) mới. ```bash adapty access-levels create --app --sdk-id "pro" --title "Pro" ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--sdk-id` | Có | Định danh dùng trong code ứng dụng để kiểm tra quyền truy cập (ví dụ: `"pro"` hoặc `"premium"`) | | `--title` | Có | Nhãn hiển thị trên Adapty dashboard | ### adapty access-levels update \{#adapty-access-levels-update\} Cập nhật [mức độ truy cập](access-level) hiện có. ```bash adapty access-levels update --app --title "Pro Access" ``` | Đối số | Mô tả | |---|---| | `access-level-id` | Access level ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--title` | Có | Nhãn hiển thị mới | `--sdk-id` không thể thay đổi sau khi tạo. ## Sản phẩm \{#products\} ### adapty products list \{#adapty-products-list\} Liệt kê tất cả [sản phẩm](product) của một ứng dụng. ```bash adapty products list --app ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Chấp nhận [cờ phân trang](#global-flags). ### adapty products get \{#adapty-products-get\} Lấy thông tin chi tiết của một [sản phẩm](product) cụ thể. ```bash adapty products get --app ``` | Đối số | Mô tả | |---|---| | `product-id` | Product ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | ### adapty products create \{#adapty-products-create\} Tạo [sản phẩm](product) mới. :::important Các ID sản phẩm trong cửa hàng (`--ios-product-id`, `--android-product-id`, `--android-base-plan-id`) không thể thay đổi sau khi tạo. Để dùng ID sản phẩm khác trong cửa hàng, hãy tạo sản phẩm mới. ::: ```bash adapty products create --app --title "Monthly" --access-level-id --period monthly --ios-product-id com.example.monthly ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--title` | Có | Tên sản phẩm | | `--access-level-id` | Có | ID [mức độ truy cập](access-level) (UUID) mà sản phẩm này mở khóa | | `--period` | Có | Chu kỳ gói đăng ký: `weekly`, `monthly`, `2_months`, `3_months`, `6_months`, `yearly`, `lifetime` | | `--ios-product-id` | Cần ít nhất một nền tảng | Product ID từ App Store Connect | | `--android-product-id` | Cần ít nhất một nền tảng | Product ID từ Google Play Console | | `--android-base-plan-id` | Bắt buộc khi dùng `--android-product-id` trừ khi `--period lifetime` | Base plan ID từ Google Play Console | ### adapty products update \{#adapty-products-update\} Cập nhật [sản phẩm](product) hiện có. Các ID sản phẩm trong cửa hàng (`--ios-product-id`, `--android-product-id`) không thể thay đổi sau khi tạo và không có trong lệnh này. Để dùng ID sản phẩm khác trong cửa hàng, hãy tạo sản phẩm mới. ```bash adapty products update --app --title "Monthly" --access-level-id ``` | Đối số | Mô tả | |---|---| | `product-id` | Product ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--title` | Không | Tên sản phẩm | | `--access-level-id` | Không | ID [mức độ truy cập](access-level) (UUID) mà sản phẩm này mở khóa | ## Paywall \{#paywalls\} ### adapty paywalls list \{#adapty-paywalls-list\} Liệt kê tất cả [paywall](paywalls) của một ứng dụng. ```bash adapty paywalls list --app ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Chấp nhận [cờ phân trang](#global-flags). ### adapty paywalls get \{#adapty-paywalls-get\} Lấy thông tin chi tiết của một [paywall](paywalls) cụ thể. ```bash adapty paywalls get --app ``` | Đối số | Mô tả | |---|---| | `paywall-id` | Paywall ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | ### adapty paywalls create \{#adapty-paywalls-create\} Tạo [paywall](paywalls) mới. ```bash adapty paywalls create --app --title "Default Paywall" --product-id ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--title` | Có | Tên paywall | | `--product-id` | Có | ID [sản phẩm](product) (UUID). Lặp lại cho nhiều sản phẩm: `--product-id --product-id ` | ### adapty paywalls update \{#adapty-paywalls-update\} Thay thế toàn bộ các trường của một [paywall](paywalls) hiện có. :::important Khi paywall đã được liên kết với một placement, không thể thay đổi sản phẩm của nó. Để dùng sản phẩm khác trong paywall đang hoạt động, hãy tạo paywall mới và cập nhật placement để trỏ đến paywall đó. ::: ```bash adapty paywalls update --app --title "Default Paywall" --product-id ``` Lệnh này thay thế toàn bộ các trường của paywall, bao gồm cả danh sách sản phẩm đầy đủ. | Đối số | Mô tả | |---|---| | `paywall-id` | Paywall ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--title` | Có | Tên paywall | | `--product-id` | Có | ID [sản phẩm](product) (UUID). Lặp lại cho nhiều sản phẩm: `--product-id --product-id ` | ### adapty paywalls placements \{#adapty-paywalls-placements\} Liệt kê tất cả [placement](placements) đang dùng một [paywall](paywalls) nhất định. ```bash adapty paywalls placements --app ``` | Đối số | Mô tả | |---|---| | `paywall-id` | Paywall ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Dùng lệnh này trước khi thay đổi paywall để xem các placement nào sẽ bị ảnh hưởng. ## Placement \{#placements\} ### adapty placements list \{#adapty-placements-list\} Liệt kê tất cả [placement](placements) của một ứng dụng. ```bash adapty placements list --app ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Chấp nhận [cờ phân trang](#global-flags). ### adapty placements get \{#adapty-placements-get\} Lấy thông tin chi tiết của một [placement](placements) cụ thể. ```bash adapty placements get --app ``` | Đối số | Mô tả | |---|---| | `placement-id` | Placement ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Kết quả trả về chứa mảng `audiences`. Mỗi phần tử có dạng `{segment_ids, paywall_id, priority}`. Đối tượng mặc định có `segment_ids: []` và giá trị ưu tiên cao nhất (được đánh giá cuối cùng). Đầu ra dạng văn bản cũng hiển thị `Paywall ID` ở cấp cao nhất, lấy từ đối tượng mặc định để tiện tham khảo. `--json` trả về dữ liệu API thô không thay đổi. ### adapty placements create \{#adapty-placements-create\} Tạo [placement](placements) mới. ```bash adapty placements create --app --title "Main" --developer-id "main" --audiences '[{"segment_ids":[],"paywall_id":"","priority":0}]' ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--title` | Có | Tên placement | | `--developer-id` | Có | Định danh chuỗi dùng trong code ứng dụng để yêu cầu [placement](placements) này | | `--audiences` | Một trong hai | Mảng JSON gồm các phần tử `{segment_ids, paywall_id, priority}`. Xem [Cấu trúc Audiences](#audiences-shape) | | `--paywall-id` | Một trong hai | **Không còn dùng.** ID [paywall](paywalls) (UUID). Được phía client chuyển đổi thành một đối tượng mặc định | Truyền đúng một trong `--audiences` hoặc `--paywall-id`. Truyền cả hai hoặc không truyền cái nào sẽ báo lỗi. :::warning `--paywall-id` không còn được khuyến nghị và sẽ bị xóa. Khi truyền vào, CLI sẽ in cảnh báo ra stderr và chuyển đổi giá trị thành một đối tượng mặc định. Hãy dùng `--audiences` cho các tự động hóa mới. ::: ### adapty placements update \{#adapty-placements-update\} Thay thế toàn bộ các trường của một [placement](placements) hiện có. ```bash adapty placements update --app --title "Main" --developer-id "main" --audiences '[{"segment_ids":[],"paywall_id":"","priority":0}]' ``` Lệnh này thay thế toàn bộ các trường của placement, bao gồm cả danh sách audiences đầy đủ. | Đối số | Mô tả | |---|---| | `placement-id` | Placement ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | | `--title` | Có | Tên placement | | `--developer-id` | Có | Định danh chuỗi dùng trong code ứng dụng để yêu cầu [placement](placements) này | | `--audiences` | Một trong hai | Mảng JSON gồm các phần tử `{segment_ids, paywall_id, priority}`. Xem [Cấu trúc Audiences](#audiences-shape) | | `--paywall-id` | Một trong hai | **Không còn dùng.** ID [paywall](paywalls) (UUID). Thay thế toàn bộ audiences bằng một đối tượng mặc định | :::warning Truyền `--paywall-id` sẽ ghi đè toàn bộ audiences của placement. Các audiences theo phân khúc sẽ bị xóa. Để giữ lại chúng, hãy dùng `--audiences` và thêm đầy đủ các phần tử bạn muốn giữ. ::: #### Cấu trúc Audiences \{#audiences-shape\} Cờ `--audiences` nhận một mảng JSON. Mỗi phần tử có các trường sau: | Trường | Kiểu | Mô tả | |---|---|---| | `segment_ids` | `string[]` | Các ID [phân khúc](segments) được nhắm mục tiêu bởi đối tượng này. Độ dài 0 hoặc 1. Mảng rỗng đánh dấu **đối tượng mặc định** — dự phòng cho người dùng không khớp với phân khúc nào khác | | `paywall_id` | `string` | ID [paywall](paywalls) (UUID) hiển thị cho người dùng trong đối tượng này | | `priority` | `number` | Bắt đầu từ 0, không trùng nhau trong cùng placement. Audiences được đánh giá từ thấp đến cao; đối tượng mặc định phải có giá trị cao nhất | Một placement phải có đúng một đối tượng mặc định. Ví dụ với một đối tượng có mục tiêu và một đối tượng mặc định: ```bash adapty placements update --app --title "Main" --developer-id "main" \ --audiences '[{"segment_ids":[""],"paywall_id":"","priority":0},{"segment_ids":[],"paywall_id":"","priority":1}]' ``` Để thay đổi paywall trên nhiều placement mà không mất cấu hình định tuyến theo phân khúc: 1. Tìm các placement bị ảnh hưởng: ```bash adapty paywalls placements --app ``` 2. Với mỗi placement, đọc toàn bộ mảng `audiences`: ```bash adapty placements get --app --json ``` 3. Thay thế các giá trị `paywall_id` tương ứng ở phía client. 4. Ghi lại payload đã chỉnh sửa: ```bash adapty placements update --app --title "" --developer-id "<developer-id>" --audiences '<modified-payload>' ``` ## Phân khúc \{#segments\} [Phân khúc](segments) chỉ có thể đọc qua CLI. Tạo và chỉnh sửa chúng trên [Adapty dashboard](https://app.adapty.io). Dùng các lệnh này để tra cứu ID phân khúc khi soạn audiences cho placement. ### adapty segments list \{#adapty-segments-list\} Liệt kê tất cả [phân khúc](segments) của một ứng dụng. ```bash adapty segments list --app <app-id> ``` | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Chấp nhận [cờ phân trang](#global-flags). ### adapty segments get \{#adapty-segments-get\} Lấy thông tin chi tiết của một [phân khúc](segments) cụ thể. ```bash adapty segments get --app <app-id> <segment-id> ``` | Đối số | Mô tả | |---|---| | `segment-id` | Segment ID (UUID) | | Cờ | Bắt buộc | Mô tả | |---|---|---| | `--app` | Có | App ID (UUID) | Kết quả trả về chứa `id`, `title` và `description`. Các quy tắc lọc không được hiển thị qua API này. ## Xác thực \{#auth\} | Lệnh | Mô tả | |---|---| | `adapty auth login` | Xác thực qua trình duyệt bằng device flow | | `adapty auth logout` | Xóa thông tin đăng nhập đã lưu ở máy cục bộ | | `adapty auth whoami` | Xác minh token với máy chủ và hiển thị thông tin người dùng | | `adapty auth status` | Hiển thị trạng thái xác thực cục bộ mà không gọi máy chủ | | `adapty auth revoke` | Thu hồi token ở phía máy chủ và xóa ở máy cục bộ | Xem [Xác thực](developer-cli-authentication) để biết chi tiết từng lệnh. --- # File: getting-started-with-server-side-api --- --- title: "Server-side API" description: "Bắt đầu với server-side API của Adapty để quản lý gói đăng ký." --- :::tip Đang dùng AI coding agent? Xem [Kiểm tra và cấp quyền truy cập gói đăng ký từ backend của bạn](server-side-api-with-ai) để có hướng dẫn đầy đủ trên một trang. ::: Với API, bạn có thể: 1. Kiểm tra trạng thái gói đăng ký của người dùng. 2. Kích hoạt gói đăng ký của người dùng với một mức độ truy cập. 3. Lấy thuộc tính người dùng. 4. Đặt thuộc tính người dùng. 5. Lấy và cập nhật cấu hình paywall. <img src="/assets/shared/img/server.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <p> </p> :::note Để theo dõi các sự kiện gói đăng ký, hãy dùng tích hợp [Webhook](webhook) trong Adapty hoặc tích hợp trực tiếp với dịch vụ hiện có của bạn. ::: ## Trường hợp 1: Đồng bộ người dùng đăng ký giữa web và mobile \{#case-1-sync-subscribers-between-web-and-mobile\} Nếu bạn dùng các nhà cung cấp thanh toán web như Stripe, ChargeBee hoặc các dịch vụ khác, bạn có thể dễ dàng đồng bộ người dùng đăng ký. Cách thực hiện: 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. [Kiểm tra trạng thái gói đăng ký của họ](api-adapty/operations/getProfile) bằng API. 3. Nếu người dùng đang dùng gói freemium, hiển thị paywall trên trang web của bạn. 4. Sau khi thanh toán thành công, [cập nhật trạng thái gói đăng ký](api-adapty/operations/setTransaction) trong Adapty qua API. 5. Người dùng đăng ký của bạn sẽ tự động được đồng bộ với ứng dụng mobile. ## Trường hợp 2: Cấp gói đăng ký \{#case-2-grant-a-subscription\} :::note Vì lý do bảo mật, bạn không thể cấp gói đăng ký qua SDK. ::: Nếu bạn bán hàng qua cửa hàng trực tuyến của mình, Amazon Appstore, Microsoft Store, hoặc bất kỳ nền tảng nào khác ngoài Google Play và App Store, bạn cần đồng bộ các giao dịch đó với Adapty để cung cấp quyền truy cập và theo dõi giao dịch trong analytics. 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. [Thiết lập một cửa hàng tùy chỉnh cho sản phẩm của bạn trong Adapty Dashboard](custom-store). 3. Đồng bộ giao dịch với Adapty bằng yêu cầu API [Set transaction](api-adapty/operations/setTransaction). ## Trường hợp 3: Cấp mức độ truy cập \{#case-3-grant-an-access-level\} Giả sử bạn đang chạy chương trình khuyến mãi với bản dùng thử miễn phí 7 ngày và muốn trải nghiệm đồng nhất trên tất cả nền tảng. Để đồng bộ điều này với ứng dụng mobile: 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. Dùng API để [cấp quyền truy cập premium](api-adapty/operations/grantAccessLevel) trong 7 ngày. Sau 7 ngày, người dùng không đăng ký sẽ bị hạ xuống gói miễn phí. ## Trường hợp 4: Đồng bộ thuộc tính và custom attributes của người dùng \{#case-4-sync-users-properties-and-custom-attributes\} Nếu bạn có custom attributes cho người dùng — chẳng hạn như số từ đã học trong ứng dụng học ngoại ngữ — bạn cũng có thể đồng bộ chúng. 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. [Cập nhật thuộc tính](api-adapty/operations/updateProfile) qua API hoặc SDK. Các custom attributes này có thể được dùng để tạo phân khúc và chạy A/B test. ## Trường hợp 5: Quản lý cấu hình paywall \{#case-5-manage-paywall-configurations\} Bạn có thể [cập nhật Remote Config trong paywall](api-adapty/operations/updatePaywall) để điều chỉnh giao diện và hành vi paywall một cách linh hoạt mà không cần triển khai lại ứng dụng. --- **Tiếp theo:** - Tiến hành [xác thực cho server-side API](ss-authorization) - Các yêu cầu: - [Lấy hồ sơ người dùng](api-adapty/operations/getProfile) - [Tạo hồ sơ người dùng](api-adapty/operations/createProfile) - [Cập nhật hồ sơ người dùng](api-adapty/operations/updateProfile) - [Xóa hồ sơ người dùng](api-adapty/operations/deleteProfile) - [Cấp mức độ truy cập](api-adapty/operations/grantAccessLevel) - [Thu hồi mức độ truy cập](api-adapty/operations/revokeAccessLevel) - [Đặt giao dịch](api-adapty/operations/setTransaction) - [Xác thực giao dịch, cấp mức độ truy cập cho khách hàng và nhập lịch sử giao dịch của họ](api-adapty/operations/validateStripePurchase) - [Thêm định danh tích hợp](api-adapty/operations/setIntegrationIdentifiers) - [Lấy paywall](api-adapty/operations/getPaywall) - [Liệt kê các paywall](api-adapty/operations/listPaywalls) - [Cập nhật paywall](api-adapty/operations/updatePaywall) --- # File: ss-authorization --- --- title: "Xác thực và định dạng yêu cầu API phía máy chủ" description: "" --- ## Xác thực \{#authorization\} Các yêu cầu API phải được xác thực bằng secret key hoặc public API key của bạn thông qua header Authorization. Bạn có thể tìm thấy chúng trong [**App Settings**](https://app.adapty.io/settings/general). Định dạng giá trị là `Api-Key {your-secret-api-key}`, ví dụ: `Api-Key secret_live_...`. :::important API key là theo từng ứng dụng. Nếu bạn có nhiều ứng dụng, hãy đảm bảo sử dụng các key khác nhau cho mỗi ứng dụng. ::: ## Định dạng yêu cầu \{#request-format\} **Headers** Các yêu cầu API phía máy chủ yêu cầu các header cụ thể và phần thân JSON. Sử dụng thông tin bên dưới để cấu trúc yêu cầu của bạn. | **Header** | **Mô tả** | | --------------------------- | ------------------------------------------------------------ | | **adapty-profile-id** | <p>ID hồ sơ người dùng Adapty của người dùng. Hiển thị trong trường **Adapty ID** trên trang [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> trang hồ sơ cụ thể. </p><p>Có thể dùng thay thế cho **adapty-customer-user-id**, dùng một trong hai đều được.</p> | | **adapty-customer-user-id** | <p>ID người dùng trong hệ thống của bạn. Hiển thị trong trường **Customer user ID** trên trang [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> trang hồ sơ cụ thể. </p><p>Có thể dùng thay thế cho **adapty-profile-id**, dùng một trong hai đều được.</p><p> ⚠️ Chỉ hoạt động nếu bạn <InlineTooltip tooltip="xác định người dùng trong ứng dụng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip> trong mã ứng dụng bằng Adapty SDK.</p> | | **adapty-platform** | (tùy chọn) Chỉ định nền tảng của thiết bị mà ứng dụng được cài đặt. Chúng tôi khuyến nghị đặt tham số này trong các yêu cầu [Create profile](api-adapty/operations/createProfile) và [Update profile](api-adapty/operations/updateProfile) khi chỉnh sửa đối tượng [Installation Meta](server-side-api-objects#installation-meta), vì nó phụ thuộc vào thiết bị người dùng đang dùng và một người dùng có thể có nhiều thiết bị. Các giá trị có thể có: `iOS`, `macOS`, `iPadOS`, `visionOS`, `Android`, hoặc `web`. | | **Content-Type** | Đặt thành `application/json` để API xử lý yêu cầu. | **Body** API yêu cầu phần thân có định dạng JSON với dữ liệu cần thiết cho yêu cầu. ## Giới hạn tốc độ \{#rate-limits\} Để tránh bị throttle, hãy đảm bảo số lượng yêu cầu (mỗi ứng dụng) không vượt quá 40.000 yêu cầu mỗi phút. Nếu vượt quá giới hạn này, hệ thống có thể bị chậm hoặc tạm thời chặn các yêu cầu tiếp theo nhằm duy trì hiệu suất tối ưu cho tất cả người dùng. ## Xoay vòng API key \{#rotate-api-keys\} Nếu bạn cần xoay vòng secret API key: 1. Trong **Settings → General**, nhấp vào **Generate new key**, sau đó nhấp vào biểu tượng thùng rác bên cạnh key cũ. 2. Cập nhật key đang dùng trong ứng dụng của bạn. --- **Tiếp theo — các yêu cầu:** - [Lấy hồ sơ](api-adapty/operations/getProfile) - [Tạo hồ sơ](api-adapty/operations/createProfile) - [Cập nhật hồ sơ](api-adapty/operations/updateProfile) - [Xóa hồ sơ](api-adapty/operations/deleteProfile) - [Cấp mức độ truy cập](api-adapty/operations/grantAccessLevel) - [Thu hồi mức độ truy cập](api-adapty/operations/revokeAccessLevel) - [Đặt giao dịch](api-adapty/operations/setTransaction) - [Xác thực mua hàng, cấp mức độ truy cập cho khách hàng và nhập lịch sử giao dịch của họ](api-adapty/operations/validateStripePurchase) - [Lấy paywall](api-adapty/operations/getPaywall) - [Liệt kê paywall](api-adapty/operations/listPaywalls) - [Cập nhật paywall](api-adapty/operations/updatePaywall) --- # File: server-side-api-specs --- --- title: "Yêu cầu API phía server" description: "Khám phá thông số kỹ thuật API phía server của Adapty để tích hợp nâng cao." --- API phía server của Adapty cho phép bạn truy cập và quản lý dữ liệu gói đăng ký theo cách lập trình, giúp tích hợp liền mạch với các dịch vụ và hạ tầng hiện có. Dù bạn đang đồng bộ dữ liệu giữa các nền tảng, cấp mức độ truy cập, hay xác thực giao dịch mua trong Stripe, API này cung cấp đầy đủ công cụ để giữ cho hệ thống của bạn luôn đồng bộ và người dùng luôn gắn kết. ## Bộ sưu tập và môi trường Postman \{#postman-collection-and-environment\} Để đơn giản hóa việc sử dụng API phía server, chúng tôi đã chuẩn bị một bộ sưu tập Postman và file môi trường mà bạn có thể tải về và nhập vào Postman. - **Bộ sưu tập Request**: Bao gồm tất cả các request có trong API phía server của Adapty. Lưu ý rằng bộ này sử dụng các biến mà bạn có thể định nghĩa trong môi trường. - **Môi trường**: Chứa danh sách các biến để bạn có thể định nghĩa giá trị một lần. Chúng tôi đã chuẩn bị một môi trường hợp nhất cho API phía server, web API và analytics export API để thuận tiện hơn cho bạn. Sau khi kích hoạt môi trường này, Postman sẽ tự động thay thế các giá trị biến đã định nghĩa vào các request của bạn. :::tip [Tải về bộ sưu tập và môi trường](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/Adapty_server_side_API_postman_collection.zip) ::: Để biết cách nhập bộ sưu tập và môi trường vào Postman, vui lòng tham khảo [tài liệu Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/). ### Các biến được sử dụng \{#variables-used\} Chúng tôi đã tạo một môi trường hợp nhất cho API phía server, web API và analytics export API để đơn giản hóa quy trình làm việc của bạn. Dưới đây là các biến dành riêng cho API phía server: | Biến | Mô tả | Giá trị ví dụ | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | | secret_api_key | Bạn có thể tìm thấy nó trong trường **Secret key** tại [**App settings**](https://app.adapty.io/settings/general). | `secret_live_Pj1P1xzM.2CvSvE1IalQRFjsWy6csBVNpH33atnod` | | adapty-customer-user-id | ID người dùng được sử dụng trong hệ thống của bạn. Trong Adapty Dashboard, bạn có thể tìm thấy nó trong trường **Customer user ID** của Profile. | `john.doe@example.com` | | adapty-profile-id | ID người dùng được gán trong Adapty. Trong Adapty Dashboard, bạn có thể tìm thấy nó trong trường **Adapty ID** của Profile. | `3286abd3-48b0-4e9c-a5f6-ac0a006333a6` | | Adapty-platform | Nền tảng người dùng sử dụng cho ứng dụng của bạn. Các giá trị có thể: `iOS`, `macOS`, `iPadOS`, `visionOS`, `Android`, `web`. | `iOS` | | stripe_token | Token của đối tượng Stripe đại diện cho một giao dịch mua duy nhất, chẳng hạn như Subscription (`sub_XXX`) hoặc Payment Intent (`pi_XXX`). | `sub_1JY8xLLy6P12345a` | **Tiếp theo: Các Request:** - [Lấy hồ sơ người dùng](api-adapty/operations/getProfile) - [Tạo hồ sơ người dùng](api-adapty/operations/createProfile) - [Cập nhật hồ sơ người dùng](api-adapty/operations/updateProfile) - [Xóa hồ sơ người dùng](api-adapty/operations/deleteProfile) - [Cấp mức độ truy cập](api-adapty/operations/grantAccessLevel) - [Thu hồi mức độ truy cập](api-adapty/operations/revokeAccessLevel) - [Đặt giao dịch](api-adapty/operations/setTransaction) - [Xác thực giao dịch mua, cung cấp mức độ truy cập cho khách hàng và nhập lịch sử giao dịch của họ](api-adapty/operations/validateStripePurchase) - [Thêm mã định danh tích hợp](api-adapty/operations/setIntegrationIdentifiers) - [Lấy paywall](api-adapty/operations/getPaywall) - [Liệt kê các paywall](api-adapty/operations/listPaywalls) - [Cập nhật paywall](api-adapty/operations/updatePaywall) --- # File: api-guides --- --- title: "Hướng dẫn API" description: "Tìm hiểu cách thực hiện các tác vụ cụ thể bằng server-side API." --- Trong phần này, bạn có thể tìm thấy các hướng dẫn bao gồm nhiều trường hợp sử dụng khác nhau, giúp bạn thực hiện các tác vụ cụ thể bằng server-side API và SDK Adapty. <CustomDocCardList /> --- # File: sync-subscribers-from-web --- --- title: "Đồng bộ hóa giao dịch mua giữa web và di động" description: "Đồng bộ người dùng đăng ký trên web và di động." --- Nếu người dùng có thể mua sản phẩm trên **website** của bạn, bạn có thể tự động đồng bộ mức độ truy cập của họ với **ứng dụng di động**. Hướng dẫn này sẽ giúp bạn thực hiện điều đó bằng Adapty API và SDK. #### Ví dụ thực tế Giả sử trong ứng dụng của bạn, người dùng có thể đăng ký gói freemium trên cả di động lẫn web. Bạn cho phép họ nâng cấp lên gói Premium trên website thông qua Stripe hoặc Chargebee. Khi người dùng đăng ký trên web, bạn muốn họ ngay lập tức có quyền truy cập Premium trong ứng dụng di động — không cần chờ đợi hay đăng nhập lại. Đó chính là điều Adapty giúp bạn tự động hóa. ## Bước 1. Xác định người dùng \{#step-1-identify-users\} Adapty sử dụng `customer_user_id` để nhận dạng người dùng trên các nền tảng. Bạn chỉ cần tạo ID này một lần và truyền nó cho cả SDK di động lẫn backend web. ### Đăng ký từ web \{#sign-up-from-web\} Khi người dùng đăng ký trên website của bạn, hãy tạo hồ sơ người dùng cho họ trong Adapty thông qua server-side API. Xem tài liệu tham khảo về phương thức [tại đây](api-adapty/operations/createProfile). ```bash curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/profile/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: YOUR_CUSTOMER_USER_ID' ``` ### Đăng ký từ ứng dụng \{#sign-up-from-app\} Khi người dùng đăng ký lần đầu từ ứng dụng, bạn có thể truyền customer user ID của họ trong lúc kích hoạt SDK. Hoặc nếu bạn đã kích hoạt Adapty SDK trước giai đoạn đăng ký, hãy dùng phương thức `identify` để tạo hồ sơ người dùng mới và gán cho nó một customer user ID. :::important Nếu bạn xác định người dùng mới sau khi kích hoạt SDK, đầu tiên SDK sẽ tạo một hồ sơ người dùng ẩn danh vì nó không thể hoạt động mà không có hồ sơ nào. Tiếp theo, khi bạn xác định người dùng và gán cho họ một customer user ID mới, một hồ sơ người dùng mới sẽ được tạo ra. Đây là hành vi hoàn toàn bình thường và sẽ không ảnh hưởng đến độ chính xác của analytics. Đọc thêm [tại đây](ios-quickstart-identify). ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="iOS (Swift-Callback)" default> ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` </TabItem> <TabItem value="android" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` </TabItem> <TabItem value="react-native" label="React Native" default> ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // successfully identified } catch (error) { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </TabItem> <TabItem value="unity" label="Unity" default> ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user if(error == null) { // successful identify } }); ``` </TabItem> <TabItem value="kmp" label="Kotlin Multiplatform" default> ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` </TabItem> <TabItem value="capacitor" label="Capacitor" default> ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` </TabItem> </Tabs> ## Bước 2. Kiểm tra trạng thái gói đăng ký qua API \{#step-2-check-subscription-status-via-api\} Khi người dùng đăng nhập trên website của bạn, hãy lấy hồ sơ người dùng Adapty của họ thông qua API. Nếu người dùng chưa có gói đăng ký đang hoạt động, bạn có thể hiển thị paywall. Xem tài liệu tham khảo về phương thức [tại đây](api-adapty/operations/getProfile). ```bash curl --request GET \ --url https://api.adapty.io/api/v2/server-side-api/profile/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'adapty-customer-user-id: YOUR_USER_ID' \ ``` ## Bước 3. Hiển thị paywall trên website \{#step-3-display-a-paywall-on-your-website\} Trên website của bạn, hiển thị paywall cho người dùng freemium. Bạn có thể sử dụng bất kỳ nhà cung cấp thanh toán nào (Stripe, Chargebee, LemonSqueezy, v.v.). ## Bước 4. Cập nhật trạng thái gói đăng ký trong Adapty \{#step-4-update-subscription-status-in-adapty\} Sau khi thanh toán hoàn tất trên website, gọi Adapty API để cập nhật mức độ truy cập của người dùng theo sản phẩm họ đã mua. Xem tài liệu tham khảo về phương thức [tại đây](api-adapty/operations/grantAccessLevel). ```bash curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/purchase/profile/grant/access-level/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: YOUR_USER_ID' \ --data '{ "access_level_id": "YOUR_ACCESS_LEVEL" }' ``` ## Bước 5. Đồng bộ trạng thái trong ứng dụng \{#step-5-sync-status-in-the-app\} Khi người dùng mở ứng dụng di động của bạn, hãy lấy hồ sơ người dùng đã được cập nhật và mở khóa các tính năng trả phí. Bạn cần lấy hồ sơ người dùng của họ hoặc để nó tự động đồng bộ, sau đó lấy mức độ truy cập từ đó. Dưới đây là cách lấy hồ sơ người dùng và kiểm tra trạng thái. Để biết thêm chi tiết, hãy xem [tại đây](ios-check-subscription-status). <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="iOS (Swift-Callback)" default> ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` </TabItem> <TabItem value="android" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success<AdaptyProfile>) result).getValue(); // check the access if (profile.getAccessLevels().get("YOUR_ACCESS_LEVEL") != null && profile.getAccessLevels().get("YOUR_ACCESS_LEVEL").getIsActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` </TabItem> <TabItem value="react-native" label="React Native" default> ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </TabItem> <TabItem value="unity" label="Unity" default> ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access if (profile.AccessLevels["YOUR_ACCESS_LEVEL"]?.IsActive ?? false) { // grant access to premium features } }); ``` </TabItem> <TabItem value="kmp" label="Kotlin Multiplatform" default> ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } .onError { error -> // handle the error } ``` </TabItem> <TabItem value="capacitor" label="Capacitor" default> ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` </TabItem> </Tabs> --- # File: sync-purchases-from-custom-stores --- --- title: "Đồng bộ giao dịch từ cửa hàng tùy chỉnh" description: "Đồng bộ giao dịch từ cửa hàng tùy chỉnh với Adapty để cấp quyền truy cập và theo dõi doanh thu." --- Nếu bạn đang bán gói đăng ký hoặc in-app purchase qua **cửa hàng tùy chỉnh** như Amazon Appstore, Microsoft Store, hoặc nền tảng thanh toán của riêng bạn, bạn có thể đồng bộ các giao dịch đó với Adapty để tự động quản lý mức độ truy cập và theo dõi doanh thu trong phần analytics. Hướng dẫn này sẽ chỉ cho bạn cách kết nối giao dịch từ cửa hàng tùy chỉnh với Adapty thông qua SDK và API. #### Ví dụ thực tế Giả sử bạn đang phân phối ứng dụng trên Amazon Appstore, hoặc bạn đã xây dựng web store riêng cho các giao dịch trực tiếp. Khi người dùng hoàn tất một giao dịch mua trên các nền tảng này, bạn muốn: - Tự động cấp quyền truy cập vào các tính năng premium trong ứng dụng di động - Theo dõi giao dịch trong Adapty analytics cùng với doanh thu từ App Store và Google Play - Kích hoạt các tích hợp và webhook giống như bất kỳ gói đăng ký nào khác Đây chính là điều mà tích hợp này giúp bạn thực hiện. ## Bước 1. Xác định người dùng \{#step-1-identify-users\} Adapty sử dụng `customer_user_id` để xác định người dùng trên các nền tảng. Bạn cần tạo ID này một lần và truyền nó cho cả SDK di động và web backend. Khi người dùng đăng ký lần đầu từ ứng dụng, bạn có thể truyền customer user ID trong quá trình kích hoạt SDK. Hoặc nếu bạn đã kích hoạt Adapty SDK trước bước đăng ký, hãy dùng phương thức `identify` để tạo hồ sơ người dùng mới và gán customer user ID cho nó. :::important Nếu bạn xác định người dùng mới sau khi kích hoạt SDK, SDK sẽ tạo một hồ sơ người dùng ẩn danh trước (nó không thể hoạt động nếu không có hồ sơ). Khi bạn gọi `identify` với customer user ID, một hồ sơ người dùng mới sẽ được tạo. Đây là hành vi bình thường và không ảnh hưởng đến độ chính xác của analytics. Đọc thêm [tại đây](ios-quickstart-identify). ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="iOS (Swift-Callback)" default> ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` </TabItem> <TabItem value="android" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` </TabItem> <TabItem value="react-native" label="React Native" default> ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // successfully identified } catch (error) { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </TabItem> <TabItem value="unity" label="Unity" default> ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user if(error == null) { // successful identify } }); ``` </TabItem> <TabItem value="kmp" label="Kotlin Multiplatform" default> ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` </TabItem> <TabItem value="capacitor" label="Capacitor" default> ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` </TabItem> </Tabs> ## Bước 2. Tạo sản phẩm trong cửa hàng tùy chỉnh trên Adapty Dashboard \{#step-2-create-products-in-a-custom-store-in-adapty-dashboard\} Để Adapty khớp các giao dịch từ cửa hàng tùy chỉnh với sản phẩm của bạn, bạn cần thêm sản phẩm và thiết lập thông tin cửa hàng tùy chỉnh cho chúng. 1. Vào [**Products**](https://app.adapty.io/settings/general) từ menu bên trái trong Adapty Dashboard và nhấp **Create product**. Hoặc nhấp vào sản phẩm đã có để chỉnh sửa. 2. Đảm bảo bạn đã chọn [mức độ truy cập](access-level) mà bạn muốn cấp cho người dùng mua sản phẩm. 3. Nhấp **+** và chọn **Add a custom store**. 4. Nhấp **Create new custom store**. 5. Đặt tên và ID cho cửa hàng của bạn (ví dụ: "Amazon Appstore", "Microsoft Store", hoặc "Web Store"). Nhấp **Create custom store**. 6. Sau đó, nhấp **Save changes** để liên kết sản phẩm với cửa hàng tùy chỉnh. 7. Nhập **Store product ID** cho sản phẩm để ánh xạ nó với một sản phẩm trong cửa hàng đó. Sau đó, nhấp **Save**. ## Bước 3. Đồng bộ giao dịch qua API \{#step-3-sync-transactions-via-api\} Khi một giao dịch mua hoàn tất trong cửa hàng tùy chỉnh của bạn, bạn cần đồng bộ nó với Adapty thông qua server-side API. Lệnh gọi API này sẽ: - Ghi lại giao dịch trong Adapty - Cấp mức độ truy cập tương ứng cho người dùng - Kích hoạt các tích hợp và webhook bạn đã cấu hình - Hiển thị giao dịch trong analytics của bạn Xem tài liệu tham khảo đầy đủ về phương thức [tại đây](api-adapty/operations/setTransaction). ```bash curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: YOUR_CUSTOMER_USER_ID' \ --data '{ "purchase_type": "PRODUCT_PERIOD", "store": "YOUR_CUSTOM_STORE", "environment": "production", "store_product_id": "YOUR_STORE_PRODUCT_ID", "store_transaction_id": "STORE_TRANSACTION_ID", "store_original_transaction_id": "ORIGINAL_TRANSACTION_ID", "price": { "country": "COUNTRY_CODE", "currency": "CURRENCY_CODE", "value": "YOUR_PRICE" }, "purchased_at": "2024-01-15T10:30:00Z" }' ``` :::important Các tham số quan trọng: - **store**: ID cửa hàng tùy chỉnh của bạn từ Bước 2 - **store_product_id**: Store product ID từ Bước 2 - **store_transaction_id**: Mã định danh duy nhất cho giao dịch này - **purchased_at**: Timestamp theo định dạng ISO 8601 khi giao dịch mua xảy ra - **price**: Số tiền người dùng đã thanh toán ::: ## Bước 4. Xác minh quyền truy cập trong ứng dụng \{#step-4-verify-access-in-the-app\} Sau khi giao dịch được đồng bộ, hồ sơ người dùng sẽ tự động được cập nhật với mức độ truy cập mới. Khi người dùng mở ứng dụng di động, hãy lấy hồ sơ của họ để kiểm tra trạng thái gói đăng ký và mở khóa các tính năng premium. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="iOS (Swift-Callback)" default> ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` </TabItem> <TabItem value="android" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success<AdaptyProfile>) result).getValue(); // check the access if (profile.getAccessLevels().get("YOUR_ACCESS_LEVEL") != null && profile.getAccessLevels().get("YOUR_ACCESS_LEVEL").getIsActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` </TabItem> <TabItem value="react-native" label="React Native" default> ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </TabItem> <TabItem value="unity" label="Unity" default> ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access if (profile.AccessLevels["YOUR_ACCESS_LEVEL"]?.IsActive ?? false) { // grant access to premium features } }); ``` </TabItem> <TabItem value="kmp" label="Kotlin Multiplatform" default> ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } .onError { error -> // handle the error } ``` </TabItem> <TabItem value="capacitor" label="Capacitor" default> ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` </TabItem> </Tabs> --- # File: grant-access-level --- --- title: "Cấp mức độ truy cập thủ công" description: "Mở khóa các tính năng trả phí thủ công cho người dùng hoặc nhóm người dùng cụ thể" --- Nếu bạn cần **mở khóa tính năng premium thủ công** cho người dùng hoặc nhóm người dùng cụ thể, bạn có thể thực hiện điều này thông qua Adapty API. Cách này hữu ích cho các chiến dịch khuyến mãi, cấp quyền truy cập cho nhà đầu tư, hoặc các trường hợp hỗ trợ khách hàng đặc biệt. Trong hướng dẫn này, bạn sẽ tìm hiểu cách xác định người dùng và cấp mức độ truy cập cho họ theo cách lập trình. #### Một số trường hợp sử dụng tiêu biểu - **Mã khuyến mãi**: Khi người dùng nhập mã khuyến mãi hợp lệ trong ứng dụng, tự động cấp cho họ quyền truy cập vào các tính năng premium. - **Quyền truy cập cho nhà đầu tư/beta tester**: Cấp quyền truy cập premium cho nhà đầu tư hoặc beta tester bằng cách kiểm tra các thuộc tính tùy chỉnh của họ. :::note **Mã khuyến mãi Google Play**: Giao dịch mua được thực hiện bằng cách đổi mã khuyến mãi Google Play có thể không có `orderId`. Tính năng xác thực sản phẩm mua một lần (non-subscription) của Adapty yêu cầu phải có `orderId`, do đó các giao dịch đổi mã này không được xác thực hoặc cấp quyền tự động. Hãy cấp quyền truy cập thủ công theo các bước bên dưới — Server-Side API không phụ thuộc vào `orderId`. ::: ## Bước 1. Xác định người dùng \{#step-1-identify-users\} Adapty sử dụng `customer_user_id` để nhận diện người dùng trên các nền tảng và thiết bị khác nhau. Điều này rất quan trọng để đảm bảo người dùng giữ được quyền truy cập sau khi cài đặt lại ứng dụng hoặc đổi thiết bị. Bạn chỉ cần tạo ID này một lần. Khi người dùng đăng ký lần đầu từ ứng dụng, bạn có thể truyền customer user ID của họ trong quá trình kích hoạt SDK, hoặc dùng phương thức `identify` nếu SDK đã được kích hoạt trước khi đăng ký. :::important Nếu bạn xác định người dùng mới sau khi kích hoạt SDK, SDK sẽ tạo một hồ sơ người dùng ẩn danh trước (SDK không thể hoạt động mà không có hồ sơ). Khi bạn gọi `identify` với customer user ID, một hồ sơ người dùng mới sẽ được tạo. Hành vi này là bình thường và sẽ không ảnh hưởng đến độ chính xác của analytics. Đọc thêm [tại đây](ios-quickstart-identify). ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="iOS (Swift-Callback)" default> ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` </TabItem> <TabItem value="android" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` </TabItem> <TabItem value="react-native" label="React Native" default> ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // successfully identified } catch (error) { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </TabItem> <TabItem value="unity" label="Unity" default> ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user if(error == null) { // successful identify } }); ``` </TabItem> <TabItem value="kmp" label="Kotlin Multiplatform" default> ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` </TabItem> <TabItem value="capacitor" label="Capacitor" default> ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` </TabItem> </Tabs> ## Bước 2. Cấp mức độ truy cập qua API \{#step-2-grant-access-level-via-api\} Sau khi người dùng được xác định bằng `customer_user_id`, bạn có thể cấp mức độ truy cập cho họ thông qua server-side API. Lệnh gọi API này sẽ cấp mức độ truy cập cho người dùng, cho phép họ sử dụng các tính năng trả phí mà không cần thực sự thanh toán. Xem tài liệu tham chiếu đầy đủ về phương thức [tại đây](api-adapty/operations/grantAccessLevel). :::tip Bạn có thể kiểm soát quyền truy cập của người dùng bằng cách thêm thuộc tính tùy chỉnh (ví dụ: Beta tester hoặc Investor) trong Adapty dashboard. Khi ứng dụng khởi chạy, [kiểm tra thuộc tính này trong hồ sơ người dùng](subscription-status) để cấp quyền truy cập tự động. Để cập nhật quyền truy cập, chỉ cần thay đổi thuộc tính trong dashboard. ::: ```bash curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/purchase/profile/grant/access-level/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: CUSTOMER_USER_ID' \ --data '{ "access_level_id": "YOUR_ACCESS_LEVEL" }' ``` ## Bước 3. Xác minh quyền truy cập trong ứng dụng \{#step-3-verify-access-in-the-app\} Sau khi cấp quyền truy cập qua API, hồ sơ người dùng sẽ được cập nhật tự động. Lấy hồ sơ của họ để kiểm tra trạng thái gói đăng ký và mở khóa các tính năng premium. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="iOS (Swift-Callback)" default> ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access if profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive ?? false { // grant access to premium features } } } ``` </TabItem> <TabItem value="android" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success<AdaptyProfile>) result).getValue(); // check the access if (profile.getAccessLevels().get("YOUR_ACCESS_LEVEL_ID") != null && profile.getAccessLevels().get("YOUR_ACCESS_LEVEL_ID").getIsActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` </TabItem> <TabItem value="react-native" label="React Native" default> ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </TabItem> <TabItem value="unity" label="Unity" default> ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access if (profile.AccessLevels["YOUR_ACCESS_LEVEL_ID"]?.IsActive ?? false) { // grant access to premium features } }); ``` </TabItem> <TabItem value="kmp" label="Kotlin Multiplatform" default> ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive == true) { // grant access to premium features } } .onError { error -> // handle the error } ``` </TabItem> <TabItem value="capacitor" label="Capacitor" default> ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` </TabItem> </Tabs> --- # File: web-api --- --- title: Adapty Web API description: "" --- Web API là phần mở rộng của server-side API, được thiết kế để sử dụng với các ứng dụng web. API này cho phép bạn lấy đúng paywall thông qua placement ID tương ứng và ghi lại lượt xem paywall để theo dõi chuyển đổi chính xác. Nhờ đó, bạn có thể tận dụng tính năng A/B test và cá nhân hóa paywall trong Adapty, đồng thời theo dõi paywall nào hoạt động hiệu quả nhất. ## Trường hợp sử dụng: Ghi lại giao dịch từ ứng dụng web và liên kết với paywall đã dùng \{#use-case-record-a-transaction-from-your-web-app-and-link-it-to-the-used-paywall\} Giả sử bạn bán sản phẩm trong ứng dụng web của mình. Bạn cần hiển thị paywall cho người dùng, cho phép họ mua sản phẩm, rồi thêm thông tin giao dịch vào Adapty. Điều quan trọng là phải liên kết các giao dịch này với paywall cụ thể mà người dùng đã thực hiện mua hàng, để analytics phản ánh dữ liệu chính xác. Việc này có thể thực hiện dễ dàng thông qua Adapty API. ### Điều kiện tiên quyết \{#prerequisites\} 1. [Tạo sản phẩm](create-product) mà bạn sẽ dùng trong paywall trên Adapty Dashboard. 2. [Tạo paywall](create-paywall) trên Adapty Dashboard. [Sử dụng Remote Config](customize-paywall-with-remote-config) để thiết kế paywall cho web. 3. [Thiết lập placement](create-placement) và liên kết paywall với nó trên Adapty Dashboard. ### Các bước thực hiện với Adapty API \{#steps-with-adapty-api\} 1. **Tạo hồ sơ người dùng:** Adapty cần có hồ sơ người dùng trước khi yêu cầu paywall để cá nhân hóa kết quả cho người dùng đó. Dùng request [Create profile](api-adapty/operations/createProfile) để tạo hồ sơ người dùng. 2. **Lấy và hiển thị paywall:** Khi người dùng đến placement trong ứng dụng web nơi paywall cần được hiển thị, dùng request [Get paywall](api-web/operations/getPaywall) để lấy paywall thông qua [placement ID](placements). Kết quả trả về sẽ là paywall dành cho [đối tượng](audience) tương ứng với người dùng của bạn. Hiển thị paywall bằng code của bạn, sử dụng các sản phẩm được trả về và (tùy chọn) [Remote Config](customize-paywall-with-remote-config) của paywall đó. 3. **Ghi lại lượt xem paywall:** Dùng [Record paywall view](api-web/operations/recordPaywallView) để ghi lại sự kiện xem paywall với Adapty, đảm bảo analytics phản ánh chính xác sự kiện này. Bước này rất quan trọng để theo dõi chuyển đổi đúng cách. 4. **Ghi lại giao dịch mua hàng:** Nếu người dùng hoàn tất mua hàng, hãy gửi thông tin giao dịch đến Adapty thông qua Adapty API. Đưa **variation ID** vào request này để liên kết giao dịch với paywall cụ thể đã được hiển thị. Để được hướng dẫn, hãy xem trang [liên kết paywall với giao dịch trong ứng dụng di động](report-transactions-observer-mode) — cách làm tương tự cũng áp dụng cho ứng dụng web. 5. **Thêm dữ liệu attribution marketing (nếu có):** Nếu bạn có dữ liệu attribution marketing (ví dụ: thông tin chiến dịch hoặc quảng cáo), dùng [Add attribution](api-web/operations/addAttribution) để hợp nhất vào hồ sơ người dùng, giúp làm phong phú thêm analytics và tìm hiểu hiệu quả quảng cáo của bạn trong Adapty. --- **Tiếp theo:** - Tiến hành [Ủy quyền Web API](web-api-authorization) - Các request: - [Add attribution](api-web/operations/addAttribution) - [Get paywall](api-web/operations/getPaywall) - [Record paywall view](api-web/operations/recordPaywallView) --- # File: web-api-authorization --- --- title: Định dạng ủy quyền và yêu cầu cho Web API description: "" --- ## Ủy quyền \{#authorization\} Các yêu cầu API phải được xác thực bằng public API key của bạn thông qua header **Authorization** với giá trị `Api-Key {your_public_api_key}`, ví dụ: `Api-Key public_live_...`. Tìm key này tại [Adapty Dashboard -> **App Settings** -> **General** tab -> **API keys** section](https://app.adapty.io/settings/general). :::important API key là riêng cho từng ứng dụng. Nếu bạn có nhiều ứng dụng, hãy đảm bảo sử dụng các key khác nhau cho mỗi ứng dụng. ::: ## Định dạng yêu cầu \{#request-format\} - **Header Content-Type**: Đặt header **Content-Type** thành `application/json` để API xử lý yêu cầu của bạn. - **Body**: API yêu cầu body của request phải ở định dạng JSON. --- # File: web-api-requests --- --- title: " Web API Requests" description: "" --- API phía máy chủ của Adapty cho phép bạn truy cập và quản lý dữ liệu gói đăng ký theo cách lập trình, giúp tích hợp liền mạch với các dịch vụ và hạ tầng hiện có. Dù bạn đang đồng bộ dữ liệu giữa các nền tảng, cấp mức độ truy cập, hay xác thực giao dịch mua trong Stripe, API này cung cấp đầy đủ công cụ để các hệ thống của bạn luôn đồng bộ và người dùng luôn gắn kết. ## Postman collection và environment \{#postman-collection-and-environment\} Để đơn giản hóa việc sử dụng web API, chúng tôi đã chuẩn bị một Postman collection và file environment mà bạn có thể tải về và nhập vào Postman. - **Request Collection**: Bao gồm tất cả các request có trong Adapty web API. Lưu ý rằng collection này sử dụng các biến mà bạn có thể định nghĩa trong environment. - **Environment**: Chứa danh sách các biến để bạn định nghĩa giá trị một lần duy nhất. Chúng tôi đã chuẩn bị một environment thống nhất cho server-side API, web API và analytics export API để tiện cho bạn. Sau khi kích hoạt environment này, Postman sẽ tự động thay thế các giá trị biến đã định nghĩa vào các request của bạn. :::tip [Tải collection và environment](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/Adapty_Web_API_postman_collection.zip) ::: Để biết cách nhập collection và environment vào Postman, vui lòng tham khảo [tài liệu Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/). ## Các biến được sử dụng \{#variables-used\} Chúng tôi đã tạo một environment thống nhất cho server-side API, web API và analytics export API để đơn giản hóa quy trình làm việc của bạn. Dưới đây là các biến dành riêng cho web API: | Biến | Mô tả | Giá trị ví dụ | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | | public_api_key | Bạn có thể tìm thấy trong trường **Public SDK key** tại [**App settings**](https://app.adapty.io/settings/general). | `public_live_Pj1P1xzM.2CvSvE1IalQRFjsWy6csBVNpH33atnod` | | adapty-customer-user-id | ID người dùng được sử dụng trong hệ thống của bạn. Trong Adapty Dashboard, bạn có thể tìm thấy trong trường **Customer user ID** của hồ sơ người dùng. | `john.doe@example.com` | | adapty-profile-id | ID người dùng được gán trong Adapty. Trong Adapty Dashboard, bạn có thể tìm thấy trong trường **Adapty ID** của hồ sơ người dùng. | `3286abd3-48b0-4e9c-a5f6-ac0a006333a6` | **Tiếp theo: Các request:** - [Lấy paywall](api-web/operations/getPaywall) - [Ghi nhận lượt xem paywall](api-web/operations/recordPaywallView) - [Thêm attribution](api-web/operations/addAttribution) --- # File: export-analytics-api --- --- title: Xuất dữ liệu analytics bằng API --- Xuất dữ liệu analytics sang CSV giúp bạn linh hoạt hơn trong việc phân tích sâu các chỉ số hiệu suất của ứng dụng, tùy chỉnh báo cáo và theo dõi xu hướng theo thời gian. Với Adapty API, bạn có thể dễ dàng kéo dữ liệu analytics chi tiết vào định dạng CSV, thuận tiện để theo dõi, chia sẻ và tinh chỉnh các insights từ dữ liệu khi cần. :::tip Đang dùng AI agent hoặc LLM để kéo dữ liệu analytics? Xem [Xuất dữ liệu analytics với AI agent](export-analytics-with-ai). ::: ## Bắt đầu với API để xuất dữ liệu analytics \{#getting-started-with-the-api-for-analytics-export\} Với API xuất analytics, bạn có thể thực hiện các việc như: 1. **Phân tích MRR từ các chiến dịch Marketing**: Đo lường tác động của các chiến dịch marketing năm ngoái tại một quốc gia cụ thể để xem chiến dịch nào mang lại doanh thu cao nhất, với theo dõi theo tuần. Sử dụng phương thức [Retrieve analytics data](api-export-analytics/operations/retrieveAnalyticsData) cho việc này. 2. **Theo dõi Retention theo Cohort theo thời gian**: Theo dõi retention theo cohort để phát hiện các điểm rời bỏ và so sánh các cohort theo thời gian, từ đó phát hiện xu hướng và những thời điểm quan trọng mà các chiến lược tương tác có thể cải thiện retention. Giới hạn theo một cửa hàng ứng dụng cụ thể, một quốc gia cụ thể và một sản phẩm cụ thể. Sử dụng phương thức [Retrieve cohort data](api-export-analytics/operations/retrieveCohortData) cho việc này. 3. **Đánh giá Tỷ lệ chuyển đổi theo các kênh**: Phân tích tỷ lệ chuyển đổi cho các kênh thu hút người dùng chính để xem kênh nào hiệu quả nhất trong việc thúc đẩy lần mua đầu tiên. Điều này giúp ưu tiên chi tiêu marketing vào các kênh có hiệu suất cao. Sử dụng phương thức [Retrieve conversion data](api-export-analytics/operations/retrieveConversionData) cho việc này. 4. **Xem lại Tỷ lệ churn**: Theo dõi tốc độ người dùng hủy đăng ký để phát hiện các mẫu churn hoặc đánh giá hiệu quả của các nỗ lực giữ chân, tập trung vào một quốc gia và một sản phẩm cụ thể. Sử dụng phương thức [Retrieve funnel data](api-export-analytics/operations/retrieveFunnelData) cho việc này. 5. **Đánh giá LTV theo phân khúc người dùng**: Xác định giá trị vòng đời của các phân khúc người dùng khác nhau để hiểu nhóm nào mang lại doanh thu cao nhất theo thời gian. Tập trung vào các phân khúc có giá trị cao như người đăng ký lâu dài, và sử dụng kết quả để tinh chỉnh chiến lược thu hút. Sử dụng phương thức [Retrieve LTV data](api-export-analytics/operations/retrieveLTVData) cho việc này. 6. **Kiểm tra Retention theo quốc gia**: Xem tỷ lệ retention theo khu vực để tìm các thị trường có mức độ tương tác cao và định hướng chiến lược bản địa hóa hoặc chiến lược theo khu vực. Sử dụng phương thức [Retrieve retention data](api-export-analytics/operations/retrieveRetentionData) cho việc này. --- **Tiếp theo**: - [Xác thực và định dạng request](export-analytics-api-authorization) - [Các request API xuất analytics](export-analytics-api-requests) --- # File: export-analytics-api-authorization --- --- title: Xác thực và định dạng yêu cầu cho API Xuất dữ liệu phân tích --- ## Xác thực \{#authorization\} Bạn cần xác thực các yêu cầu API bằng secret API key của mình thông qua header Authorization. Bạn có thể tìm thấy nó trong [App Settings](https://app.adapty.io/settings/general). Định dạng là `Api-Key {YOUR_SECRET_API_KEY}`, ví dụ: `Api-Key secret_live_...`. :::important API key gắn với từng ứng dụng. Nếu bạn có nhiều ứng dụng, hãy đảm bảo sử dụng các key khác nhau cho mỗi ứng dụng. ::: ## Định dạng yêu cầu \{#request-format\} **Headers** Các yêu cầu API phía server yêu cầu headers cụ thể và phần thân JSON. Sử dụng thông tin dưới đây để cấu trúc yêu cầu của bạn: | Header | Mô tả | | ------------ | ------------------------------------------------------------ | | Content-Type | (Bắt buộc) Đặt thành `application/json` để API xử lý yêu cầu. | | Adapty-Tz | (Tùy chọn) Đặt múi giờ để xác định cách dữ liệu được nhóm và hiển thị. Sử dụng [định dạng IANA Time Zone Database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (ví dụ: `Europe/Berlin`). | ## Phần thân \{#body\} API yêu cầu phần thân định dạng JSON với dữ liệu cần thiết cho yêu cầu. ## Giới hạn tốc độ \{#rate-limits\} Tối đa 2 yêu cầu mỗi giây cho mỗi API key. Vượt quá giới hạn này sẽ trả về lỗi `429 Too Many Requests`. ## Xoay vòng API key \{#rotate-api-keys\} Nếu bạn cần xoay vòng secret API key: 1. Trong **Settings → General**, nhấp vào **Generate new key**, sau đó nhấp vào biểu tượng thùng rác bên cạnh key cũ. 2. Cập nhật key đang dùng trong ứng dụng của bạn. --- **Tiếp theo: Các yêu cầu:** - [Truy xuất dữ liệu phân tích](api-export-analytics/operations/retrieveAnalyticsData) - [Truy xuất dữ liệu cohort](api-export-analytics/operations/retrieveCohortData) - [Truy xuất dữ liệu chuyển đổi](api-export-analytics/operations/retrieveConversionData) - [Truy xuất dữ liệu phễu](api-export-analytics/operations/retrieveFunnelData) - [Truy xuất dữ liệu Lifetime Value (LTV)](api-export-analytics/operations/retrieveLTVData) - [Truy xuất dữ liệu giữ chân người dùng](api-export-analytics/operations/retrieveRetentionData) --- # File: export-analytics-api-requests --- --- title: Xuất các yêu cầu API analytics --- Xuất dữ liệu analytics sang CSV giúp bạn linh hoạt hơn trong việc phân tích sâu các chỉ số hiệu suất của ứng dụng, tùy chỉnh báo cáo và theo dõi xu hướng theo thời gian. Với Adapty API, bạn có thể dễ dàng lấy dữ liệu analytics chi tiết dưới dạng CSV, thuận tiện để theo dõi, chia sẻ và tinh chỉnh các insight dữ liệu khi cần. ## Bộ sưu tập và môi trường Postman \{#postman-collection-and-environment\} Để đơn giản hóa việc sử dụng API xuất dữ liệu analytics, chúng tôi đã chuẩn bị một bộ sưu tập Postman và file môi trường mà bạn có thể tải xuống và nhập vào Postman. - **Bộ sưu tập request**: Bao gồm tất cả các request có trong API xuất analytics của Adapty. Lưu ý rằng nó sử dụng các biến mà bạn có thể định nghĩa trong môi trường. - **Môi trường**: Chứa danh sách các biến để bạn định nghĩa giá trị một lần. Chúng tôi đã chuẩn bị một môi trường thống nhất cho server-side API, web API và API xuất analytics để giúp mọi thứ thuận tiện hơn. Sau khi kích hoạt môi trường này, Postman sẽ tự động thay thế các giá trị biến đã định nghĩa vào các request của bạn. :::tip [Tải xuống bộ sưu tập và môi trường](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/Adapty_export_analytics_API_postman_collection.zip) ::: Để biết cách nhập bộ sưu tập và môi trường vào Postman, vui lòng tham khảo [tài liệu Postman](https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/). ### Các biến được sử dụng \{#variables-used\} Chúng tôi đã tạo một môi trường thống nhất cho server-side API, web API và API xuất analytics để đơn giản hóa quy trình làm việc của bạn. Dưới đây là các biến dành riêng cho API xuất analytics: | Biến | Mô tả | Giá trị ví dụ | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | | secret_api_key | Bạn có thể tìm thấy nó trong trường **Secret key** tại [**App settings**](https://app.adapty.io/settings/general). | `secret_live_Pj1P1xzM.2CvSvE1IalQRFjsWy6csBVNpH33atnod` | **Các request:** - [Lấy dữ liệu analytics](api-export-analytics/operations/retrieveAnalyticsData) - [Lấy dữ liệu cohort](api-export-analytics/operations/retrieveCohortData) - [Lấy dữ liệu chuyển đổi](api-export-analytics/operations/retrieveConversionData) - [Lấy dữ liệu funnel](api-export-analytics/operations/retrieveFunnelData) - [Lấy dữ liệu Lifetime Value (LTV)](api-export-analytics/operations/retrieveLTVData) - [Lấy dữ liệu retention](api-export-analytics/operations/retrieveRetentionData) --- # End of Documentation _Generated on: 2026-07-24T13:01:43.379Z_ _Successfully processed: 17/18 files_ # CAPACITOR - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.381Z Total files: 45 --- # File: capacitor-sdk-overview --- --- title: "Capacitor SDK overview" description: "Tìm hiểu về Adapty Capacitor SDK và các tính năng chính của nó." --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Capacitor.svg?style=flat&logo=capacitor)](https://github.com/adaptyteam/AdaptySDK-Capacitor/releases) Chào mừng! Chúng tôi ở đây để giúp in-app purchase trở nên đơn giản hơn bao giờ hết 🚀 Chúng tôi đã xây dựng [Adapty Capacitor SDK](https://github.com/adaptyteam/AdaptySDK-Capacitor/) để loại bỏ những rắc rối liên quan đến in-app purchase, giúp bạn tập trung vào điều mình giỏi nhất – xây dựng những ứng dụng tuyệt vời. Đây là những gì chúng tôi xử lý cho bạn: - Xử lý giao dịch mua hàng, xác thực biên lai và quản lý gói đăng ký ngay từ đầu - Tạo và kiểm thử paywall mà không cần cập nhật ứng dụng - Nhận phân tích mua hàng chi tiết mà không cần thiết lập – bao gồm cohort, LTV, churn và phân tích funnel - Luôn cập nhật trạng thái gói đăng ký của người dùng trên các phiên và thiết bị - Tích hợp ứng dụng của bạn với các dịch vụ attribution marketing và phân tích chỉ bằng một dòng code :::note Trước khi bắt đầu viết code, bạn cần tích hợp Adapty với App Store Connect và Google Play Console, sau đó thiết lập sản phẩm trong dashboard. Hãy xem [hướng dẫn quickstart](quickstart) để cấu hình mọi thứ trước. ::: ## Bắt đầu \{#get-started\} 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. Đây là những gì chúng ta sẽ đề cập trong hướng dẫn tích hợp: 1. [Cài đặt & cấu hình SDK](sdk-installation-capacitor): Thêm SDK làm [dependency](https://www.npmjs.com/package/@adapty/capacitor) vào dự án của bạn và kích hoạt nó trong code. 2. [Bật giao dịch mua hàng qua paywall](capacitor-quickstart-paywalls): Thiết lập luồng mua hàng để người dùng có thể mua sản phẩm. 3. [Kiểm tra trạng thái gói đăng ký](capacitor-check-subscription-status): Tự động kiểm tra trạng thái gói đăng ký của người dùng và kiểm soát quyền truy cập vào nội dung trả phí. 4. [Xác định người dùng (tùy chọn)](capacitor-quickstart-identify): Liên kết người dùng với hồ sơ người dùng Adapty để đảm bảo dữ liệu của họ được lưu trữ nhất quán trên các thiết bị. ### Xem thực tế \{#see-it-in-action\} Muốn xem cách mọi thứ kết hợp với nhau? Chúng tôi đã chuẩn bị sẵn: **Ứng dụng mẫu**: Xem các ví dụ đầy đủ minh họa toàn bộ thiết lập: - [React](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-react-example) - [Vue.js](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-vue-example) - [Angular](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-angular-example) - [Công cụ phát triển nâng cao](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/adapty-devtools) ## Các khái niệm chính \{#main-concepts\} Trước khi bắt đầu viết code, hãy làm quen với các khái niệm cốt lõi giúp Adapty hoạt động. Điểm hay trong cách tiếp cận của Adapty là chỉ có placement được hardcode trong ứng dụng của bạn. Mọi thứ khác – sản phẩm, thiết kế paywall, giá cả và ưu đãi – đều có thể được quản lý linh hoạt từ Adapty Dashboard mà không cần cập nhật ứng dụng: 1. **Sản phẩm** - Bất kỳ thứ gì có thể mua trong ứng dụng của bạn – gói đăng ký, consumable product hoặc quyền truy cập trọn đời. 2. **Flow hoặc paywall** - Các sản phẩm được gộp cùng cấu hình, gắn vào một placement. Có hai dạng: - **[Flow](adapty-flow-builder)** - Giao diện trực quan, không cần code, được xây dựng trong Flow Builder. Adapty render UI và xử lý giao dịch mua hàng cho bạn. - **[Paywall](paywalls)** - Không có cấu hình giao diện; bạn tự xây dựng UI trong code của mình và tự gọi `makePurchase`. Xem [Triển khai paywall thủ công](capacitor-quickstart-manual). Trong code SDK, cả hai đều được truy xuất qua cùng một phương thức `getFlow`. 3. **Placement** - Một điểm chiến lược trong hành trình người dùng nơi bạn muốn hiển thị paywall. Hãy nghĩ về placement như "nơi" và "khi nào" trong chiến lược kiếm tiền của bạn. Các placement phổ biến bao gồm: - `main` - Vị trí paywall chính của bạn - `onboarding` - Hiển thị trong luồng onboarding của người dùng - `settings` - Có thể truy cập từ phần cài đặt ứng dụng Bắt đầu với những cái cơ bản như `main` hoặc `onboarding` cho lần tích hợp đầu tiên, sau đó suy nghĩ về những nơi khác trong ứng dụng mà người dùng có thể sẵn sàng mua hàng. 4. **Hồ sơ người dùng** - Khi người dùng mua sản phẩm, hồ sơ người dùng của họ sẽ được gán một **mức độ truy cập** mà bạn dùng để xác định quyền truy cập vào các tính năng trả phí. --- # File: sdk-installation-capacitor --- --- title: "Capacitor - Cài đặt & cấu hình Adapty SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên Capacitor cho ứng dụng dựa trên gói đăng ký." --- Adapty SDK bao gồm hai module chính để tích hợp liền mạch vào ứng dụng Capacitor của bạn: - **Core Adapty**: Module này bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), công cụ no-code thân thiện với người dùng để tạo paywall đa nền tảng dễ dàng. AdaptyUI được kích hoạt tự động cùng với module core. :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Yêu cầu \{#requirements\} [Adapty Capacitor SDK](https://github.com/adaptyteam/AdaptySDK-Capacitor/) có các yêu cầu phiên bản sau: | Phiên bản Adapty SDK | Phiên bản Capacitor | Phiên bản iOS | |--------------------|-------------------|-------------| | 3.16.0+ | 8 | 15.0+ | | 3.15 | 7 | 14.0+ | Capacitor phiên bản 6 trở xuống không được hỗ trợ. Việc xây dựng cho iOS với Adapty SDK v4 (beta) yêu cầu **Xcode 26** trở lên — native iOS SDK được xây dựng bằng Swift tools 6.2. Yêu cầu iOS 15.0+, Capacitor 8, và Android minSdk 24 vẫn giống như SDK 3.16+. :::info Bắt đầu từ SDK v3.17, Adapty SDK sử dụng Google Play Billing Library v8.0.0 theo mặc định. ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK \{#install-adapty-sdk\} :::important Các bước dưới đây cài đặt Adapty SDK 3.x. SDK v4 (beta) — bắt buộc cho [Flow Builder](adapty-flow-builder) và được sử dụng bởi [quickstart](capacitor-quickstart-paywalls) — được cài đặt theo cách khác: làm theo [Adapty SDK 4.0 (beta)](#adapty-sdk-40-beta) bên dưới, hoặc xem [hướng dẫn migration](migration-to-capacitor-sdk-v4). ::: [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Capacitor.svg?style=flat&logo=capacitor)](https://github.com/adaptyteam/AdaptySDK-Capacitor/releases) Cài đặt Adapty SDK: ```sh npm install @adapty/capacitor npx cap sync ``` ### Adapty SDK 4.0 (beta) Capacitor SDK 4.0 — tích hợp hỗ trợ [Flow Builder](adapty-flow-builder) — là phiên bản pre-release. Hãy cài đặt đúng phiên bản (npm không xử lý pre-release qua caret/tilde), sau đó sync: ```sh npm install @adapty/capacitor@4.0.0-beta.2 ``` ```sh npx cap sync ``` Trên iOS, v4 kéo các native Adapty SDK thông qua **Swift Package Manager** — podspec CocoaPods đã bị loại bỏ ([kho spec của CocoaPods sẽ chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/)). Dự án iOS của app bạn phải sử dụng tích hợp SPM của Capacitor: - Đối với ứng dụng mới, thêm nền tảng iOS với trình quản lý gói SPM: ```sh npx cap add ios --packagemanager SPM ``` - Đối với ứng dụng hiện có, migrate dự án iOS từ CocoaPods sang SPM bằng cách làm theo [hướng dẫn của Capacitor về cách sử dụng SPM trong dự án hiện có](https://capacitorjs.com/docs/ios/spm#using-spm-in-an-existing-capacitor-project). Để xem danh sách đầy đủ các thay đổi API trong v4, xem [Migrate Adapty Capacitor SDK sang v4](migration-to-capacitor-sdk-v4). ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: Để lấy **Public SDK Key**: 1. Truy cập Adapty Dashboard và điều hướng đến [**App settings → General**](https://app.adapty.io/settings/general). 2. Trong phần **Api keys**, sao chép **Public SDK Key** (KHÔNG phải Secret Key). 3. Thay thế `"YOUR_PUBLIC_SDK_KEY"` trong code. Hoặc lấy theo cách lập trình, sử dụng [Adapty CLI](developer-cli): ``` npm install -g adapty adapty auth login adapty apps list ``` Hoặc, trực tiếp: ``` npx adapty auth login adapty apps list ``` - Đảm bảo bạn sử dụng **Public SDK key** để khởi tạo Adapty, **Secret key** chỉ nên dùng cho [server-side API](getting-started-with-server-side-api). - **SDK keys** là duy nhất cho mỗi ứng dụng, vì vậy nếu bạn có nhiều ứng dụng, hãy đảm bảo chọn đúng key. Sao chép đoạn code sau vào bất kỳ file nào trong ứng dụng để kích hoạt Adapty: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // verbose logging is recommended for the development purposes and for the first production release logLevel: 'verbose', // in the development environment, use this variable to avoid multiple activation errors. Set it to your development environment variable __ignoreActivationOnFastRefresh: true, } }); console.log('Adapty activated successfully!'); } catch (error) { console.error('Failed to activate Adapty SDK:', error); } ``` :::important Hãy đợi `activate` hoàn thành trước khi gọi bất kỳ phương thức nào khác của Adapty SDK. Xem [Thứ tự gọi trong Capacitor SDK](capacitor-sdk-call-order) để biết toàn bộ trình tự. ::: :::tip Để tránh lỗi kích hoạt trong môi trường phát triển, hãy sử dụng các [mẹo](#development-environment-tips). ::: 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), hãy làm theo [hướng dẫn nhanh về Paywall Builder](capacitor-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](capacitor-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder), bạn cần module AdaptyUI. Module này được kích hoạt tự động khi bạn kích hoạt module core; bạn không cần thực hiện thêm bất kỳ thao tác nào khác. ## Thiết lập tùy chọn \{#optional-setup\} ### Ghi nhật ký \{#logging\} #### Thiết lập hệ thống ghi nhật ký \{#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 điều gì đang xảy ra. Các mức độ ghi nhật ký có sẵn như sau: | Level | Description | | ---------- | ------------------------------------------------------------ | | `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à các thông báo thông tin đa dạng | | `verbose` | Ghi lại mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như các lệnh gọi hàm, truy vấn API, v.v. | Bạn có thể đặt mức log trong ứng dụng trước hoặc trong quá trình cấu hình Adapty: ```typescript showLineNumbers // Set log level before activation adapty.setLogLevel({ logLevel: 'verbose' }); // Or set it during configuration await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { logLevel: 'verbose', } }); ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi lên, nhưng bạn có thể triển khai thêm các chính sách bảo mật dữ liệu để tuân thủ hướng dẫn của cửa hàng hoặc quy định của từng quốc gia. #### Tắt tính năng 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 tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để bảo vệ quyền riêng tư của người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm thiểu việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không cần thiết cho ứng dụng của bạn. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ipAddressCollectionDisabled: true, } }); ``` #### Tắt thu thập và chia sẻ advertising ID \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ios.idfaCollectionDisabled` (iOS) hoặc `android.adIdCollectionDisabled` (Android) thành `true` để tắt việc thu thập mã định danh quảng cáo. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách của App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc nếu ứng dụng của bạn không cần attribution quảng cáo hay phân tích dựa trên ID quảng cáo. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ios: { idfaCollectionDisabled: true, }, android: { adIdCollectionDisabled: true, }, } }); ``` #### Thiết lập cấu hình bộ nhớ đệm media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Theo 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 lưu lượng mạng. Bạn có thể tùy chỉnh cài đặt cache bằng cách cung cấp một cấu hình tùy chỉnh. Dùng `mediaCache` để ghi đè các cài đặt cache mặc định: ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { mediaCache: { memoryStorageTotalCostLimit: 200 * 1024 * 1024, // Optional: memory cache size in bytes memoryStorageCountLimit: 2147483647, // Optional: max number of items in memory diskStorageSizeLimit: 200 * 1024 * 1024, // Optional: disk cache size in bytes }, } }); ``` | 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 theo giá trị của từng nền tảng. | | memoryStorageCountLimit | tùy chọn | Giới hạn số lượng mục trong bộ nhớ cache. Mặc định theo giá trị của từng nền tảng. | | 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 theo giá trị của từng nền tảng. | ### 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 `localAccessLevelAllowed` thành `true`: ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { localAccessLevelAllowed: true, }, } }); ``` ### Xóa dữ liệu khi khôi phục bản sao lưu \{#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ừ bản sao lưu iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng đã lưu trong bộ nhớ đệm, chi tiết sản phẩm và paywall. Sau đó SDK sẽ khởi tạo ở trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ bộ nhớ đệm cục bộ của SDK bị xóa. Lịch sử giao dịch với Apple và dữ liệu người dùng trên máy chủ Adapty vẫn không thay đổi. ::: ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ios: { clearDataOnBackup: true, }, } }); ``` ## Mẹo cho môi trường phát triển \{#development-environment-tips\} #### Khắc phục lỗi kích hoạt SDK trên Capacitor's live-reload \{#troubleshoot-sdk-activation-errors-on-capacitors-live-reload\} Khi phát triển với Adapty SDK trong Capacitor, bạn có thể gặp lỗi: `Adapty can only be activated once. Ensure that the SDK activation call is not made more than once.` Lỗi này xảy ra vì tính năng live-reload của Capacitor kích hoạt nhiều lần gọi kích hoạt trong quá trình phát triển. Để tránh điều này, hãy sử dụng tùy chọn `__ignoreActivationOnFastRefresh` được đặt thành cờ chế độ phát triển của Capacitor – giá trị này sẽ khác nhau tùy thuộc vào bundle bạn đang sử dụng. ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // Set your development environment variable __ignoreActivationOnFastRefresh: true, } }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` ## Khắc phục sự cố \{#troubleshooting\} #### Lỗi phiên bản iOS tối thiểu \{#minimum-ios-version-error\} :::note Phần này áp dụng cho các dự án dùng CocoaPods trên **SDK 3.x**. SDK 4.0 cài đặt trên iOS thông qua Swift Package Manager (không có `Podfile`) và yêu cầu iOS 15.0 — hãy đặt deployment target thành 15.0 trong Xcode. ::: Nếu bạn gặp lỗi phiên bản iOS tối thiểu trên SDK 3.x, hãy cập nhật Podfile của bạn: ```diff -platform :ios, min_ios_version_supported +platform :ios, '15.0' ``` #### Quy tắc sao lưu Android (Cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `<manifest>` có chứa tools: ```xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app"> ... </manifest> ``` #### 2. Ghi đè các thuộc tính backup trong `<application>` \{#2-override-backup-attributes-in-application\} Trong cùng file `AndroidManifest.xml`, cập nhật thẻ `<application>` để ứ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 <application android:name=".App" android:allowBackup="true" android:fullBackupContent="@xml/sample_backup_rules" android:dataExtractionRules="@xml/sample_data_extraction_rules" tools:replace="android:fullBackupContent,android:dataExtractionRules"> ... </application> ``` 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" <?xml version="1.0" encoding="utf-8"?> <data-extraction-rules> <cloud-backup> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </cloud-backup> <device-transfer> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </device-transfer> </data-extraction-rules> ``` **Dành cho Android 11 trở xuống** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" <?xml version="1.0" encoding="utf-8"?> <full-backup-content> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> :::tip Sau khi thay đổi các file Android native, hãy chạy `npx cap sync android` để Capacitor cập nhật các resource mới nếu bạn tái tạo platform. ::: #### Giao dịch mua thất bại sau khi quay lại từ ứng dụng khác trên Android \{#purchases-fail-after-returning-from-another-app-in-android\} Nếu Activity khởi động 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ị xử lý như là đã hủy. Để đảm bảo giao dịch mua hoạt động đúng, hãy chỉ sử dụng chế độ khởi chạy `standard` hoặc `singleTop` cho Activity bắt đầu luồng mua hàng, và tránh các chế độ khác. Trong `AndroidManifest.xml`, đảm bảo Activity bắt đầu luồng mua hàng được đặt thành `standard` hoặc `singleTop`: ```xml <activity android:name=".MainActivity" android:launchMode="standard" /> ``` #### Lỗi build Swift 6 do Podfile ghi đè SWIFT_VERSION \{#swift-6-build-errors-caused-by-podfile-swift_version-override\} :::note Nội dung này áp dụng cho các dự án dùng CocoaPods trên **SDK 3.x**. SDK 4.0 cài đặt các native SDK thông qua Swift Package Manager, nên không có `Podfile` nào cần điều chỉnh. ::: Khi build ứng dụng Capacitor cho iOS, bạn có thể gặp 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 không khớp `@Sendable` trong `AdaptyUIBuilderLogic`, thiếu conformance `Sendable` trên các kiểu của Adapty, hoặc lỗi actor isolation. Các pod Adapty khai báo `s.swift_version = '6.0'` và yêu cầu Swift 6 để build. Code của ứng dụng bạn vẫn có thể dùng Swift 5 — chỉ có các pod target của Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`, `AdaptyLogger`, `AdaptyPlugin`) mới cần build với Swift 6. Nguyên nhân phổ biến nhất là hook `post_install` trong `ios/App/Podfile` ghi đè `SWIFT_VERSION` cho mọi pod target: ```ruby showLineNumbers title="ios/App/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 sửa**: Loại trừ các pod target của Adapty khỏi việc ghi đè: ```ruby showLineNumbers title="ios/App/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 `npx cap sync ios` và build lại. Để kiểm tra, mở `ios/App/Pods/Pods.xcodeproj`, chọn pod target `Adapty` → **Build Settings** → **Swift Language Version**. Giá trị phải là **Swift 6**. --- # File: capacitor-quickstart-paywalls --- --- title: "Bật tính năng mua hàng với Flow Builder trong Capacitor SDK" description: "Hướng dẫn nhanh để bật in-app purchase với Adapty Flow Builder." --- Để bật in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – mọi thứ người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flows**](adapty-flow-builder) – chuỗi màn hình giới thiệu sản phẩm đến người dùng, được xây dựng trong Flow Builder không cần code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện bằng code, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](capacitor-quickstart-manual). - [**Placements**](placements) – nơi và thời điểm bạn hiển thị flow trong ứng dụng (như `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trên dashboard, sau đó gọi chúng theo placement ID trong code. Điều này giúp dễ dàng chạy A/B test và hiển thị flow khác nhau cho từng nhóm người dùng. Adapty cung cấp ba cách để kích hoạt tính năng 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: | Triển khai | Độ phức tạp | Khi nào sử dụng | |---|---|---| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng thanh toán trong trình xây dựng không cần code](quickstart-paywalls). Adapty tự động hiển thị và xử lý toàn bộ luồng mua hàng phức tạp, xác thực biên lai và quản lý gói đăng ký. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự triển khai giao diện paywall trong code ứng dụng, nhưng vẫn lấy đối tượng flow từ Adapty để linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](capacitor-quickstart-manual). | | Observer mode | 🔴 Khó | Bạn đã có cơ sở hạ tầng xử lý thanh toán riêng và muốn tiếp tục sử dụng nó. Lưu ý rằng observer mode có những hạn chế nhất định trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, hãy xem [Triển khai paywall thủ công](capacitor-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code của ứng dụng, bạn chỉ cần: 1. **Lấy flow**: Lấy từ Adapty. 2. **Hiển thị nó và Adapty sẽ xử lý giao dịch mua cho bạn**: Hiển thị view trong ứng dụng của bạn. 3. **Xử lý hành động nút**: Liên kết tương tác của người dùng với phản hồi tương ứng trong ứng dụng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. Kết nối ứng dụng của bạn với [App Store](initial_ios) và/hoặc [Google Play](initial-android) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo 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 Adapty SDK](sdk-installation-capacitor) 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 quickstart](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy flow \{#1-get-the-flow\} Các flow của bạn được liên kết với các placement đã cấu hình trên dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy một flow đã tạo trong Adapty Flow Builder, hãy lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow`. Flow chứa các phần tử giao diện và kiểu dáng cần thiết để hiển thị nó. ```typescript showLineNumbers title="Capacitor" try { const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID', }); // the requested flow } catch (error) { // handle the error } ``` ## 2. Hiển thị flow \{#display-the-flow\} Bây giờ, khi bạn đã có flow, chỉ cần thêm vài dòng code để hiển thị nó. Tạo một `view` bằng phương thức `createFlowView`, thiết lập các event handler cho nó, rồi gọi `view.present()`. Mỗi `view` chỉ có thể dùng một lần. Nếu bạn cần hiển thị lại flow, hãy gọi `createFlowView` thêm một lần nữa để tạo một `view` instance mới. ```typescript showLineNumbers title="Capacitor" try { const view = await createFlowView(flow); await view.setEventHandlers({ onPurchaseCompleted(purchaseResult, product) { return purchaseResult.type === 'success'; // close the flow on a successful purchase, keep it open for cancelled or pending purchases }, }); await view.present(); } catch (error) { // handle the error } ``` :::tip Để biết thêm chi tiết về cách hiển thị một flow, xem [hướng dẫn](capacitor-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút bấm \{#3-handle-button-actions\} Khi người dùng nhấn các nút trong flow, Capacitor SDK tự động xử lý việc mua hàng, khôi phục, đóng flow và mở URL. Tuy nhiên, một số nút có ID tùy chỉnh hoặc được định nghĩa sẵn và cần bạn xử lý hành động trong code. Hoặc bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ, đây là hành vi mặc định của nút đóng. Bạn không cần thêm đoạn này vào code, nhưng ở đây bạn có thể thấy cách thực hiện nếu cần. ```typescript showLineNumbers title="Capacitor" const unsubscribe = await view.setEventHandlers({ onCloseButtonPress() { return true; // allow the flow to close }, }); ``` :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [hành động](capacitor-handle-paywall-actions) và [sự kiện](capacitor-handling-events) của nút bấm. ::: ## Bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Kiểm tra các giao dịch mua trong [sandbox App Store](test-purchases-in-sandbox) hoặc trong [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. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](capacitor-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í đúng cho người dùng. ## Ví dụ đầy đủ \{#full-example\} Dưới đây là cách tích hợp tất cả các bước trong hướng dẫn này vào ứng dụng của bạn. ```typescript showLineNumbers title="Capacitor" export async function showFlow() { try { const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID', }); const view = await createFlowView(flow); await view.setEventHandlers({ onCloseButtonPress() { return true; }, onPurchaseCompleted(purchaseResult, product) { return purchaseResult.type === 'success'; // close the flow on a successful purchase, keep it open for cancelled or pending purchases }, }); await view.present(); } catch (error) { // handle any error that may occur during the process console.warn('Error showing flow:', error); } } ``` --- # File: capacitor-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong Capacitor SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Capacitor 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) trong hồ sơ người dùng của họ. Bài viết này hướng dẫ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ọ — cho họ xem paywall hay cho phép họ truy cập các tính năng trả phí. ## Lấy trạng thái gói đăng ký \{#get-subscription-status\} Khi quyết định có nên 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ọ. Bạn có hai lựa chọn: - Gọi `getProfile` nếu bạn cần dữ liệu hồ sơ mới nhất ngay lập tức (ví dụ: khi khởi động ứng dụng) hoặc muốn buộc cập nhật. - Thiết lập **cập nhật hồ sơ tự động** để giữ một bản sao cục bộ được làm mới tự động 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` để truy cập hồ sơ: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); } catch (error) { // handle the error } ``` ### Lắng nghe cập nhật gói đăng ký \{#listen-to-subscription-updates\} Để tự động nhận cập nhật hồ sơ người dùng trong ứng dụng: 1. Dùng `adapty.addListener('onLatestProfileLoad')` để lắng nghe các thay đổi của hồ sơ người dùng — Adapty sẽ tự động gọi phương thức này mỗi khi trạng thái gói đăng ký của người dùng thay đổi. 2. Lưu lại dữ liệu hồ sơ người dùng được cập nhật khi phương thức này được gọi, để bạn có thể dùng nó trong toàn bộ ứng dụng mà không cần thực hiện thêm yêu cầu mạng. ```typescript showLineNumbers class SubscriptionManager { private currentProfile: any = null; constructor() { // Listen for profile updates adapty.addListener('onLatestProfileLoad', (data) => { this.currentProfile = data.profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() hasAccess(): boolean { return this.currentProfile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive ?? false; } } ``` :::note Adapty tự động gọi event listener `onLatestProfileLoad` khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký đã được cache ngay cả khi thiết bị không có kết nối mạng. ::: ## Kết nối hồ sơ người dùng với logic hiển thị 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 vào các tính năng trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách tiếp cận này hữu ích trong các tình huống như khi khởi động ứng dụng, khi truy cập vào các khu vực premium, hoặc trước khi hiển thị nội dung cụ thể. ```typescript showLineNumbers const checkAccessLevel = async () => { try { const profile = await adapty.getProfile(); return profile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive === true; } catch (error) { console.warn('Error checking access level:', error); return false; // Show paywall if access check fails } }; const getAccessLevel = (profile: AdaptyProfile) => { return profile.accessLevels?.['YOUR_ACCESS_LEVEL']; }; const initializePaywall = async () => { try { await loadPaywall(); const hasAccess = await checkAccessLevel(); if (!hasAccess) { // Show paywall if no access } } catch (error) { console.warn('Error initializing paywall:', error); } }; ``` ## Các bước tiếp theo \{#next-steps\} Giờ bạn đã biết cách theo dõi trạng thái gói đăng ký, hãy tìm hiểu cách [làm việc với hồ sơ người dùng](capacitor-quickstart-identify) để đảm bảo họ có thể truy cập những gì họ đã thanh toán. --- # File: capacitor-quickstart-identify --- --- title: "Xác định người dùng trong Capacitor 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 với Capacitor." --- Cách bạn quản lý 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 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ó (hoặc sẽ có) xác thực backend, xem [phần về người dùng đã xác định](#identified-users). :::tip **Các khái niệm chính**: - **Hồ sơ người dùng** là các thực thể cần thiết để SDK hoạt động. Adapty tự động tạo chúng. Chúng có thể là ẩn danh (không có customer user ID) hoặc đã xác định (có customer user ID). - **Customer user ID** là định danh tùy chọn **bạn tạo** để Adapty liên kết người dùng của bạn với hồ sơ người dùng Adapty của họ. ::: Dưới đây là sự khác biệt giữa người dùng ẩn danh và người dùng đã xác định: | | Người dùng ẩn danh | Người dùng đã xác định | |-------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------| | **Quản lý 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ơ người dùng** | Hồ sơ mới mỗi lần cài đặt lại | Cùng một hồ sơ người dùng trên các phiên và thiết bị | | **Tính bền vững của dữ liệu** | Dữ liệu người dùng ẩn danh gắn với thiết bị/lần cài đặt | Dữ liệu người dùng đã xác định tồn tại trên các thiết bị và phiên | ## 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 mã ứng dụng**: 1. Khi SDK được kích hoạt lần đầu tiên khởi chạy ứng dụng, Adapty **tạo một hồ sơ người dùng mới cho người dùng**. 2. Khi người dùng mua bất cứ thứ gì trong ứng dụng, giao dịch mua này được **liên kết với hồ sơ người dùng Adapty và tài khoản cửa hàng của họ**. 3. Khi người dùng **cài đặt lại** ứng dụng hoặc cài đặt từ **thiết bị mới**, Adapty **tạo một hồ sơ người dùng mới trống khi kích hoạt**. 4. Nếu người dùng đã từng mua hàng trong ứng dụng của bạn trước đó, theo mặc định, giao dịch mua của họ sẽ được tự động đồng bộ từ App Store khi SDK được kích hoạt. :::note Khôi phục từ bản sao lưu hoạt động khác với cài đặt lại. Theo mặc định, khi người dùng khôi phục từ bản sao lưu, SDK giữ nguyên dữ liệu đã lưu trong bộ nhớ đệm và không tạo hồ sơ người dùng mới. Bạn có thể cấu hình hành vi này bằng cài đặt `clearDataOnBackup`. [Tìm hiểu thêm](sdk-installation-capacitor#clear-data-on-backup-restore). ::: ## Người dùng đã xác định \{#identified-users\} - Nếu một hồ sơ người dùng chưa có customer user ID (nghĩa là **người dùng chưa đăng nhập**), khi bạn gửi customer user ID, nó sẽ được liên kết với hồ sơ người dùng đó. - Nếu đây là **cài đặt lại, đăng nhập lại, hoặc cài đặt từ thiết bị mới**, và bạn đã gửi customer user ID của họ trước đó, thì không tạo hồ sơ người dùng mới. Thay vào đó, chúng tôi chuyển sang hồ sơ người dùng hiện có được liên kết với customer user ID. Bạn có hai lựa 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ẻ, vì vậy cả hai hồ sơ người dùng đều có quyền truy cập có trả phí. Bạn có thể cấu hình cài đặt này để chuyển quyền truy cập có trả phí từ hồ sơ người dùng này sang hồ sơ người dùng khác hoặc tắt hoàn toàn tính năng chia sẻ. Xem [bài viết](general#6-sharing-paid-access-between-user-accounts) để biết thêm chi tiết. ::: <img src="/assets/shared/img/identify-diagram.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### 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 vào ứng dụng hoặc đăng ký), hãy sử dụng phương thức `identify` để đặt customer user ID cho 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ơ người dùng 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ơ người dùng được liên kết với customer user ID này. :::tip Khi tạo customer user ID, hãy lưu nó cùng với dữ liệu người dùng để bạn có thể gửi cùng một ID khi họ đăng nhập từ thiết bị mới hoặc cài đặt lại ứng dụng của bạn. ::: Luôn `await` `identify` trước khi gọi các phương thức SDK khác. Các lời gọi đồng thời sẽ tạo ra lỗi `#3006 profileWasChanged` hoặc kết thúc trên hồ sơ người dùng ẩn danh. Xem [Thứ tự gọi trong Capacitor SDK](capacitor-sdk-call-order). ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` ### Trong quá trình kích hoạt SDK \{#during-the-sdk-activation\} Nếu bạn đã biết customer user ID khi kích hoạt SDK, bạn có thể gửi nó trong phương thức `activate` thay vì gọi `identify` riêng. Nếu bạn biết customer user ID nhưng chỉ đặt nó sau khi kích hoạt, điều đó có nghĩa là khi kích hoạt, Adapty sẽ tạo một hồ sơ người dùng mới trống và chỉ chuyển sang hồ sơ người dùng hiện có sau khi bạn gọi `identify`. Bạn có thể truyền customer user ID hiện có (cái bạn đã sử dụng trước đây) hoặc customer user ID mới. Nếu bạn truyền cái mới, hồ sơ người dùng mới được tạo khi kích hoạt sẽ tự động được liên kết với customer user ID đó. :::tip Để loại trừ các hồ sơ người dùng trống đã tạo khỏi analytics của dashboard, hãy vào **App settings** và thiết lập [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```typescript showLineNumbers await adapty.activate({ apiKey: "YOUR_PUBLIC_SDK_KEY", params: { customerUserId: "YOUR_USER_ID" } }); ``` ### Đă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`. Thao tác này tạo một ID hồ sơ người dùng ẩn danh mới cho người dùng. ```typescript showLineNumbers try { await adapty.logout(); // successful logout } catch (error) { // handle the 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ể mua hàng cả trước và sau khi đăng nhập vào ứng dụng, bạn không cần thiết lập thêm: Cách hoạt động: 1. Khi người dùng chưa đăng nhập thực hiện mua hàng, Adapty liên kết giao dịch đó với ID hồ sơ người dùng ẩ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ơ người dùng đã xác định của họ. - Nếu đây là customer user ID hiện có (customer user ID đã được liên kết với một hồ sơ người dùng), Adapty sẽ tự động đồng bộ các giao dịch của nó. - 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ơ người dùng hiện tại, vì vậy toàn bộ lịch sử giao dịch mua được duy trì. --- # File: adapty-sdk-integration-skill-capacitor --- --- title: "Tích hợp Adapty vào ứng dụng Capacitor của bạn với kỹ năng tích hợp SDK" description: "Sử dụng kỹ năng adapty-sdk-integration để tích hợp Adapty SDK vào ứng dụng Capacitor của bạn từ đầu đến cuối với công cụ lập trình AI của bạn." --- :::important Kỹ năng này đang 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-capacitor) — 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 đúng tài liệu cần thiết. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn form phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt xong, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-capacitor --- --- title: "Tích hợp Adapty vào ứng dụng Capacitor 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 Capacitor 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ẽ đưa bạn qua từng bước tích hợp Adapty vào ứng dụng Capacitor bằng công cụ lập trình AI — bạn chỉ cần cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ dòng 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 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 ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng](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 — kể cả khi nào cần mở Dashboard để kết nối cửa hàng. ### Phương pháp cấu hình qua Dashboard \{#dashboard-approach\} Nếu bạn muốn tự cấu hình mọi thứ theo cách thủ công, đây là những gì bạn cần chuẩn bị trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard thay bạn — bạn sẽ cần tự cung cấp chúng. 1. **Kết nối 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 Capacitor của bạn nhắm đến cả hai nền tảng. Đây là yêu cầu bắt buộc để mua hàng hoạt động được. [Kết nối cửa hàng ứng dụng](integrate-payments) 2. **Sao chép Public SDK key của bạn**: Trong Adapty Dashboard, vào **App settings → General**, sau đó tìm phần **API keys**. Trong code, đây là chuỗi bạn truyền vào `adapty.activate()`. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu trực tiếp các sản phẩm trong code — Adapty phân phối chúng thông qua các paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo paywall và placement**: Trong Adapty Dashboard, tạo một 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.getFlow()`. [Tạo paywall](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình theo từng sản phẩm trên trang **Products**. Trong code, chuỗi được kiểm tra là `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 truy cập các tính năng khác nhau tùy theo sản phẩm (ví dụ: gói `basic` và 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 lập trình. :::tip Khi bạn đã có đủ năm thông tin 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 ra code khởi tạo và lấy flow chính xác. ::: ### Thiết lập khi sẵn sàng \{#set-up-when-ready\} Những bước này không bắt buộc để bắt đầu code, nhưng bạn sẽ cần chúng khi tích hợp trở nên hoàn chỉ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ệnh 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 \{#feed-adapty-docs-to-your-llm\} ### Sử dụng Context7 (khuyến nghị) \{#use-context7-recommended\} [Context7](https://context7.com) là một MCP server cho phép LLM của bạn truy cập trực tiếp vào tài liệu Adapty cập nhật mới nhất. LLM sẽ tự động tìm và lấy đúng tài liệu cần thiết dựa trên câu hỏi của bạn — 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. Để cài đặt, chạy lệnh: ``` npx ctx7 setup ``` Lệnh này sẽ tự động nhận diện trình soạn thảo của bạn và cấu hình Context7 server. Để cài đặt thủ công, xem [kho lưu trữ Context7 trên GitHub](https://github.com/upstash/context7). Sau khi cấu hình xong, hãy tham chiếu thư viện Adapty trong các prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the Capacitor SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn rất 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ột để đảm bảo mọi thứ hoạt động đúng. ::: ### Dùng tài liệu dạng văn bản thuần \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng Markdown thuần. 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-capacitor.md](https://adapty.io/docs/vi/adapty-cursor-capacitor.md). Mỗi giai đoạn 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. Để lấy nhiều tài liệu hơn cùng một lúc, xem [file index và các tập con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này sẽ dẫn bạn qua tích hợp Adapty theo đúng thứ tự triển khai. Mỗi giai đoạn bao gồm tài liệu cần gửi cho LLM, kết quả mong đợi khi hoàn thành và các lỗi thường gặp. ### Lập kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết code, hãy nhờ LLM phân tích dự án của bạn và tạo một 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 mode của Cursor hoặc Claude Code), hãy 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ỳ dòng code nào. Hãy cho LLM biết bạn đang dùng cách tiếp cận nào cho in-app purchase — điều này ảnh hưởng đến các hướng dẫn mà LLM sẽ theo: - [**Adapty Flow Builder**](adapty-flow-builder): Bạn tạo flow trong trình xây dựng không cần code của Adapty, và SDK sẽ tự động render chúng. - [**Paywalls tạo thủ công**](capacitor-making-purchases): Bạn tự xây dựng giao diện paywall bằng code nhưng vẫn dùng Adapty để lấy sản phẩm và xử lý thanh toán. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên hạ tầng thanh toán 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 phần quickstart](capacitor-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm dependency Adapty SDK bằng npm và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — mọi thứ khác đều không hoạt động nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-capacitor) :::info Hướng dẫn này dành cho Adapty Capacitor SDK v4 (beta) — API được hướng dẫn trong [quickstart](capacitor-quickstart-paywalls). v4 là bản phát hành trước, vì vậy hãy đảm bảo LLM của bạn ghim đúng phiên bản (`npm install @adapty/capacitor@4.0.0-beta.2`) thay vì cài đặt phiên bản ổn định 3.x mới nhất. Xem [phần cài đặt SDK 4.0](sdk-installation-capacitor#adapty-sdk-40-beta) và [hướng dẫn migration](migration-to-capacitor-sdk-v4). ::: 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-capacitor.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Ứng dụng build và chạy được trên cả iOS và Android. Console hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra lại xem bạn đã thay thế placeholder bằng key thật 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 sẽ phụ thuộc vào cách bạn xử lý giao dịch mua. Hãy kiểm tra từng giao dịch mua trong sandbox trong quá trình thực hiện — đừng đợi đến cuối. Xem [Kiểm tra giao dịch mua trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn cài đặt. <Tabs groupId="paywall-approach"> <TabItem value="builder" label="Flow Builder" default> **Hướng dẫn:** - [Kích hoạt mua hàng bằng flows (quickstart)](capacitor-quickstart-paywalls) - [Lấy flows & paywalls](capacitor-get-pb-paywalls) - [Hiển thị flows & paywalls](capacitor-present-paywalls) - [Xử lý sự kiện](capacitor-handling-events) - [Phản hồi các hành động](capacitor-handle-paywall-actions) ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/capacitor-quickstart-paywalls.md - https://adapty.io/docs/vi/capacitor-get-pb-paywalls.md - https://adapty.io/docs/vi/capacitor-present-paywalls.md - https://adapty.io/docs/vi/capacitor-handling-events.md - https://adapty.io/docs/vi/capacitor-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Flow hiển thị với các sản phẩm bạn đã cấu hình. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Flow trống hoặc lỗi `getFlow` → kiểm tra xem placement ID có khớp chính xác với dashboard không và placement đã được gán đối tượng chưa. ::: </TabItem> <TabItem value="manual" label="Manual paywalls"> **Hướng dẫn:** - [Bật tính năng mua hàng trong paywall tùy chỉnh của bạn (quickstart)](capacitor-quickstart-manual) - [Tải paywall và sản phẩm](fetch-paywalls-and-products-capacitor) - [Hiển thị paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-capacitor) - [Thực hiện mua hàng](capacitor-making-purchases) - [Khôi phục giao dịch mua](capacitor-restore-purchase) ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/capacitor-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products-capacitor.md - https://adapty.io/docs/vi/present-remote-config-paywalls-capacitor.md - https://adapty.io/docs/vi/capacitor-making-purchases.md - https://adapty.io/docs/vi/capacitor-restore-purchase.md ``` :::tip[Checkpoint] - **Expected:** Paywall tùy chỉnh của bạn hiển thị các sản phẩm lấy từ Adapty. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Gotcha:** Mảng sản phẩm trống → kiểm tra xem paywall đã được gán sản phẩm trong dashboard chưa và placement có đối tượng chưa. ::: </TabItem> <TabItem value="observer" label="Observer mode"> **Hướng dẫn:** - [Tổng quan về Observer mode](observer-vs-full-mode) - [Triển khai Observer mode](implement-observer-mode-capacitor) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode-capacitor) ``` 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-capacitor.md - https://adapty.io/docs/vi/report-transactions-observer-mode-capacitor.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi thực hiện mua hàng sandbox bằng flow mua hàng hiện có của bạn, giao dịch sẽ xuất hiện trong **Event Feed** trên Adapty dashboard. - **Lưu ý:** Không có sự kiện nào → hãy kiểm tra xem bạn đã báo cáo giao dịch cho Adapty chưa và thông báo máy chủ đã được cấu hình cho cả hai cửa hàng chưa. ::: </TabItem> </Tabs> ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua hàng, kiểm tra hồ sơ người dùng để xác nhận mức độ truy cập đang hoạt động nhằm kiểm soát nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](capacitor-check-subscription-status) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/capacitor-check-subscription-status.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi mua hàng sandbox, `profile.accessLevels['premium']?.isActive` trả về `true`. - **Lỗi thường gặp:** `accessLevels` rỗng sau khi mua → kiểm tra 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 để lịch sử mua hàng được lưu giữ trên tất cả các 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](capacitor-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/capacitor-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 tải paywall để tránh việc attribution hồ sơ người dùng ẩ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 xem 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 production. **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 máy chủ, luồng mua hàng, kiểm tra mức độ truy cập và các yêu cầu về quyền riêng tư. - **Lưu ý:** Thiếu thông báo máy chủ → 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**. ::: ## Tệp chỉ mục tài liệu dạng văn bản thuần túy \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM của mình ngữ cảnh rộng hơn ngoài các trang riêng lẻ, chúng tôi lưu trữ các tệp chỉ mục liệt kê hoặc 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 các liên kết `.md`. Đây là [tiêu chuẩn mới nổi](https://llmstxt.org/) giúp các LLM có thể truy cập website. Lưu ý rằng với một số AI agent (ví dụ: ChatGPT), bạn cần tải `llms.txt` xuống 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ộ tài liệu Adapty được gộp thành một file duy nhất. Rất lớn — chỉ dùng khi bạn cần xem toàn bộ nội dung. - Các file dành riêng cho Capacitor: [`capacitor-llms.txt`](https://adapty.io/docs/vi/capacitor-llms.txt) và [`capacitor-llms-full.txt`](https://adapty.io/docs/vi/capacitor-llms-full.txt): Các tập con theo nền tảng giúp tiết kiệm token so với toàn bộ tài liệu. --- # File: capacitor-paywalls --- --- title: "Flows và paywalls - Capacitor" description: "Hiển thị và quản lý các flow và paywall được xây dựng bằng Adapty Flow Builder hoặc Paywall Builder trong ứng dụng Capacitor của bạn." --- ## Hiển thị paywall \{#display-paywalls\} ### Adapty Flow Builder & Paywall Builder \{#adapty-flow-builder--paywall-builder\} <CustomDocCardList ids={['capacitor-get-pb-paywalls', 'capacitor-present-paywalls', 'capacitor-handling-events', 'capacitor-handle-paywall-actions']} /> :::tip Để bắt đầu nhanh với các flow và paywall của Adapty, hãy xem [hướng dẫn bắt đầu nhanh](capacitor-quickstart-paywalls) của chúng tôi. ::: ### Triển khai paywall thủ công \{#implement-paywalls-manually\} <CustomDocCardList ids={['capacitor-quickstart-manual', 'fetch-paywalls-and-products-capacitor', 'present-remote-config-paywalls-capacitor', 'capacitor-making-purchases']} /> Để xem thêm các hướng dẫn về triển khai paywall và xử lý giao dịch mua thủ công, hãy xem [danh mục này](capacitor-implement-paywalls-manually). ## Các tính năng hữu ích \{#useful-features\} <CustomDocCardList ids={['capacitor-use-fallback-paywalls', 'capacitor-web-paywall']} /> --- # File: capacitor-get-pb-paywalls --- --- title: "Lấy flows & paywalls - Capacitor" description: "Lấy flows và paywalls từ Adapty trong ứng dụng Capacitor của bạn." --- <SDKv4> <MethodPromo method="getFlow" /> Sau khi [bạn đã thiết kế flow hoặc paywall trong Paywall Builder](adapty-paywall-builder), bạn có thể hiển thị chúng trong ứng dụng mobile của mình. Bước đầu tiên là lấy flow hoặc paywall được liên kết với placement cùng cấu hình view của nó như mô tả bên dưới. Lưu ý rằng chủ đề này đề cập đến các flow và paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall thủ công, vui lòng tham khảo chủ đề [Lấy paywall và sản phẩm cho remote config paywall trong ứng dụng mobile của bạn](fetch-paywalls-and-products-capacitor). :::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. ::: <details> <summary>Trước khi bắt đầu hiển thị flow và paywall trong ứng dụng di động của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 2. [Tạo flow/paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-capacitor) vào ứng dụng di động của bạn. </details> ## Lấy flow/paywall \{#fetch-flowpaywall\} Nếu bạn đã thiết kế một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall như vậy đã chứa sẵn cả nội dung hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view của nó, rồi mới có thể trình bày nó trong ứng dụng di động. Tải flow hoặc paywall và tạo [view](capacitor-get-pb-paywalls#fetch-the-view-configuration) của nó càng sớm càng tốt — lý tưởng nhất là trước khi hiển thị. Phương thức `createFlowView` tải cấu hình view và bắt đầu tải xuống, cache hình ảnh ở nền. Bạn gọi nó càng sớm, các hình ảnh càng có nhiều thời gian để tải xong. Đến khi bạn hiển thị flow hoặc paywall, cấu hình và hình ảnh của nó đã có thể được cache sẵn sàng để hiển thị. Để lấy một flow hoặc paywall, sử dụng phương thức `getFlow`: ```typescript showLineNumbers try { const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID', }); // the requested flow/paywall } catch (error) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |-------------------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `'reload_revalidating_cache_data'` | <p>Được truyền bên trong object `params` tùy chọn. Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>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 `'return_cache_data_else_load'` để 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 hoàn toàn mới nhất, nhưng sẽ có tốc độ tải nhanh hơn dù kết nối internet không ổn định. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các network request không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>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ột server dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **loadTimeoutMs** | mặc định: 5 giây | <p>Được truyền bên trong object `params` tùy chọn. Giá trị này giới hạn thời gian timeout cho phương thức này. Nếu đạt đến timeout, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeoutMs`, do thao tác có thể bao gồm nhiều request khác nhau bên dưới.</p> | **Đừng hardcode product ID.** ID duy nhất bạn cần hardcode là placement ID. Flow và paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt — nếu hôm nay paywall trả về hai sản phẩm, ngày mai trả về ba, hãy hiển thị tất cả mà không cần sửa code. Tham số phản hồi: | Tham số | Mô tả | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Flow | Một đối tượng `AdaptyFlow` chứa các định danh của flow (`id`, `variationId`), tên, placement, các biến thể paywall (`paywalls`), và bất kỳ Remote Config nào (`remoteConfigs`). | ## Lấy cấu hình view \{#fetch-the-view-configuration\} :::important Hãy đảm bảo bật toggle **Show on device** trong builder. Nếu tùy chọn này chưa được bật, cấu hình view sẽ không thể lấy được. ::: Nếu placement được thiết kế trong **Flow Builder** hoặc **Paywall Builder**, Adapty sẽ tự động render giao diện người dùng cho bạn. Tạo view bằng `createFlowView`, sau đó [hiển thị flow hoặc paywall](capacitor-present-paywalls). Nếu placement là một paywall tùy chỉnh không có Builder UI, hãy [xử lý nó như một Remote Config paywall](present-remote-config-paywalls-capacitor). Trong Capacitor SDK, gọi `createFlowView` trực tiếp — bạn không cần lấy cấu hình view trước. :::warning Kết quả của phương thức `createFlowView` chỉ có thể dùng một lần. Nếu cần dùng lại, hãy gọi lại phương thức `createFlowView`. Gọi hai lần mà không tạo lại có thể dẫn đến lỗi. ::: ```typescript showLineNumbers try { const view = await createFlowView(flow); } catch (error) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :------------------- | :------- | :----------------------------------------------------------- | | **flow** | bắt buộc | Đối tượng `AdaptyFlow` để lấy controller cho flow/paywall mong muốn. | | **customTags** | tùy chọn | Định nghĩa một dictionary gồm các custom tag và giá trị tương ứng. Custom tag đóng vai trò là placeholder trong nội dung, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong flow/paywall. Tham khảo chủ đề Custom tags in paywall builder để biết thêm chi tiết. | | **prefetchProducts** | tùy chọn | Bật để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Khi đặt là `true`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. Mặc định: `true`. | | **android.enableSafeArea** | tùy chọn | Chỉ dành cho Android (bỏ qua trên iOS). Đặt dưới key `android`. Khi đặt là `true`, flow view sẽ áp dụng padding safe-area. Mặc định: `true`. Giá trị mặc định phù hợp với hầu hết các trường hợp. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa flow](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã ngôn ngữ đúng cách [tại đây](capacitor-localizations-and-locale-codes). ::: Sau khi có view, hãy [hiển thị flow/paywall](capacitor-present-paywalls). ## Lấy flow hoặc paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, flow và paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng của bạn có kết nối internet yếu, việc tải flow hoặc paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị flow hoặc paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow hoặc paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow hoặc paywall bằng phương thức `getFlow`, như đã mô tả chi tiết trong phần [Lấy flow/paywall](#fetch-flowpaywall) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa targeting (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow hoặc paywall nhanh hơn, hãy dùng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` được mô tả [ở trên](#fetch-flowpaywall). ::: ```typescript showLineNumbers try { const flow = await adapty.getFlowForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', }); // the requested flow/paywall } catch (error) { // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo một placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `'reload_revalidating_cache_data'` | <p>Được truyền vào trong object `params` tùy chọn. Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyên dùng phương án này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để 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 mạng có chập chờn đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các request mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh asset \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong flow/paywall của bạn, hãy triển khai các custom asset. Hình ảnh hero và video có ID được xác định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh xem trước trước khi phát video. Dưới đây là ví dụ về cách cung cấp custom assets thông qua một dictionary đơn giản: ```typescript showLineNumbers const customAssets: Record<string, AdaptyCustomAsset> = { 'custom_image': { type: 'image', relativeAssetPath: 'custom_image.png' }, 'hero_video': { type: 'video', fileLocation: { ios: { fileName: 'custom_video.mp4' }, android: { relativeAssetPath: 'videos/custom_video.mp4' } } } }; const view = await createFlowView(flow, { customAssets }); ``` :::note Nếu không tìm thấy asset, flow/paywall sẽ trở về giao diện mặc định. ::: </SDKv4> <SDKv3> 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 của mình. Bước đầu tiên trong quá trình này là lấy paywall được liên kết với placement và cấu hình view của nó như mô tả bên dưới. Xin lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Để biết hướng dẫn về việc lấy paywall Remote Config, vui lòng tham khảo chủ đề [Lấy paywall và sản phẩm cho paywall Remote Config trong ứng dụng mobile của bạn](fetch-paywalls-and-products-capacitor). <details> <summary>Trước khi bắt đầu hiển thị paywall trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-capacitor) vào ứng dụng di động của bạn. </details> ## Tải paywall được thiết kế bằng Paywall Builder \{#fetch-paywall-designed-with-paywall-builder\} Nếu bạn đã [thiết kế paywall bằng Paywall Builder](adapty-paywall-builder), bạn không cần lo việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall đó đã chứa đầy đủ thông tin về nội dung cần hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view của nó, rồi trình bày nó trong ứng dụng. Để đảm bảo hiệu suất tối ưu, điều quan trọng là phải lấy paywall và [cấu hình view](capacitor-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, tạo đủ thời gian để tải ảnh trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `getPaywall`: ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', }); // the requested paywall } catch (error) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này cần là mã ngôn ngữ gồm một hoặc hai thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là cho ngôn ngữ, thẻ con thứ hai là cho vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>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.</p> | | **params** | tùy chọn | Các tham số bổ sung để tải paywall. | **Đừng hardcode product ID.** ID duy nhất bạn nên hardcode là placement ID. Vì paywall được cấu hình từ xa, số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. App của bạn phải xử lý những thay đổi này một cách linh hoạt — nếu hôm nay paywall trả về hai sản phẩm, ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. Tham số phản hồi: | Tham số | Mô tả | | :-------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | Đối tượng [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) chứa danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình hiển thị của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bạn đã bật nút **Show on device** trong Paywall Builder. Nếu tùy chọn này chưa được bật, cấu hình hiển thị sẽ không thể lấy được. ::: Sau khi tải paywall về, hãy kiểm tra xem nó có chứa `ViewConfiguration` không — đây là dấu hiệu cho thấy paywall được tạo bằng Paywall Builder. Điều này sẽ giúp bạn xác định cách hiển thị paywall. Nếu có `ViewConfiguration`, hãy xử lý như một paywall Paywall Builder; nếu không, [hãy xử lý như một paywall Remote Config](present-remote-config-paywalls-capacitor). Trong Capacitor SDK, hãy gọi trực tiếp phương thức `createPaywallView` mà không cần tải view configuration theo cách thủ công. :::warning Kết quả của phương thức `createPaywallView` chỉ có thể được sử dụng một lần. Nếu bạn cần sử dụng lại, hãy gọi phương thức `createPaywallView` một lần nữa. ::: ```typescript showLineNumbers if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { // use your custom logic } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :------------------- | :------- | :----------------------------------------------------------- | | **paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **customTags** | tùy chọn | Định nghĩa một dictionary gồm các custom tag và giá trị tương ứng. Custom tag đóng vai trò là placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Custom tags in paywall builder để biết thêm chi tiết. | | **prefetchProducts** | tùy chọn | Bật tùy chọn này để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Khi đặt là `true`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. Mặc định: `false`. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](capacitor-localizations-and-locale-codes). ::: Sau khi có view, hãy [hiển thị paywall](capacitor-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được tải về gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này 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ả chi tiết trong phần [Lấy thông tin Paywall](#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Tại sao chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **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 sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (legacy) hoặc chấp nhận rằng người dùng đang dùng phiên bản đó có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất 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 chấp nhận những hạn chế đó để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` được mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder). ::: ```typescript showLineNumbers try { const paywall = await adapty.getPaywallForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', }); // the requested paywall } catch (error) { // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [ngôn ngữ bản địa hóa paywall](add-remote-config-locale). Tham số này phải là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Bản địa hóa và mã ngôn ngữ](capacitor-localizations-and-locale-codes) để biết thêm thông tin về mã ngôn ngữ và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **params** | tùy chọn | Các tham số bổ sung để lấy paywall. | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets. Hình ảnh và video hero có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm tới các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh xem trước trước khi phát video. Dưới đây là ví dụ về cách cung cấp custom assets thông qua một dictionary đơn giản: ```typescript showLineNumbers const customAssets: Record<string, AdaptyCustomAsset> = { 'custom_image': { type: 'image', relativeAssetPath: 'custom_image.png' }, 'hero_video': { type: 'video', fileLocation: { ios: { fileName: 'custom_video.mp4' }, android: { relativeAssetPath: 'videos/custom_video.mp4' } } } }; view = await createPaywallView(paywall, { customAssets }); ``` :::note Nếu không tìm thấy asset, paywall sẽ tự động quay về giao diện mặc định. ::: </SDKv3> --- # File: capacitor-present-paywalls --- --- title: "Hiển thị flows & paywalls - Capacitor" description: "Trình bày flows và paywalls cho người dùng trong ứng dụng Capacitor của bạn với Adapty." --- <SDKv4> Nếu bạn đã tạo một flow hoặc paywall trong Flow Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã [tạo một flow hoặc paywall](create-paywall). 2. Bạn đã thêm nó vào một [placement](placements). 3. Bạn đã [lấy flow và chuẩn bị view](capacitor-get-pb-paywalls). :::warning Hướng dẫn này chỉ dành cho **flows và paywall được tạo bằng Paywall Builder**, yêu cầu SDK v4.0 trở lên. Quy trình hiển thị flows khác với paywall remote config. - Để hiển thị **paywall remote config**, xem [Hiển thị paywall được thiết kế bằng remote config](present-remote-config-paywalls-capacitor). ::: Để hiển thị một flow hoặc paywall dưới dạng màn hình độc lập, hãy dùng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createFlowView`](capacitor-get-pb-paywalls#fetch-the-view-configuration). Mỗi `view` chỉ có thể dùng một lần. Nếu cần hiển thị lại flow, hãy gọi `createFlowView` thêm một lần nữa để tạo instance `view` mới. :::warning Không được tái sử dụng cùng một `view` mà không tạo lại. Điều này sẽ gây ra lỗi. ::: ```typescript showLineNumbers const view = await createFlowView(flow); // Optional: handle flow events (close, purchase, restore, etc) // await view.setEventHandlers({ ... }); try { await view.present(); } catch (error) { // handle the error } ``` :::important Gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã thiết lập trước đó cho những sự kiện cụ thể đó. ::: ## Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\} Cấu hình cách flow hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này nhận giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. Trên Android, flow luôn được hiển thị dưới dạng full-screen activity. ```typescript showLineNumbers try { await view.present({ iosPresentationStyle: 'page_sheet' }); } catch (error) { // handle the error } ``` ## Sử dụng timer do nhà phát triển định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer do nhà phát triển định nghĩa trong ứng dụng di động, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer bạn đã thiết lập trong Adapty dashboard. Điều này đảm bảo ứng dụng tự động cập nhật timer với giá trị chính xác — chẳng hạn `13d 09h 03m 34s` (được tính bằng thời gian kết thúc của timer, như Năm Mới, trừ đi thời gian hiện tại). ```typescript showLineNumbers const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) }; const view = await createFlowView(flow, { customTimers }); ``` Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của timer do bạn tự định nghĩa trong Adapty dashboard. Timer này đảm bảo ứng dụng của bạn tự động cập nhật giá trị đếm ngược chính xác—ví dụ như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, chẳng hạn như Ngày đầu năm mới, trừ đi thời điểm hiện tại). ## Hiển thị hộp thoại \{#show-dialog\} Dùng phương thức này thay cho hộp thoại alert mặc định khi một flow view đang được hiển thị trên Android. Trên Android, các alert thông thường sẽ bị che khuất phía sau flow view, khiến người dùng không nhìn thấy. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng vị trí, phía trên flow, trên mọi nền tảng. ```typescript showLineNumbers try { const action = await view.showDialog({ title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', }); if (action === 'secondary') { // User confirmed - close the flow await view.dismiss(); } // If primary - do nothing, user stays } catch (error) { // handle error } ``` ## Thay thế một gói đăng ký bằng gói khác \{#replace-one-subscription-with-another\} Khi người dùng cố mua một gói đăng ký mới trong khi đã có một gói đăng ký đang hoạt động trên Android, bạn có thể kiểm soát cách xử lý giao dịch mua mới bằng cách truyền các tham số cập nhật gói đăng ký khi tạo flow view. Để thay thế gói đăng ký hiện tại bằng gói mới, hãy dùng `productPurchaseParams` trong `createFlowView` với các tham số `oldSubVendorProductId` và `prorationMode`. ```typescript showLineNumbers const productPurchaseParams = flow.paywalls .flatMap((paywall) => paywall.productIdentifiers) .map((productId) => { const params: MakePurchaseParamsInput = {}; if (Capacitor.getPlatform() === 'android') { params.android = { subscriptionUpdateParams: { oldSubVendorProductId: 'PRODUCT_ID_OF_THE_CURRENT_ACTIVE_SUBSCRIPTION', prorationMode: 'with_time_proration', }, }; } return { productId, params }; }); const view = await createFlowView(flow, { productPurchaseParams }); ``` </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này chỉ dành cho **paywall được tạo bằng Paywall Builder**. Quy trình hiển thị paywall khác đối với Remote Config paywall. Để hiển thị **Remote Config paywall**, xem [Render paywall được thiết kế bằng Remote Config](present-remote-config-paywalls). ::: Để 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`](capacitor-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 một `view` instance mới. :::warning Việc tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi. ::: ```typescript showLineNumbers const view = await createPaywallView(paywall); view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); try { await view.present(); } catch (error) { // handle the error } ``` ## Sử dụng timer do nhà phát triển định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer do nhà phát triển định nghĩa trong ứng dụng, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer do nhà phát triển định nghĩa mà bạn đã cấu hình trên Adapty dashboard. Điều này đảm bảo ứng dụng tự động cập nhật timer với giá trị chính xác — ví dụ `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, chẳng hạn Ngày đầu năm mới, trừ đi thời điểm hiện tại). ```typescript showLineNumbers const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) }; const view = await createPaywallView(paywall, { customTimers }); ``` Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của timer do nhà phát triển định nghĩa mà bạn đã thiết lập trong Adapty dashboard. Timer này đảm bảo ứng dụng của bạn tự động cập nhật bộ đếm với giá trị chính xác—chẳng hạn như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, ví dụ như Ngày đầu năm mới, trừ đi thời điểm hiện tại). ## Hiển thị hộp thoại \{#show-dialog\} Sử dụng phương thức này thay vì các 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 sẽ 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 được hiển thị đúng cách phía trên paywall trên mọi nền tảng. ```typescript showLineNumbers title="Capacitor" try { const action = await view.showDialog({ title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', }); if (action === 'secondary') { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (error) { // handle error } ``` ## Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này nhận giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. ```typescript showLineNumbers await view.present({ iosPresentationStyle: 'page_sheet' }); ``` </SDKv3> --- # File: capacitor-handle-paywall-actions --- --- title: "Phản hồi hành động flow - Capacitor" description: "Xử lý hành động nút từ flow và paywall trong Capacitor bằng Adapty để tối ưu hóa việc kiếm tiền từ ứng dụng." --- <SDKv4> Nếu bạn đang xây dựng flow hoặc paywall bằng [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) của Adapty, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm một [nút trong builder](paywall-buttons) và gán cho nó một action có sẵn hoặc tạo một custom action ID. 2. Viết code trong app của bạn để xử lý từng action đã gán. Hướng dẫn này trình bày cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Việc mua hàng, khôi phục, đóng flow và paywall, cũng như mở URL đều được xử lý tự động.** Bạn có thể cấu hình hành vi mặc định của chúng hoặc triển khai phản hồi cho các custom action. ::: :::note Khi bạn đặt một handler cho một sự kiện, hành vi mặc định của sự kiện đó sẽ bị thay thế hoàn toàn. Các handler bạn không đặt sẽ giữ nguyên hành vi mặc định. ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall: 1. Trong builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai một handler cho hành động `close` để đóng flow hoặc paywall. :::info Trong Capacitor SDK, hành động `close` sẽ tự động đóng flow hoặc paywall theo mặc định. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ: đóng một flow có thể kích hoạt mở một flow khác. ::: ```typescript showLineNumbers const view = await createFlowView(flow); const unsubscribe = await view.setEventHandlers({ onCloseButtonPress() { return true; // allow the flow to close }, }); ``` Trên Android, nút **Back** của hệ thống và cử chỉ vuốt để quay lại sẽ kích hoạt một sự kiện riêng là `onAndroidSystemBack`. Trong SDK v4, nút này không còn tự động đóng flow theo mặc định nữa. Trả về `true` từ handler nếu bạn muốn nút **Back** đóng flow: ```typescript showLineNumbers const unsubscribe = await view.setEventHandlers({ onAndroidSystemBack() { return true; // close the flow when the Back button is pressed }, }); ``` ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch mua), hãy thêm phần tử **Link** trong builder và xử lý nó tương tự như các nút có hành động **Open URL**. ::: Để thêm nút mở một liên kết từ flow hoặc paywall (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Nếu cần, trong code ứng dụng của bạn, hãy triển khai một handler cho hành động `openUrl` để mở URL nhận được theo cách của riêng bạn. :::info Trong Capacitor SDK, khi nhấn vào URL, mặc định sẽ mở bằng trình duyệt gốc: SDK gọi `adapty.openWebUrl({ url, openIn })`, tuân theo tùy chọn **Open in** bạn đã thiết lập trong builder, và giữ nguyên flow. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```typescript showLineNumbers const unsubscribe = await view.setEventHandlers({ onUrlPress(url) { // Open the URL your own way, e.g. with the Capacitor Browser plugin Browser.open({ url }); return false; // keep the flow open }, }); ``` ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm nút xử lý các hành động khác: 1. Trong builder, thêm một nút, gán cho nó hành động **Custom**, và đặt cho nó một ID. 2. Trong code của ứng dụng, triển khai handler cho ID hành động bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đãi gói đăng ký 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 flow hoặc paywall khác: ```typescript showLineNumbers const unsubscribe = await view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another flow or paywall } }, }); ``` </SDKv4> <SDKv3> 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 bấm đúng cách là rất quan trọng: 1. Thêm [nút trong Paywall Builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một ID hành động tùy chỉnh. 2. Viết code trong ứng dụng của bạn để xử lý từng hành động bạn đã gán. Hướng dẫn này chỉ cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn. ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai trình xử lý cho hành động `close` để đóng paywall. :::info Trong Capacitor SDK, hành động `close` sẽ kích hoạt việc đóng paywall theo mặc định. 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 việc mở một paywall khác. ::: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { console.log('User closed paywall'); return true; // Allow the paywall to close } }); ``` ## Mở URL từ paywall \{#open-urls-from-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch), hãy thêm phần tử **Link** trong paywall builder và xử lý nó theo cách tương tự như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết từ paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong paywall builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code của ứng dụng, triển khai handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong Capacitor SDK, action `window.open` sẽ mặc định mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; // Don't close the paywall }, }); ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút đăng nhập cho người dùng vào ứng dụng của bạn: 1. Trong paywall builder, thêm một nút và gán cho nó hành động **Login**. 2. Trong code ứng dụng của bạn, triển khai một handler cho hành động `login` để xác định người dùng. ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'login') { // Navigate to login screen console.log('User requested login'); } } }); ``` ## Xử lý hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong 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 mã nguồn ứng dụng, triển khai một handler cho ID hành động bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút sẽ hiển thị một paywall khác: ```typescript showLineNumbers const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another paywall } }, }); ``` </SDKv3> --- # File: capacitor-handling-events --- --- title: "Xử lý sự kiện flow & paywall - Capacitor" description: "Xử lý sự kiện flow và paywall trong ứng dụng Capacitor của bạn với SDK của Adapty." --- <SDKv4> :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, lựa chọn sản phẩm và hiển thị flow. Bạn cũng có thể thiết lập xử lý nút (đóng flow, mở liên kết, hành động tùy chỉnh, v.v.). Xem [hướng dẫn xử lý hành động nút](capacitor-handle-paywall-actions) để biết thêm chi tiết. ::: Flow và paywall được xây dựng bằng [Flow Builder](adapty-flow-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm các lần nhấn nút (nút đóng, URL, lựa chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện trên flow. Tìm hiểu cách phản hồi các sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình flow trong ứng dụng di động của bạn, hãy triển khai phương thức `view.setEventHandlers`: :::important Bạn chỉ có thể đặt một handler cho mỗi sự kiện: gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã đặt trước đó cho những sự kiện cụ thể đó. Các handler bạn không đặt vẫn giữ nguyên hành vi mặc định. `setEventHandlers` trả về một hàm hủy đăng ký, và `view.dismiss()` sẽ xóa tất cả các handler. ::: ```typescript showLineNumbers const view = await createFlowView(flow); const unsubscribe = await view.setEventHandlers({ onCloseButtonPress() { return true; // close the flow (default behavior) }, onAndroidSystemBack() { return true; // close the flow; by default, it stays open }, onPurchaseCompleted(purchaseResult, product) { return purchaseResult.type === 'success'; // close the flow on a successful purchase, keep it open for cancelled or pending purchases }, onPurchaseStarted(product) { /***/ }, onPurchaseFailed(error, product) { /***/ }, onRestoreCompleted(profile) { /***/ }, onRestoreFailed(error) { /***/ }, onProductSelected(productId) { /***/ }, onError(error) { /***/ }, onLoadingProductsFailed(error) { /***/ }, onUrlPress(url, openIn) { adapty.openWebUrl({ url, openIn }).catch(console.warn); // same as the SDK default return false; // keep the flow open }, onAppeared() { /***/ }, onDisappeared() { /***/ }, onWebPaymentNavigationFinished() { /***/ }, }); ``` <Details> <summary>Ví dụ về sự kiện (Nhấp để mở rộng)</summary> Các ví dụ dưới đây hiển thị các thuộc tính có sẵn trong mỗi handler, với các giá trị minh họa trong phần chú thích. ```typescript // onUrlPress url; // 'https://example.com/terms' openIn; // 'browser_in_app' or 'browser_out_app' // onCustomAction actionId; // 'login' // onProductSelected productId; // 'premium_monthly' // onPurchaseStarted, onPurchaseCompleted, onPurchaseFailed product.vendorProductId; // 'premium_monthly' product.localizedTitle; // 'Premium Monthly' product.localizedDescription; // 'Premium subscription for 1 month' product.price?.amount; // 9.99 product.price?.currencyCode; // 'USD' product.price?.localizedString; // '$9.99' // onPurchaseCompleted purchaseResult.type; // 'success', 'pending', or 'user_cancelled' if (purchaseResult.type === 'success') { purchaseResult.profile.accessLevels['premium']?.isActive; // true } // onRestoreCompleted profile.accessLevels['premium']?.isActive; // true // onPurchaseFailed, onRestoreFailed, onError, onLoadingProductsFailed error.message; // 'Purchase failed due to insufficient funds' ``` </Details> Bạn có thể đăng ký các event handler mà bạn cần, và bỏ qua những cái không cần. Như vậy, các event listener không dùng đến sẽ không được tạo ra. Không có event handler nào là bắt buộc. Các event handler trả về một giá trị boolean. Nếu trả về `true`, quá trình hiển thị được coi là hoàn tất, flow screen sẽ đóng lại và các event listener cho view này sẽ bị xóa. Một số event handler có hành vi mặc định mà bạn có thể ghi đè nếu cần: - `onCloseButtonPress`: đóng flow khi người dùng nhấn nút đóng. - `onUrlPress`: mở URL được nhấn trong trình duyệt gốc thông qua `adapty.openWebUrl`, tuân theo tùy chọn **Open in** đã thiết lập trong builder, và giữ flow mở. - `onAndroidSystemBack`: giữ flow mở khi người dùng nhấn nút **Back**. Trả về `true` để đóng flow. - `onPurchaseCompleted`: giữ flow mở sau khi hoàn tất giao dịch mua. Trả về `true` để đóng flow. - `onRestoreCompleted`: giữ flow mở sau khi khôi phục thành công. Trả về `true` để đóng flow. - `onError`: đóng flow nếu quá trình render thất bại. ### Trình xử lý sự kiện \{#event-handlers\} | Trình xử lý sự kiện | Mô tả | |:-----------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCustomAction** | Được gọi khi người dùng thực hiện một hành động tùy chỉnh, ví dụ: nhấp vào [nút tùy chỉnh](paywall-buttons). | | **onUrlPress** | Được gọi khi người dùng nhấp vào một URL trong flow của bạn. | | **onAndroidSystemBack** | Được gọi khi người dùng nhấn nút **Back** hệ thống của Android. Theo mặc định, flow vẫn mở; trả về `true` để đóng flow. | | **onCloseButtonPress** | Được gọi khi nút đóng hiển thị và người dùng nhấn vào nó. Nên đóng màn hình flow trong trình xử lý này. | | **onPurchaseCompleted** | Được gọi khi giao dịch mua hoàn tất, dù thành công, bị người dùng hủy, hay đang chờ phê duyệt. Nếu mua thành công, sự kiện cung cấp `AdaptyProfile` đã được cập nhật. Trường hợp người dùng hủy và thanh toán đang chờ xử lý (ví dụ: cần phê duyệt của phụ huynh) sẽ kích hoạt sự kiện này, không phải `onPurchaseFailed`. | | **onPurchaseStarted** | Được gọi khi người dùng nhấn nút hành động "Mua" để bắt đầu quá trình thanh toán. | | **onPurchaseFailed** | Được gọi khi giao dịch mua thất bại do lỗi (ví dụ: hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng, lỗi xác minh giao dịch). Không được gọi khi người dùng hủy hoặc thanh toán đang chờ xử lý — những trường hợp đó sẽ kích hoạt `onPurchaseCompleted`. | | **onRestoreStarted** | Được gọi khi người dùng bắt đầu quá trình khôi phục giao dịch mua. | | **onRestoreCompleted** | Được gọi khi khôi phục giao dịch mua thành công và cung cấp `AdaptyProfile` đã được cập nhật. Nên đóng màn hình nếu người dùng có `accessLevel` yêu cầu. Xem chủ đề [Trạng thái gói đăng ký](capacitor-listen-subscription-changes) để biết cách kiểm tra. | | **onRestoreFailed** | Được gọi khi quá trình khôi phục thất bại và cung cấp `AdaptyError`. | | **onProductSelected** | Được gọi khi bất kỳ sản phẩm nào trong giao diện flow được chọn, cho phép bạn theo dõi lựa chọn của người dùng trước khi mua. | | **onError** | Được gọi khi xảy ra lỗi trong quá trình render giao diện và cung cấp `AdaptyError`. Những 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. | | **onLoadingProductsFailed** | Được gọi khi tải sản phẩm thất bại và cung cấp `AdaptyError`. Nếu bạn chưa đặt `prefetchProducts: true` khi tạo giao diện, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ máy chủ. | | **onAppeared** | Được gọi khi flow được hiển thị cho người dùng. Trên iOS, cũng được gọi khi người dùng nhấn [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một flow và một web paywall mở trong trình duyệt trong ứng dụng. | | **onDisappeared** | Được gọi khi người dùng đóng flow. 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ừ flow trong trình duyệt trong ứng dụng biến mất khỏi màn hình. | | **onWebPaymentNavigationFinished** | Được gọi sau khi cố gắng mở một [web paywall](web-paywall) để mua, dù thành công hay thất bại. | | **onRequestAppReview** | Dành riêng cho yêu cầu đánh giá ứng dụng từ một flow. Các flow chưa kích hoạt yêu cầu đánh giá ứng dụng, nên bạn không cần triển khai nó. | | **onAnalytics** | Dành riêng cho các sự kiện phân tích tùy chỉnh từ một flow. Các flow chưa phát ra những sự kiện này vào mã của bạn, nên bạn không cần triển khai nó. | | **onRequestPermission** | Dành riêng cho các yêu cầu quyền hệ thống (như thông báo đẩy hoặc quyền truy cập camera) từ một flow. Các flow chưa kích hoạt yêu cầu quyền, nên bạn không cần triển khai nó. | | **onObserverPurchaseInitiated** | Chỉ dành cho chế độ observer: Được gọi khi người dùng nhấn nút mua trong một flow. Adapty không thực hiện giao dịch mua — hãy thực hiện bằng mã mua hàng của riêng bạn, sau đó báo cáo giao dịch cho Adapty. Xem [Xử lý giao dịch mua trong chế độ observer](#handle-purchases-in-observer-mode) bên dưới. | | **onObserverRestoreInitiated** | Chỉ dành cho chế độ observer: Được gọi khi người dùng nhấn nút khôi phục trong một flow. Adapty không thực hiện khôi phục — hãy tự thực hiện, sau đó báo cáo bất kỳ giao dịch nào đã khôi phục. Xem [Xử lý giao dịch mua trong chế độ observer](#handle-purchases-in-observer-mode) bên dưới. | ### Xử lý mua hàng trong Observer mode \{#handle-purchases-in-observer-mode\} Nếu bạn khởi tạo SDK ở chế độ [Observer mode](implement-observer-mode-capacitor) (`observerMode: true`) và hiển thị flow được Adapty render, SDK sẽ không tự thực hiện mua hàng cho bạn. Khi người dùng nhấn nút mua hoặc khôi phục, SDK sẽ gọi `onObserverPurchaseInitiated` hoặc `onObserverRestoreInitiated` để bạn có thể tự xử lý việc mua hoặc khôi phục bằng code của riêng mình. Xem [Hiển thị flow trong Observer mode](capacitor-present-flows-in-observer-mode) để biết cách thiết lập đầy đủ. </SDKv4> <SDKv3> :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các lần 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](capacitor-handle-paywall-actions) của chúng tôi để biết thêm chi tiết. ::: Paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Những sự kiện này bao gồm các lần nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện trên paywall. Hãy xem cách phản hồi các sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai phương thức `view.setEventHandlers`: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { console.log('User closed paywall'); return true; // Allow the paywall to close }, onAndroidSystemBack() { console.log('User pressed back button'); return true; // Allow the paywall to close }, onAppeared() { console.log('Paywall appeared'); return false; // Don't close the paywall }, onDisappeared() { console.log('Paywall disappeared'); }, onPurchaseCompleted(purchaseResult, product) { console.log('Purchase completed:', purchaseResult); return purchaseResult.type !== 'user_cancelled'; // Close if not cancelled }, onPurchaseStarted(product) { console.log('Purchase started:', product); return false; // Don't close the paywall }, onPurchaseFailed(error, product) { console.error('Purchase failed:', error); return false; // Don't close the paywall }, onRestoreCompleted(profile) { console.log('Restore completed:', profile); return true; // Close the paywall after successful restore }, onRestoreFailed(error) { console.error('Restore failed:', error); return false; // Don't close the paywall }, onProductSelected(productId) { console.log('Product selected:', productId); return false; // Don't close the paywall }, onRenderingFailed(error) { console.error('Rendering failed:', error); return false; // Don't close the paywall }, onLoadingProductsFailed(error) { console.error('Loading products failed:', error); return false; // Don't close the paywall }, onUrlPress(url) { window.open(url, '_blank'); return false; // Don't close the paywall }, }); ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```typescript // onCloseButtonPress { "event": "close_button_press" } // onAndroidSystemBack { "event": "android_system_back" } // onAppeared { "event": "paywall_shown" } // onDisappeared { "event": "paywall_closed" } // onUrlPress { "event": "url_press", "url": "https://example.com/terms" } // onCustomAction { "event": "custom_action", "actionId": "login" } // onProductSelected { "event": "product_selected", "productId": "premium_monthly" } // onPurchaseStarted { "event": "purchase_started", "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // onPurchaseCompleted - Success { "event": "purchase_completed", "purchaseResult": { "type": "success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // onPurchaseCompleted - Cancelled { "event": "purchase_completed", "purchaseResult": { "type": "user_cancelled" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // onPurchaseFailed { "event": "purchase_failed", "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } } } // onRestoreCompleted { "event": "restore_completed", "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" } ] } } // onRestoreFailed { "event": "restore_failed", "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } // onRenderingFailed { "event": "rendering_failed", "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } // onLoadingProductsFailed { "event": "loading_products_failed", "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ``` </Details> Bạn có thể đăng ký những event handler cần thiết và bỏ qua những cái không cần. Như vậy, các event listener không dùng đến sẽ không được tạo ra. Không có event handler nào là bắt buộc. Event handler trả về giá trị boolean. Nếu trả về `true`, quá trình hiển thị được coi là hoàn tất, paywall sẽ đóng lại và các event listener của view đó sẽ bị xóa. Một số event handler có hành vi mặc định mà bạn có thể ghi đè nếu cần: - `onCloseButtonPress`: đóng paywall khi nhấn nút đóng. - `onAndroidSystemBack`: đóng paywall khi nhấn nút **Back**. - `onRestoreCompleted`: đóng paywall sau khi khôi phục thành công. - `onPurchaseCompleted`: đóng paywall trừ khi người dùng hủy. - `onRenderingFailed`: đóng paywall nếu việc hiển thị thất bại. - `onUrlPress`: mở URL trong trình duyệt hệ thống và giữ paywall mở. ### Trình xử lý sự kiện \{#event-handlers\} | Event handler | Mô tả | |:----------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCustomAction** | Được gọi khi người dùng thực hiện một hành động tùy chỉnh, ví dụ: nhấp vào [nút tùy chỉnh](paywall-buttons). | | **onUrlPress** | Được gọi khi người dùng nhấp vào một URL trong paywall. | | **onAndroidSystemBack** | Được gọi khi người dùng nhấn nút **Back** hệ thống của Android. | | **onCloseButtonPress** | Được gọi khi nút đóng hiển thị và người dùng nhấn vào đó. Nên đóng màn hình paywall trong handler này. | | **onPurchaseCompleted** | Được gọi khi quá trình mua hoàn tất, bất kể thành công, người dùng hủy hay đang chờ phê duyệt. Trong trường hợp mua thành công, sự kiện này cung cấp một `AdaptyProfile` đã được cập nhật. Việc người dùng hủy và các khoản thanh toán đang chờ (ví dụ: cần phê duyệt của phụ huynh) sẽ kích hoạt sự kiện này, không phải `onPurchaseFailed`. | | **onPurchaseStarted** | Được gọi khi người dùng nhấn nút hành động "Mua" để bắt đầu quá trình mua. | | **onPurchaseCancelled** | Được gọi khi người dùng bắt đầu quá trình mua nhưng tự ý ngắt nó (hủy hộp thoại thanh toán). | | **onPurchaseFailed** | Được gọi khi giao dịch mua thất bại do lỗi (ví dụ: hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng, lỗi xác minh giao dịch). Không được gọi khi người dùng hủy hoặc khi thanh toán đang chờ — những trường hợp đó sẽ kích hoạt `onPurchaseCompleted`. | | **onRestoreStarted** | Được gọi khi người dùng bắt đầu quá trình khôi phục giao dịch mua. | | **onRestoreCompleted** | Được gọi khi khôi phục giao dịch mua thành công và cung cấp một `AdaptyProfile` đã được cập nhật. Nên đóng màn hình nếu người dùng có `accessLevel` cần thiết. Xem thêm tại [Trạng thái gói đăng ký](capacitor-listen-subscription-changes) để biết cách kiểm tra. | | **onRestoreFailed** | Được gọi khi quá trình khôi phục thất bại và cung cấp `AdaptyError`. | | **onProductSelected** | Được gọi khi người dùng chọn bất kỳ sản phẩm nào trong paywall, giúp bạn theo dõi lựa chọn của người dùng trước khi mua. | | **onAppeared** | Được gọi khi màn hình paywall xuất hiện trên màn hình. Trên iOS, cũng được gọi khi người dùng nhấn [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong paywall và một web paywall mở ra trong trình duyệt trong ứng dụng. | | **onDisappeared** | Được gọi khi màn hình paywall biến mất khỏi màn hình. 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. | | **onRenderingFailed** | Được gọi khi xảy ra lỗi trong quá trình render giao diện và cung cấp `AdaptyError`. 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, vui lòng thông báo cho chúng tôi. | | **onLoadingProductsFailed** | Được gọi khi tải sản phẩm thất bại và cung cấp `AdaptyError`. Nếu bạn chưa đặt `prefetchProducts: true` khi tạo view, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ máy chủ. | </SDKv3> --- # File: capacitor-use-fallback-paywalls --- --- title: "Capacitor - 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" --- Để 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\} ### Android 1. Thêm tệp cấu hình dự phòng vào ứng dụng của bạn. Chọn một trong các thư mục sau: * **android/app/src/main/assets/** * **android/app/src/main/res/raw/** :::note Thư mục `res/raw` có quy tắc đặt tên file đặc biệt (phải bắt đầu bằng chữ cái, không dùng chữ hoa, không dùng ký tự đặc biệt ngoại trừ dấu gạch dưới, và không có khoảng trắng trong tên file). ::: 2. Cập nhật thuộc tính `android` của hằng số `FileLocation`: * Nếu file nằm trong thư mục `assets`, truyền vào đường dẫn của file tương đối so với thư mục đó. * Nếu file nằm trong thư mục `res/raw`, truyền vào tên file không bao gồm phần mở rộng. ### iOS \{#ios\} 1. Thêm file JSON dự phòng vào bundle dự án của bạn: mở menu **File** trong XCode và chọn tùy chọn **Add Files to "YourProjectName"**. 2. Truyền tên file cấu hình của bạn vào thuộc tính `ios` của hằng số `FileLocation`. ## Ví dụ \{#example\} ```typescript showLineNumbers const fileLocation = { ios: { fileName: 'ios_fallback.json' }, android: { //if the file is located in 'android/app/src/main/assets/' relativeAssetPath: 'android_fallback.json' } }; await adapty.setFallback({ fileLocation }); ``` :::important `setFallback` phải chạy trước khi SDK tải flow, paywall hoặc onboarding mục tiêu. ::: Tham số: | Parameter | Description | | :------------------- | :------------------------------------------------------- | | **fileLocation** | Đối tượng biểu thị vị trí của tệp cấu hình paywall dự phòng. | --- # File: capacitor-localizations-and-locale-codes --- --- title: "Sử dụng bản địa hóa và mã ngôn ngữ trong Capacitor SDK" description: "Tìm hiểu cách bản địa hóa paywall trong ứng dụng Capacitor của bạn với Adapty SDK." --- <SDKv4> ## Tại sao điều này quan trọng \{#why-this-is-important\} Mã ngôn ngữ (locale code) được sử dụng khi Adapty chọn bản dịch cho một flow, và khi bạn đọc Remote Config cho một custom paywall. Mã ngôn ngữ khá phức tạp và có thể khác nhau tùy nền tảng, vì vậy Adapty dựa trên một tiêu chuẩn nội bộ thống nhất cho mọi nền tảng được hỗ trợ. Hiểu được tiêu chuẩn đó sẽ giúp bạn dự đoán được bản dịch nào mà người dùng nhận được. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Để mã hóa ngôn ngữ, Adapty sử dụng phiên bản [chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) được điều chỉnh nhẹ: 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 tìm kiếm bản bản địa hóa phù hợp với locale của người dùng, quá trình diễn ra như sau: 1. Chuỗi locale được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Adapty tìm bản địa hóa có mã locale khớp hoàn toàn 3. Nếu không tìm thấy, Adapty lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tìm bản địa hóa phù hợp 4. Nếu vẫn không tìm thấy, Adapty trả về bản địa hóa mặc định `en` Nhờ vậy, `'pt_BR'`, `pt-BR` và `pt-br` đều trỏ đến cùng một bản địa hóa. ## Triển khai bản địa hóa \{#implementing-localizations\} Trong SDK v4, bạn không cần truyền mã ngôn ngữ khi lấy một flow. - **Paywall dùng Flow Builder và Paywall Builder**: Adapty tự động xác định bản địa hóa dựa trên thiết bị và các bản địa hóa bạn đã cấu hình trong builder. Render flow bằng `createFlowView` — không cần mã ngôn ngữ. - **Paywall tùy chỉnh (remote config)**: `getFlow` trả về tất cả các bản địa hóa đã cấu hình trong `flow.remoteConfigs`. Mỗi mục có một mã `lang` và một đối tượng `data`. Hãy chọn mục phù hợp với người dùng, kèm theo logic dự phòng của riêng bạn: ```typescript showLineNumbers const flow = await adapty.getFlow({ placementId: 'placement_id' }); const config = flow.remoteConfigs?.find((c) => c.lang === 'en') ?? flow.remoteConfigs?.[0]; // read your values from config?.data ``` Các quy tắc khớp mã ngôn ngữ ở trên mô tả cách Adapty chuẩn hóa các mã `lang` được lưu trữ trên mỗi remote config. </SDKv4> <SDKv3> ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một số trường hợp mà mã ngôn ngữ (locale code) đóng vai trò quan trọng — ví dụ, khi bạn cần lấy đúng paywall cho ngôn ngữ hiện tại của ứng dụng. Vì mã ngôn ngữ khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ thống nhất cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, điều quan trọng là bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng bản ngôn ngữ, và quá trình xử lý tiếp theo diễn ra như thế nào — để luôn nhận được kết quả như mong đợi. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng phiên bản có chỉnh sửa nhẹ của [tiêu chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi mã bao gồm các subtag viết thường, phân tách bằng dấu gạch ngang. Một số ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Khớp mã ngôn ngữ \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client kèm theo mã ngôn ngữ và bắt đầu tìm kiếm bản bản địa hóa tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Hệ thống tìm kiếm bản bản địa hóa có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy, hệ thống lấy chuỗi con trước dấu gạch ngang đầu tiên (ví dụ: `pt` từ `pt-br`) và tìm kiếm bản bản địa hóa khớp với chuỗi đó 4. Nếu vẫn không tìm thấy, hệ thống trả về bản bản địa hóa mặc định `en` Theo cách này, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` sẽ đều nhận được kết quả giống nhau. ## Triển khai bản địa hóa: cách được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang tìm hiểu về bản địa hóa, có thể bạn đã làm việc với các file chuỗi đã được bản địa hóa trong dự án của mình. Nếu vậy, chúng tôi khuyến nghị bạn thêm một cặp key-value chứa mã locale Adapty tương ứng vào mỗi file bản địa hóa. Sau đó, trích xuất giá trị của key đó khi gọi SDK của chúng tôi, như sau: ```javascript showLineNumbers // 1. Modify your localization files (e.g., using react-i18next) /* en.json */ { "adapty_paywalls_locale": "en" } /* es.json */ { "adapty_paywalls_locale": "es" } /* pt-BR.json */ { "adapty_paywalls_locale": "pt-br" } // 2. Extract and use the locale code const MyComponent = () => { const { t } = useTranslation(); const fetchPaywall = async () => { const locale = t('adapty_paywalls_locale'); // pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale); }; }; ``` That way you can ensure you're in full control of what localization will be retrieved for every user of your app. ## Triển khai bản địa hóa theo cách khác \{#implementing-localizations-the-other-way\} Bạn có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống nhau) mà không cần phải khai báo rõ mã ngôn ngữ cho từng bản địa hóa. Thay vào đó, bạn có thể lấy mã ngôn ngữ từ các đối tượng khác mà nền tảng của bạn cung cấp, như sau: ```javascript showLineNumbers const getLocaleCode = () => { if (Capacitor.getPlatform() === 'ios') { return navigator.language || 'en'; } else { return navigator.language || 'en'; } }; const fetchPaywall = async () => { const locale = getLocaleCode(); // pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale); }; ``` Lưu ý rằng chúng tôi không khuyến nghị cách tiếp cận này vì một số lý do: 1. Trên iOS, ngôn ngữ ưu tiên và locale hiện tại không giống nhau. Nếu bạn muốn localization được chọn đúng, bạn cần dựa vào logic của Apple — hoạt động tự động nếu bạn dùng cách được khuyến nghị với các file chuỗi đã được bản địa hóa — hoặc tự tái tạo logic đó. 2. Rất khó đoán chính xác server của Adapty sẽ nhận được gì. Ví dụ, trên iOS, thiết bị có thể trả về một locale như `ar_OM@numbers='latn'` và gửi nó lên server của chúng tôi. Với lệnh gọi này, bạn sẽ không nhận được bản địa hóa `ar-om` như mong đợi, mà thay vào đó là `ar` — điều này nhiều khả năng không phải kết quả bạn muốn. Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. </SDKv3> --- # File: capacitor-web-paywall --- --- title: "Triển khai web paywall" description: "Tìm hiểu cách triển khai web paywall trong ứng dụng Capacitor của bạn với Adapty SDK." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trong dashboard](web-paywall) và đã cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: ## Mở web paywall \{#open-web-paywalls\} Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywall bằng phương thức SDK. Phương thức `.openWebPaywall`: 1. Tạo một URL duy nhất cho phép Adapty liên kết paywall cụ thể đang hiển thị với một người dùng cụ thể đến 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 trong hồ sơ người dùng có được cập nhật hay không. Bằng cách này, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ kích hoạt trong ứng dụng gần như ngay lập tức. ```typescript showLineNumbers try { await adapty.openWebPaywall({ paywallOrProduct: product }); } catch (error) { console.error('Failed to open web paywall:', error); } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall({ paywallOrProduct: product })` tạo URL theo paywall và thêm dữ liệu sản phẩm vào URL. 2. `openWebPaywall({ paywallOrProduct: paywall })` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng phiên bản này khi các sản phẩm trong paywall Adapty của bạn khác với các sản phẩm trong web paywall. Trong SDK v4, phía paywall của `paywallOrProduct` nhận một `AdaptyFlowPaywall` — một biến thể paywall của flow đã tải về. Hãy kiểm tra `flow.paywalls` không rỗng trước khi truy cập vào nó, ví dụ `flow.paywalls[0]`. ::: #### Xử lý lỗi \{#handle-errors\} | Lỗi | Mô tả | Hành động khuyến nghị | |-----------------------------------------|--------------------------------------------------------------------|----------------------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | Paywall chưa được cấu hình URL mua hàng qua web | Kiểm tra xem paywall đã được cấu hình đúng 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 rằng các tham số URL hợp lệ và được định dạng đúng | ## Lấy URL web paywall mà không cần mở nó \{#get-the-web-paywall-url-without-opening-it\} Nếu bạn muốn tự hiển thị trang mua hàng trên web thay vì để SDK mở nó, hãy dùng `createWebPaywallUrl`. Hàm này trả về cùng một URL duy nhất mà `openWebPaywall` sẽ mở, vì vậy bạn có thể render nó trong web view của riêng mình hoặc xử lý redirect theo cách bạn muốn. Hàm này nhận cùng tham số `paywallOrProduct` — một biến thể paywall của flow đã fetch (`AdaptyFlowPaywall`) hoặc một `AdaptyPaywallProduct`. ```typescript showLineNumbers try { const url = await adapty.createWebPaywallUrl({ paywallOrProduct: product }); // open `url` in your own web view, or handle the redirect yourself } catch (error) { console.error('Failed to create web paywall URL:', error); } ``` :::note Để mở một URL tùy ý (không phải web paywall) qua trình duyệt native — ví dụ từ nút bấm trong flow — hãy dùng [`adapty.openWebUrl`](capacitor-handle-paywall-actions#open-urls-from-flows-and-paywalls) thay thế. ::: ## Mở web paywall trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} :::important Mở web paywall trong trình duyệt trong ứng dụng được hỗ trợ từ Adapty SDK v3.15 trở lên. ::: Theo mặc định, web paywall sẽ mở trong trình duyệt bên ngoài. Để mang lại trải nghiệm liền mạch cho người dùng, bạn có thể mở web paywall trong trình duyệt tích hợp trong ứng dụng. Điều này hiển thị trang mua hàng ngay bên trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt `openIn` thành `WebPresentation.BrowserInApp` trong `openWebPaywall`: ```typescript showLineNumbers try { await adapty.openWebPaywall({ paywallOrProduct: product, openIn: WebPresentation.BrowserInApp, // default – WebPresentation.BrowserOutApp }); } catch (error) { console.error('Failed to open web paywall:', error); } ``` --- # File: capacitor-present-flows-in-observer-mode --- --- title: "Hiển thị flow trong chế độ Observer trong Capacitor SDK" description: "Hiển thị flow và paywall Paywall Builder trong chế độ Observer trong ứng dụng Capacitor của bạn trong khi xử lý giao dịch mua bằng code của riêng bạn." --- Nếu bạn đã tùy chỉnh flow hoặc paywall bằng builder, bạn không cần lo về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. :::warning Phần này chỉ áp dụng cho [Observer mode](observer-vs-full-mode). Nếu bạn không làm việc trong Observer mode, hãy xem chủ đề [Hiển thị flows & paywalls](capacitor-present-paywalls). ::: :::info Tính năng này yêu cầu Adapty Capacitor SDK 4.0 trở lên — trước đây chỉ có trong các SDK gốc iOS và Android. Xem [hướng dẫn migration](migration-to-capacitor-sdk-v4) để nâng cấp. ::: <details> <summary>Trước khi bắt đầu hiển thị flow (Nhấn để mở rộng)</summary> 1. Thiết lập tích hợp ban đầu của Adapty [với App Store](initial_ios) và [với Google Play](initial-android). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo [hướng dẫn cài đặt Capacitor SDK](sdk-installation-capacitor#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình flow hoặc paywall trong các builder](create-paywall) và gán sản phẩm cho chúng. 5. [Tạo placement và gán flow hoặc paywall vào chúng](create-placement). 6. [Lấy flow và cấu hình của chúng](capacitor-get-pb-paywalls) trong code ứng dụng di động của bạn. </details> Trong chế độ Observer, SDK không thực hiện giao dịch mua hàng thay bạn. Khi người dùng nhấn nút mua hoặc khôi phục trong một flow hoặc paywall do Adapty hiển thị, SDK sẽ gọi event handler `onObserverPurchaseInitiated` hoặc `onObserverRestoreInitiated` của bạn thay vào đó — hãy thực hiện giao dịch mua hoặc khôi phục bằng code của riêng bạn ở đó. 1. Thiết lập các event handler chế độ observer trên view. Không giống các nền tảng khác, không có đối tượng resolver riêng biệt — các handler là một phần của [event handlers](capacitor-handling-events) thông thường, vì vậy hãy thiết lập chúng trên mỗi view bạn tạo: ```typescript showLineNumbers title="Capacitor" import { adapty, createFlowView } from '@adapty/capacitor'; const view = await createFlowView(flow); const unsubscribe = await view.setEventHandlers({ onObserverPurchaseInitiated(product, onStartPurchase, onFinishPurchase) { onStartPurchase(); // the view shows its loading indicator myPurchaseApi(product.vendorProductId) .then((transactionId) => adapty.reportTransaction({ transactionId, variationId: flow.variationId }), ) .finally(() => onFinishPurchase()); // the view hides the loading indicator return false; // keep the flow open; dismiss it yourself after success }, onObserverRestoreInitiated(onStartRestore, onFinishRestore) { onStartRestore(); myRestoreApi().finally(() => onFinishRestore()); return false; }, }); ``` `onObserverPurchaseInitiated` thông báo cho bạn rằng người dùng đã bắt đầu thực hiện mua hàng, còn `onObserverRestoreInitiated` — rằng người dùng đã bắt đầu khôi phục. Hãy kích hoạt flow mua hàng hoặc khôi phục tùy chỉnh của bạn để phản hồi. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về quá trình mua hàng hoặc khôi phục. Điều này cần thiết để flow hoạt động đúng cách, chẳng hạn như hiển thị loader: | Callback | Mô tả | | :----------------- | :------------------------------------------------------------------------------------------- | | onStartPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã bắt đầu. | | onFinishPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã hoàn tất. | | onStartRestore() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã bắt đầu. | | onFinishRestore() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã hoàn tất. | 2. Trình bày flow như thường lệ: [lấy flow và tạo view của nó](capacitor-get-pb-paywalls), sau đó [hiển thị nó](capacitor-present-paywalls). Không cần thêm tham số nào — các handler chỉ được kích hoạt khi SDK được khởi động với `observerMode: true`. :::warning Đừng quên [báo cáo giao dịch và liên kết nó với paywall](report-transactions-observer-mode-capacitor). Nếu không, Adapty sẽ không nhận ra giao dịch và sẽ không xác định được paywall nguồn của giao dịch mua. ::: --- # File: capacitor-implement-paywalls-manually --- --- title: "Triển khai paywall thủ công" description: "Tìm hiểu cách triển khai paywall thủ công trong ứng dụng Capacitor của bạn với Adapty SDK." --- ## Xử lý giao dịch mua \{#accept-purchases\} Nếu bạn đang làm việc với các paywall do chính bạn triển khai, bạn có thể ủy quyền xử lý giao dịch mua cho Adapty thông qua phương thức `makePurchase`. Theo cách này, chúng tôi sẽ xử lý tất cả các tình huống của người dùng, và bạn chỉ cần xử lý kết quả giao dịch mua. :::important `makePurchase` hoạt động với các sản phẩm được tạo trong Adapty Dashboard. Hãy đảm bảo bạn đã cấu hình sản phẩm và cách lấy chúng trong dashboard bằng cách làm theo [hướng dẫn quickstart](quickstart). ::: <CustomDocCardList ids={['capacitor-quickstart-manual', 'fetch-paywalls-and-products-capacitor', 'present-remote-config-paywalls-capacitor', 'capacitor-making-purchases', 'capacitor-restore-purchase']} /> ## Chế độ Observer \{#observer-mode\} Nếu bạn muốn tự xây dựng logic xử lý giao dịch mua từ đầu nhưng vẫn muốn tận dụng tính năng phân tích nâng cao trong Adapty, bạn có thể sử dụng chế độ observer mode. :::important Xem xét các giới hạn của observer mode [tại đây](observer-vs-full-mode). ::: <CustomDocCardList ids={['implement-observer-mode-capacitor', 'report-transactions-observer-mode-capacitor']} /> --- # File: capacitor-quickstart-manual --- --- title: "Kích hoạt mua hàng trong paywall tùy chỉnh của bạn trên Capacitor SDK" description: "Tích hợp Adapty SDK vào các paywall Capacitor tùy chỉnh của bạn để kích hoạt in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Giữ toàn quyền kiểm soát việc triển khai paywall, trong khi Adapty SDK tự động lấy sản phẩm, xử lý các giao dịch mua mới và khôi phục các giao dịch trước đó. Hướng dẫn này sử dụng các API của Adapty Capacitor SDK v4 — nếu bạn đang dùng v3, hãy xem [hướng dẫn migration](migration-to-capacitor-sdk-v4) để biết tên các phương thức tương ứng. :::important **Hướng dẫn này dành cho các nhà phát triển đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để kích hoạt tính năng mua hàng, hãy sử dụng [Adapty Paywall Builder](capacitor-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall bằng trình chỉnh sửa trực quan không cần code, Adapty xử lý toàn bộ logic mua hàng tự động, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để bật tính năng in-app purchase, bạn cần nắm rõ ba khái niệm chính: - [**Sản phẩm**](product) – những gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – 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. Trong SDK v4, các biến thể paywall cho một placement được chứa trong đối tượng **flow** — bạn fetch một flow và truy vấn các sản phẩm của nó. - [**Placements**](placements) – nơi và thời điểm hiển thị paywall trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn cài đặt paywall cho các placement trên dashboard, sau đó yêu cầu chúng theo placement ID trong code. Điều này giúp dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho từng nhóm người dùng. Hãy đảm bảo bạn hiểu các khái niệm này ngay cả khi bạn tự xây dựng paywall theo cách của mình. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm vào một **placement**. Thiết lập này cho phép bạn lấy danh sách sản phẩm. Để hiểu những gì cần làm trên dashboard, hãy xem hướng dẫn quickstart [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc có hoặc không có xác thực backend từ phía mình. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo cách khác nhau. Đọc [hướng dẫn nhanh về xác định người dùng](capacitor-quickstart-identify) để hiểu rõ sự khác biệt và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy danh sách sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền ID [placement](placements) vào phương thức `getFlow`. 2. Lấy mảng sản phẩm cho flow này bằng phương thức `getPaywallProducts`. ```typescript showLineNumbers async function loadPaywall() { try { const flow: AdaptyFlow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID' }); const products: AdaptyPaywallProduct[] = await adapty.getPaywallProducts({ flow }); // Use products to build your custom paywall UI } catch (error) { // Handle the error } } ``` ## Bước 2. Xử lý 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ý flow thanh toán và trả về hồ sơ người dùng đã được cập nhật. ```typescript showLineNumbers async function purchaseProduct(product: AdaptyPaywallProduct) { try { const result: AdaptyPurchaseResult = await adapty.makePurchase({ product }); if (result.type === 'success') { // Purchase successful, profile updated } else if (result.type === 'user_cancelled') { // User canceled the purchase } else if (result.type === 'pending') { // Purchase is pending (e.g., user will pay offline with cash) } } catch (error) { // 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 có thể khôi phục giao dịch mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Thao tác này sẽ đồng bộ lịch sử giao dịch mua của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```typescript showLineNumbers async function restorePurchases() { try { const profile: AdaptyProfile = await adapty.restorePurchases(); // Restore successful, profile updated } catch (error) { // Handle the error } } ``` ## Bước 4. Kiểm tra trạng thái gói đăng ký \{#step-4-check-the-subscription-status\} Sau khi mua hàng hoặc khôi phục, hãy kiểm tra [mức độ truy cập](access-level) của người dùng để quyết định có hiển thị paywall hay mở khóa các tính năng trả phí. Các phương thức `makePurchase` và `restorePurchases` đã trả về hồ sơ người dùng được cập nhật; bất cứ khi nào bạn cần trạng thái hiện tại ở nơi khác trong ứng dụng, hãy sử dụng phương thức `getProfile`: ```typescript showLineNumbers async function hasPremiumAccess(): Promise<boolean> { try { const profile = await adapty.getProfile(); return profile.accessLevels?.['premium']?.isActive ?? false; } catch (error) { // Handle the error } return false; } ``` Để tìm hiểu thêm các cách kiểm tra và theo dõi trạng thái gói đăng ký, bao gồm cả việc lắng nghe cập nhật theo thời gian thực, xem [Kiểm tra trạng thái gói đăng ký](capacitor-check-subscription-status). ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra các giao dịch mua của bạn trong [sandbox App Store](test-purchases-in-sandbox) hoặc trong [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử nghiệm từ paywall. Để xem cách triển khai này hoạt động trong một ứng dụng thực tế, hãy tham khảo [App.tsx](https://github.com/adaptyteam/AdaptySDK-Capacitor/blob/master/examples/adapty-devtools/src/screens/app/App.tsx) 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 xử lý lỗi đúng cách, trạng thái tải, và tích hợp SDK toàn diện. --- # File: fetch-paywalls-and-products-capacitor --- --- title: "Lấy paywall và sản phẩm cho remote config paywalls trong Capacitor SDK" description: "Lấy paywall và sản phẩm trong Adapty Capacitor SDK để tối ưu hóa việc kiếm tiền từ người dùng." --- <SDKv4> Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng phần này đề cập đến Remote Config và paywall tùy chỉnh. Để biết hướng dẫn lấy flow hoặc paywall được tùy chỉnh trong **Flow Builder** hoặc **Paywall Builder**, vui lòng tham khảo [Lấy flow từ Flow Builder, paywall từ Paywall Builder và cấu hình của chúng](capacitor-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. ::: <details> <summary>Trước khi bắt đầu lấy flow và sản phẩm trong ứng dụng di động của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo một flow hoặc paywall và đưa các sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo các placement và đưa flow hoặc paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-capacitor) vào ứng dụng di động của bạn. </details> ## Lấy thông tin flow \{#fetch-flow-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các flow và paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy `AdaptyFlow` từ một trong các [placement](placements) của bạn bằng phương thức `getFlow`. :::important **Đừng hardcode product ID.** ID duy nhất bạn nên hardcode là placement ID. Flow được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay flow trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```typescript showLineNumbers try { const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); // the requested flow } catch (error) { console.error('Failed to fetch flow:', error); } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **params.fetchPolicy** | <p>tùy chọn</p><p>mặc định: `'reload_revalidating_cache_data'`</p> | <p>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.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn, bất kể kết nối internet có kém đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong suốt phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ flow và paywall theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](capacitor-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải flow và paywall nhanh hơn, cùng với 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 flow trong khi vẫn duy trì độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **params.loadTimeoutMs** | <p>tùy chọn</p><p>mặc định: 5000 ms</p> | <p>Giá trị này giới hạn thời gian chờ (tính bằng mili giây) 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ề.</p><p></p><p>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 hạn muộn hơn một chút so với giá trị được chỉ định trong `loadTimeoutMs`, do thao tác này có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | :::note Trong v4, `getFlow` không còn nhận tham số `locale` nữa. Đối với các paywall tùy chỉnh, tất cả các ngôn ngữ có sẵn đều được trả về trong Remote Config của flow (`flow.remoteConfigs`) — hãy chọn ngôn ngữ phù hợp với thiết bị hoặc cài đặt ứng dụng của người dùng. ::: Đừng hardcode ID sản phẩm! Vì các flow được cấu hình từ xa, các sản phẩm hiện có, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những trường hợp này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị đúng 2 sản phẩm đó. Nhưng nếu sau này lấy được 3 sản phẩm, ứng dụng phải hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn cần hardcode là placement ID. Tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, các biến thể paywall của nó (`paywalls`), và một mảng `remoteConfigs` (một mục cho mỗi locale đã cấu hình). Để lấy sản phẩm cho flow, gọi `getPaywallProducts({ flow })`. | ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có flow, bạn có thể lấy mảng sản phẩm tương ứng với nó: ```typescript showLineNumbers try { const products = await adapty.getPaywallProducts({ flow }); // the requested products list } catch (error) { console.error('Failed to fetch products:', error); } ``` Tham số trả về: | Tham số | Mô tả | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi triển khai thiết kế paywall tùy chỉnh, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct). Dưới đây là các thuộc tính được sử dụng phổ biến nhất — tham khảo tài liệu được liên kết để xem đầy đủ 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 việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Price** | Để hiển thị giá đã bản địa hóa, dùng `product.price?.localizedString`. Việc bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể lấy giá dưới dạng số bằng `product.price?.amount`. Giá trị sẽ được tính theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price?.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedSubscriptionPeriod`. Việc bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo lập trình, dùng `product.subscription?.subscriptionPeriod`. Từ đó bạn có thể truy cập thuộc tính `unit` để lấy đơn vị thời gian (tức là `'day'`, `'week'`, `'month'`, `'year'`, hoặc `'unknown'`). Giá trị `numberOfUnits` sẽ cho bạn biết số đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, thuộc tính `unit` sẽ là `'month'` và `numberOfUnits` sẽ là `3`. | | **Introductory Offer** | Để hiển thị huy hiệu hoặc chỉ báo cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `paymentMode`: chuỗi với các giá trị `'free_trial'`, `'pay_as_you_go'`, `'pay_up_front'`, và `'unknown'`. Giai đoạn dùng thử miễn phí sẽ có kiểu `'free_trial'`.<br/>• `price`: Giá giảm dưới dạng số. Với giai đoạn dùng thử miễn phí, giá trị này là `0`.<br/>• `localizedNumberOfPeriods`: chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày sẽ hiển thị `'3 days'` trong trường này.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết của chu kỳ ưu đãi bằng thuộc tính này. Cách dùng tương tự như phần trước đã mô tả.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký giảm giá đã được định dạng theo ngôn ngữ của người dùng. | ## Tăng tốc độ tải flow với flow đối tượng mặc định \{#speed-up-flow-fetching-with-default-audience-flow\} Thông thường, flow được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị vẫn là lấy flow bằng phương thức `getFlow`, như đã trình bày trong phần [Lấy thông tin flow](fetch-paywalls-and-products-capacitor#fetch-flow-information) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng đang dùng phiên bản đó có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa nhắm mục tiêu (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` được mô tả [ở trên](fetch-paywalls-and-products-capacitor#fetch-flow-information). ::: ```typescript showLineNumbers try { const flow = await adapty.getFlowForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', params: { fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache } }); // the requested flow } catch (error) { console.error('Failed to fetch default audience flow:', error); } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **params.fetchPolicy** | <p>tùy chọn</p><p>mặc định: `'reload_revalidating_cache_data'`</p> | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyên dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để 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 mạng có kém đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p> | </SDKv4> <SDKv3> Trước khi hiển thị Remote Config và paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng phần này đề cập đến Remote Config và paywall tùy chỉnh. Để biết hướng dẫn lấy paywall cho các paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo [Lấy paywall từ Paywall Builder và cấu hình của chúng](capacitor-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. ::: <details> <summary>Trước khi bắt đầu lấy paywall và sản phẩm trong ứng dụng di động của bạn (nhấp để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall](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-capacitor) vào ứng dụng di động của bạn. </details> ## 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. Những sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của mình bằng phương thức `getPaywall`. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); // the requested paywall } catch (error) { console.error('Failed to fetch paywall:', error); } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều subtag được phân cách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là mã ngôn ngữ, subtag thứ hai là mã vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Bản địa hóa và mã locale](capacitor-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.</p> | | **params.fetchPolicy** | <p>tùy chọn</p><p>mặc định: `'reload_revalidating_cache_data'`</p> | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn dù kết nối internet kém. Cache được cập nhật thường xuyên, nên việc dùng cache trong phiên làm việc để tránh các yêu cầu mạng là hoàn toàn an toàn.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p> | | **params.loadTimeoutMs** | <p>tùy chọn</p><p>mặc định: 5000 ms</p> | <p>Giá trị này giới hạn thời gian chờ (tính bằng mili giây) 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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeoutMs`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | **Đừng hardcode product ID.** ID duy nhất bạn nên hardcode là placement ID. Paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt — nếu hôm nay paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) gồm: danh sách ID sản phẩm, mã định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có paywall, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```typescript showLineNumbers try { const products = await adapty.getPaywallProducts({ paywall }); // the requested products list } catch (error) { console.error('Failed to fetch products:', error); } ``` Tham số trả về: | Tham số | Mô tả | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) với: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi triển khai thiết kế paywall của riêng bạn, bạn có thể cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct). 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 thông tin đầy đủ về tất cả các thuộc tính hiện có. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | Để hiển thị tiêu đề sản phẩm, sử dụng `product.localizedTitle`. Lưu ý rằng việc bản địa hóa dựa trên quốc gia của 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, sử dụng `product.price?.localizedString`. Việc 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, sử dụng `product.price?.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), sử dụng `product.subscription?.localizedSubscriptionPeriod`. Việc 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, sử dụng `product.subscription?.subscriptionPeriod`. Từ đó bạn có thể truy cập thuộc tính `unit` để lấy đơn vị thời gian (tức là `'day'`, `'week'`, `'month'`, `'year'`, hoặc `'unknown'`). Giá trị `numberOfUnits` sẽ cho bạn biết số đơn vị trong chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `'month'` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. | | **Introductory Offer** | Để hiển thị huy hiệu hoặc chỉ báo khác cho thấy gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là 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 giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `paymentMode`: một chuỗi với các giá trị `'free_trial'`, `'pay_as_you_go'`, `'pay_up_front'`, và `'unknown'`. Dùng thử miễn phí sẽ có kiểu `'free_trial'`.<br/>• `price`: Giá ưu đãi dưới dạng số. Với dùng thử miễn phí, giá trị này là `0`.<br/>• `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.<br/>• `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. Cách hoạt động tương tự như đã mô tả ở phần trước.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký được định dạng theo locale của người dùng cho khoản giảm giá. | ## Tăng tốc tải paywall với paywall cho đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng của bạn có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống đó, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà, thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ lấy paywall của placement được chỉ định dành cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị 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-capacitor#fetch-paywall-information) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp phải những thách thức. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (legacy) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (legacy) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng 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**, điều này có nghĩa là bạn mất đi khả năng targeting cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như hướng dẫn dưới đây. Nếu không, hãy tiếp tục dùng `getPaywall` đã được mô tả [ở trên](fetch-paywalls-and-products-capacitor#fetch-paywall-information). ::: ```typescript showLineNumbers try { const paywall = await adapty.getPaywallForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache } }); // the requested paywall } catch (error) { console.error('Failed to fetch default audience paywall:', error); } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Bản địa hóa và mã locale](capacitor-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.</p> | | **params.fetchPolicy** | <p>tùy chọn</p><p>mặc định: `'reload_revalidating_cache_data'`</p> | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp vấn đề với kết nối internet không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn bất kể kết nối internet có kém đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | </SDKv3> --- # File: present-remote-config-paywalls-capacitor --- --- title: "Hiển thị paywall được thiết kế bằng Remote Config trong Capacitor SDK" description: "Khám phá cách hiển thị paywall Remote Config trong Adapty Capacitor SDK để cá nhân hóa trải nghiệm người dùng." --- <SDKv4> Nếu bạn đã tùy chỉnh một flow bằng Remote Config, bạn cần triển khai phần hiển thị trong code của ứng dụng di động để hiển thị nó cho người dùng. Vì Remote Config cung cấp sự linh hoạt phù hợp với nhu cầu của bạn, bạn hoàn toàn kiểm soát những gì được đưa vào và giao diện flow của bạn trông như thế nào. Chúng tôi cung cấp một phương thức để lấy cấu hình remote, giúp bạn tự do hiển thị flow tùy chỉnh đã được cấu hình qua Remote Config. ## Lấy remote config của flow và hiển thị nó \{#get-flow-remote-config-and-present-it\} Trong v4, một flow mang một mục `AdaptyRemoteConfig` cho mỗi ngôn ngữ được cấu hình trong mảng `remoteConfigs`. Chọn ngôn ngữ phù hợp với tùy chọn của người dùng, sau đó đọc các giá trị bạn cần từ `data` của nó. ```typescript showLineNumbers try { const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID' }); const config = flow.remoteConfigs?.find((c) => c.lang === 'en') ?? flow.remoteConfigs?.[0]; const headerText = config?.data?.['header_text']; } catch (error) { console.error('Failed to fetch flow:', error); } ``` Đến đây, sau khi đã nhận được tất cả các giá trị cần thiết, bạn có thể render và kết hợp chúng thành một trang có giao diện hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng hiển thị khác nhau trên các thiết bị di động, mang lại trải nghiệm mượt mà và thân thiện với người dùng. :::warning Hãy đảm bảo [ghi lại sự kiện xem paywall](present-remote-config-paywalls-capacitor#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ị flow xong, hãy tiếp tục thiết lập luồng mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ flow của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](capacitor-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](capacitor-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 flow. Trong khi chúng tôi tự động thu thập dữ liệu về các giao dịch mua, việc ghi lại lượt xem flow cần có sự hỗ trợ từ phía bạn vì chỉ bạn mới biết khi nào người dùng thấy một flow. Để ghi lại sự kiện xem flow, chỉ cần gọi `.logShowFlow({ flow })`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong phần funnels và A/B test. :::important Không cần gọi `.logShowFlow({ flow })` nếu bạn đang hiển thị các flow hoặc paywall được dựng bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder). Adapty tự động theo dõi lượt xem trong những trường hợp đó. ::: ```typescript showLineNumbers await adapty.logShowFlow({ flow }); ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :-------- | :------- |:-------------------------------------------------------------------------------------| | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `adapty.getFlow({ placementId })`. | </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng thấy được paywall đó. Vì Remote Config cho phép linh hoạt theo nhu cầu của bạn, bạn hoàn toàn chủ động trong việc 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, truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); const headerText = paywall.remoteConfig?.data?.['header_text']; } catch (error) { console.error('Failed to fetch paywall:', error); } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và kết hợp chúng thành một trang trông hấp dẫn. Hãy đảm bảo rằng thiết kế tương thích với nhiều loại màn hình điện thoại và các hướng xoay khác nhau, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Đảm bảo [ghi lại sự kiện xem paywall](present-remote-config-paywalls-capacitor#track-paywall-view-events-1) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](capacitor-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](capacitor-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 hiển thị paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi dữ liệu mua hàng được thu thập tự động, việc ghi lại lượt xem paywall cần bạn thực hiện thủ công vì chỉ bạn mới biết khi nào khách hàng thấy một paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)`, và sự kiện này sẽ được phản ánh trong các chỉ số paywall của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị các paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```typescript showLineNumbers try { await adapty.logShowPaywall({ paywall }); } catch (error) { console.error('Failed to log paywall view:', error); } ``` 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://capacitor.adapty.io/interfaces/adaptypaywall). | </SDKv3> --- # File: capacitor-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng với Capacitor SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng di động là bước thiết yếu để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ hiển thị paywall thôi là đủ để hỗ trợ thanh toán nếu bạn sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall của mình. Nếu bạn không dùng Paywall Builder, bạn cần sử dụng một phương thức riêng là `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung cho người dùng. Phương thức này đóng vai trò cầu nối giúp người dùng tương tác với các paywall và thực hiện các giao dịch mong 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 muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm thanh toán. Hãy đảm bảo bạn đã [thực hiện cấu hình ban đầu](quickstart) mà không bỏ qua bất kỳ bước nào. Nếu không có bước đó, 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)?** Giao dịch mua hàng được xử lý tự động — bạn có thể bỏ qua bước này. **Cần hướng dẫn từng bước?** Xem [hướng dẫn quickstart](capacitor-implement-paywalls-manually) để có hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product }); if (result.type === 'success') { const isSubscribed = result.profile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features console.log('User is now subscribed!'); } } else if (result.type === 'user_cancelled') { console.log('Purchase cancelled by user'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { console.error('Purchase failed:', error); } ``` | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------| | **product** | bắt buộc | Một đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) lấy từ flow thông qua `getPaywallProducts`. | Tham số phản hồi: | Tham số | Mô tả | |---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **result** | Một đối tượng [`AdaptyPurchaseResult`](https://capacitor.adapty.io/types/adaptypurchaseresult) với trường `type` cho biết kết quả giao dịch (`'success'`, `'user_cancelled'`, hoặc `'pending'`) và trường `profile` chứa [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile) đã được cập nhật khi mua hàng thành công. | ## Thay đổi gói đăng ký khi thực hiện 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 hoạt động sẽ khác nhau tùy theo cửa hàng: - Đối với App Store, gói đăng ký được tự động cập nhật trong nhóm gói đăng ký. Nếu người dùng mua một gói đăng ký từ nhóm này trong khi đã có gói đăng ký từ nhóm khác, cả hai gói sẽ hoạt động cùng lúc. - Đối với Google Play, gói đăng ký không được tự động cập nhật. Bạn cần xử lý việc chuyển đổi trong code ứng dụng của mình 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: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product, params: { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } } }); if (result.type === 'success') { const isSubscribed = result.profile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features console.log('Subscription updated successfully!'); } } else if (result.type === 'user_cancelled') { console.log('Purchase cancelled by user'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { console.error('Purchase failed:', error); } ``` Tham số yêu cầu bổ sung: | Tham số | Bắt buộc | Mô tả | | :--------- | :------- | :----------------------------------------------------------- | | **params** | tùy chọn | Một object kiểu [`MakePurchaseParamsInput`](https://capacitor.adapty.io/types/makepurchaseparamsinput) chứa các tham số mua hàng theo từng nền tảng. | Cấu trúc `MakePurchaseParamsInput` bao gồm: ```typescript { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } } ``` 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 khi nâng cấp gói đăng ký. Hạ cấp không được hỗ trợ. - Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Việc thay đổi gói đăng ký thực sự chỉ diễn ra khi chu kỳ thanh toán hiện tại kết thúc. ### 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 cho vài tháng), bạn có thể bật [giao dịch đang chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { pendingPrepaidPlansEnabled: true, }, } }); ``` ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} <Details> <summary>Về offer code</summary> Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} :::warning Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. ::: Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác. </Details> Để hiển thị giao diện nhập mã trong ứng dụng của bạn: ```typescript showLineNumbers try { await adapty.presentCodeRedemptionSheet(); } catch (error) { console.error('Failed to present code redemption sheet:', error); } ``` :::danger Theo quan sát của chúng tôi, giao diện nhập mã ưu đãi trong một số ứng dụng có thể hoạt động không ổn định. Chúng tôi khuyến nghị bạn chuyển hướng người dùng trực tiếp đến App Store. Để làm điều này, bạn cần mở URL theo định dạng sau: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: capacitor-restore-purchase --- --- title: "Khôi phục giao dịch trong ứng dụng di động với Capacitor SDK" description: "Tìm hiểu cách khôi phục giao dịch trong Adapty để đảm bảo trải nghiệm người dùng liền mạch." --- Restoring Purchases trong cả iOS và Android là tính năng cho phép người dùng khôi phục 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 dùng đã gỡ cài đặt và cài đặt lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập nội dung đã mua trước đó mà không cần phải trả tiền lại. :::note Trong các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), các giao dịch mua sẽ được khôi phục tự động mà không cần thêm code từ bạn. Nếu đó là trường hợp của bạn — bạn có thể bỏ qua bước này. ::: Để khôi phục giao dịch mua nếu 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()`: ```typescript showLineNumbers try { const profile = await adapty.restorePurchases(); const isSubscribed = profile.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Restore access to paid features console.log('Access restored successfully!'); } else { console.log('No active subscriptions found'); } } catch (error) { console.error('Failed to restore purchases:', error); } ``` Tham số phản hồi: | Tham số | Mô tả | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **profile** | Một đối tượng [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile). Model này chứa thông tin về các 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. | --- # File: implement-observer-mode-capacitor --- --- title: "Triển khai Observer mode trong Capacitor SDK" description: "Triển khai observer mode trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong Capacitor SDK." --- Nếu bạn đã có hệ thống mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp phân tích nâng cao và tích hợp liền mạch với các hệ thống attribution và phân tích. Nếu điều này phù hợp với nhu cầu của bạn, bạn chỉ cần: 1. Bật tính năng này khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. Làm theo hướng dẫn cài đặt cho [Capacitor](sdk-installation-capacitor#activate-adapty-module-of-adapty-sdk). 2. [Báo cáo giao dịch](report-transactions-observer-mode-capacitor) từ cơ sở hạ tầng mua hàng hiện có của bạn lên Adapty. :::tip Trong SDK v4, bạn cũng có thể hiển thị các flow và paywall do Adapty render trong Observer mode: khi người dùng nhấn vào nút mua hàng hoặc khôi phục, SDK sẽ chuyển hành động đó sang code của bạn để bạn tự thực hiện việc mua hàng hoặc khôi phục. Xem [Hiển thị flow trong Observer mode](capacitor-present-flows-in-observer-mode). ::: ### 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 chỉ 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 đó. ::: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { observerMode: true // Enable observer mode } }); } catch (error) { console.error('Failed to activate Adapty:', error); } ``` | Tham số | Mô tả | | --- | --- | | **observerMode** | Giá trị boolean kiểm soát [Observer mode](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thêm một số cài đặt trong Observer mode. Đây là những gì bạn cần thực hiện ngoài các bước trên: 1. Hiển thị paywall như bình thường cho [remote config paywalls](present-remote-config-paywalls-capacitor). 2. [Liên kết paywall](report-transactions-observer-mode-capacitor) với các giao dịch mua hàng. --- # File: report-transactions-observer-mode-capacitor --- --- title: "Báo cáo giao dịch trong Observer Mode trong Capacitor SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trong Capacitor SDK." --- Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng được thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng của mình. Việc thiết lập điều này **trước** khi phát hành ứng dụng là rất quan trọ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 được. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận ra giao dịch đó, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy đính kèm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```typescript showLineNumbers const variationId = paywall.variationId; try { await adapty.reportTransaction({ transactionId: 'your_transaction_id', variationId: variationId }); } catch (error) { console.error('Failed to report transaction:', error); } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | **transactionId** | bắt buộc | <ul><li> Đối với iOS: Mã định danh của giao dịch.</li><li> Đối với 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 billing library.</li></ul> | | **variationId** | tùy chọn | Mã định danh dạng chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://capacitor.adapty.io/interfaces/adaptypaywall). | --- # File: capacitor-user --- --- title: "Người dùng & quyền truy cập" description: "Tìm hiểu cách làm việc với người dùng và mức độ truy cập trong ứng dụng Capacitor của bạn với Adapty SDK." --- <CustomDocCardList /> --- # File: capacitor-identifying-users --- --- title: "Xác định người dùng trong Capacitor SDK" description: "Tìm hiểu cách xác định người dùng trong ứng dụng Capacitor của bạn với Adapty SDK." --- 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 thiết lập Customer User ID của chính 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), ID này sẽ được gửi đến tất cả các tích hợp. ### Thiết lập customer user ID khi cấu hình \{#setting-customer-user-id-on-configuration\} Nếu bạn đã có user ID trong quá trình cấu hình, chỉ cần truyền nó dưới dạng tham số `customerUserId` vào phương thức `.activate()`: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { customerUserId: 'YOUR_USER_ID' } }); } catch (error) { console.error('Failed to activate Adapty:', error); } ``` ### Thiết lập 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ể thiết lập nó sau bất kỳ lúc nào bằng phương thức `.identify()`. Trường hợp phổ biến nhất để 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ừ trạng thái ẩn danh sang người dùng đã xác thực. ```typescript showLineNumbers try { await adapty.identify({ customerUserId: 'YOUR_USER_ID' }); console.log('User identified successfully'); } catch (error) { console.error('Failed to identify user:', error); } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **customerUserId** | bắt buộc | Chuỗi định danh người dùng. | :::warning Gửi lại dữ liệu quan trọng của người dù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ủ của 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ư các thuộc tính tùy chỉnh hoặc attribution từ các mạng bên thứ ba, bạn nên gửi lại dữ liệu đó cho người dùng đã xác định. Điều quan trọng cần lưu ý là bạn nên yêu cầu 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 kỳ lúc nào bằng cách gọi phương thức `.logout()`: ```typescript showLineNumbers try { await adapty.logout(); console.log('User logged out successfully'); } catch (error) { console.error('Failed to logout user:', 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 bạn. StoreKit gắn token này với mọi giao dịch, nhờ đó backend của bạn có thể khớp dữ liệu App Store với người dùng của bạn. Hãy sử dụng một UUID ổn định được tạo cho mỗi người dùng và tái sử dụng nó cho cùng một tài khoản trên các thiết bị. Điều này đảm bảo rằng các giao dịch mua và thông báo App Store được liên kết chính xác. Bạn có thể thiết lập 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. ::: ```typescript showLineNumbers // During configuration: await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { customerUserId: 'YOUR_USER_ID', ios: { appAccountToken: "YOUR_APP_ACCOUNT_TOKEN" }, } }); // Or when identifying users await adapty.identify({ customerUserId: 'YOUR_USER_ID', params: { ios: { appAccountToken: 'YOUR_APP_ACCOUNT_TOKEN' }, } }); ``` ### Thiết lập 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ất định nhằm tăng cường quyền riêng tư và bảo mật cho 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 để ngăn chặn gian lận và phân tích. Bạn có thể cần thiết lập 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 phải tuân thủ các quy định về quyền riêng tư cụ thể. Các ID obfuscated 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ế. ```typescript showLineNumbers // During configuration: await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' }, } }); // Or when identifying users await adapty.identify({ customerUserId: 'YOUR_USER_ID', params: { android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' }, } }); ``` ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: capacitor-setting-user-attributes --- --- title: "Đặt thuộc tính người dùng trong Capacitor SDK" description: "Tìm hiểu cách cập nhật thuộc tính người dùng và dữ liệu hồ sơ trong ứng dụng Capacitor của bạn với Adapty SDK." --- Bạn có thể đặt 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 của mình. Sau đó, bạn có thể sử dụng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM. ### Đặt thuộc tính người dùng \{#setting-user-attributes\} Để đặt thuộc tính người dùng, gọi phương thức `.updateProfile()`: ```typescript showLineNumbers const params = { email: 'email@email.com', phoneNumber: '+18888888888', firstName: 'John', lastName: 'Appleseed', gender: 'other', birthday: new Date().toISOString(), }; try { await adapty.updateProfile(params); console.log('Profile updated successfully'); } catch (error) { console.error('Failed to update profile:', error); } ``` Lưu ý rằng các thuộc tính bạn đã đặt trước đó bằng phương thức `updateProfile` sẽ không bị reset. :::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` và giá trị tương ứng được liệt kê bên dưới: | Key | Giá trị | |---|-----| | **email** | String | | **phoneNumber** | String | | **firstName** | String | | **lastName** | String | | **gender** | Enum, các giá trị được phép: `'female'`, `'male'`, `'other'` | | **birthday** | Chuỗi ngày tháng theo định dạng ISO | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể đặt 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 ngôn ngữ, đó có thể 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 sử dụng trong analytics để xác định chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```typescript showLineNumbers try { await adapty.updateProfile({ codableCustomAttributes: { key_1: 'value_1', key_2: 2, }, }); console.log('Custom attributes updated successfully'); } catch (error) { console.error('Failed to update custom attributes:', error); } ``` Để xóa các key hiện có, truyền `null` làm giá trị của chúng: ```typescript showLineNumbers try { // to remove keys, pass null as their values await adapty.updateProfile({ codableCustomAttributes: { key_1: null, key_2: null, }, }); console.log('Custom attributes removed successfully'); } catch (error) { console.error('Failed to remove custom attributes:', error); } ``` Đô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 sử dụng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không còn mới nhất, vì thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau vào bất kỳ lúc nào, nên các thuộc tính trên server có thể đã thay đổi sau lần đồng bộ cuối cùng. ::: ### Giới hạn \{#limits\} - Tối đa 30 thuộc tính tùy chỉnh mỗi người dùng - Tên key tối đa 30 ký tự. Tên key có thể bao gồm 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: capacitor-listen-subscription-changes --- --- title: "Kiểm tra trạng thái gói đăng ký trong Capacitor SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng cho ứng dụng Capacitor của bạn." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải chèn thủ công các product ID vào code. Thay vào đó, bạn có thể dễ dàng xác nhận trạng thái gói đăng ký của người dùng bằng cách kiểm tra [mức độ truy cập](access-level) đang hoạt động. <details> <summary>Trước khi bắt đầu kiểm tra trạng thái gói đăng ký (Nhấn để mở rộng)</summary> - 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) </details> ## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là các thuộc tính của đối tượng [AdaptyProfile](https://capacitor.adapty.io/interfaces/adaptyprofile). Chúng tôi khuyến nghị bạn lấy hồ sơ người dùng ngay khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](capacitor-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ể sử dụng đối tượng hồ sơ người dùng mà không cần gửi yêu cầu lặp đi lặp lại. Để nhận thông báo khi hồ sơ người dùng có thay đổi, hãy lắng nghe các 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](capacitor-listen-subscription-changes) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ máy chủ \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ máy chủ, sử dụng phương thức `.getProfile()`: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); console.log('Profile retrieved successfully'); } catch (error) { console.error('Failed to get profile:', error); } ``` Tham số phản hồi: | Tham số | Mô tả | | --------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **profile** | Một đối tượng [AdaptyProfile](https://capacitor.adapty.io/interfaces/adaptyprofile). 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 trả về kết quả mới nhất vì nó luôn cố gắng truy vấn API. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ máy chủ, dữ liệu từ bộ nhớ cache sẽ được trả về. Cũng cần lưu ý rằng Adapty SDK thường xuyên cập nhật cache `AdaptyProfile` để giữ thông tin này luôn được cập nhật nhất có thể. | Phương thức `.getProfile()` cung cấp cho bạn hồ sơ người dùng, từ đó bạn có thể kiểm tra trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Ví dụ, nếu bạn có ứng dụng báo và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, hầu hết các trường hợp bạn chỉ cần một mức độ truy cập — khi đó, bạn có thể dùng mức độ truy cập mặc định là "premium". Dưới đây là ví dụ kiểm tra mức độ truy cập mặc định "premium": ```typescript showLineNumbers try { const profile = await adapty.getProfile(); const isActive = profile.accessLevels?.['premium']?.isActive; if (isActive) { // Grant access to premium features console.log('User has premium access'); } else { console.log('User does not have premium access'); } } catch (error) { console.error('Failed to check subscription status:', error); } ``` ### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\} Bất cứ khi nào 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: ```typescript showLineNumbers // Create an "onLatestProfileLoad" event listener adapty.addListener('onLatestProfileLoad', (data) => { const profile = data.profile; const isActive = profile.accessLevels?.['premium']?.isActive; if (isActive) { console.log('Subscription status updated: User has premium access'); } else { console.log('Subscription status updated: User does not have premium access'); } }); ``` Adapty cũng kích hoạt một sự kiện khi ứng dụng khởi động. Trong trường hợp này, trạng thái gói đăng ký được lưu trong cache sẽ được truyền vào. ### Bộ nhớ đệm trạng thái gói đăng ký \{#subscription-status-cache\} Bộ nhớ đệm được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi máy chủ không khả dụng, dữ liệu đã được lưu trong bộ nhớ đệm vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ người dùng. Tuy nhiên, cần lưu ý rằng không thể truy vấn dữ liệu trực tiếp từ cache. SDK định kỳ gửi yêu cầu đến máy chủ mỗi phút một lần để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ người dùng. Nếu có bất kỳ thay đổi nào, chẳng hạn như giao dịch mới hoặc các cập nhật khác, chúng sẽ được đồng bộ vào dữ liệu trong cache để giữ cho cache luôn nhất quán với máy chủ. --- # File: capacitor-deal-with-att --- --- title: "Xử lý ATT trong Capacitor SDK" description: "Bắt đầu với Adapty trên Capacitor để đơ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 cấp phép theo dõi ứng dụng cho người dùng, bạn cần gửi [trạng thái cấp phép](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) đến Adapty. ```typescript showLineNumbers try { await adapty.updateProfile({ appTrackingTransparencyStatus: AppTrackingTransparencyStatus.Authorized, }); console.log('ATT status updated successfully'); } catch (error) { console.error('Failed to update ATT status:', 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: capacitor-onboardings --- --- title: "Onboardings" description: "Tìm hiểu cách làm việc với onboardings trong ứng dụng Capacitor của bạn với Adapty SDK." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Tính năng này không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](capacitor-get-pb-paywalls) thay thế: khác với onboardings chạy trong WebView, flows được render trực tiếp trên thiết bị — mang lại hiệu ứng chuyển động mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](capacitor-get-pb-paywalls) và [Hiển thị flows & paywalls](capacitor-present-paywalls) để bắt đầu. ::: <CustomDocCardList /> --- # File: capacitor-get-onboardings --- --- title: "Lấy onboarding trong Capacitor SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho Capacitor." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn được sửa lỗi hay cải thiện nữa. Hãy sử dụng [flows](capacitor-get-pb-paywalls) thay thế: không giống như onboarding chạy trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](capacitor-get-pb-paywalls) và [Hiển thị flows & paywalls](capacitor-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding của mình](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Capacitor của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement và cấu hình view của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã [tạo một onboarding](create-onboarding). 2. 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 trữ dưới dạng một container chứa cấu hình mà ứng dụng cần lấy về và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung hiển thị, cách trình bày, cũng như cách xử lý các tương tác của người dùng (như câu trả lời quiz hay dữ liệu nhập vào form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần tự triển khai tính năng theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để hình ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy onboarding, hãy dùng phương thức `getOnboarding`: ```typescript showLineNumbers try { const onboarding = await adapty.getOnboarding({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); console.log('Onboarding fetched successfully'); } catch (error) { console.error('Failed to fetch onboarding:', error); } ``` Sau đó, gọi phương thức `createOnboardingView` để tạo một view instance. :::warning Kết quả của phương thức `createOnboardingView` chỉ có thể được sử dụng một lần. Nếu bạn cần sử dụng lại, hãy gọi lại phương thức `createOnboardingView` từ đầu. ::: ```typescript showLineNumbers if (onboarding.hasViewConfiguration) { try { const view = await createOnboardingView(onboarding); console.log('Onboarding view created successfully'); } catch (error) { console.error('Failed to create onboarding view:', error); } } else { // Use your custom logic console.log('Onboarding does not have view configuration'); } ``` Các 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh bản địa hóa của onboarding. Tham số này phải là mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân tách bằng dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ phụ thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>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.</p> | | **params.fetchPolicy** | <p>tùy chọn</p><p>mặc định: `'reload_revalidating_cache_data'`</p> | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã được lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để 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 hoàn toàn mới nhất, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet của họ ra sao. Cache được cập nhật thường xuyên, nên có thể dùng an toàn trong phiên để tránh các yêu cầu mạng.</p><p></p><p>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 cài đặt lại ứng dụng hoặc khi xóa thủ công.</p> | | **params.loadTimeoutMs** | <p>tùy chọn</p><p>mặc định: 5000 ms</p> | <p>Giá trị này giới hạn thời gian chờ (tính bằng mili giây) 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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeoutMs`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | ## Tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding** | Một đối tượng [`AdaptyOnboarding`](https://capacitor.adapty.io/interfaces/adaptyonboarding) 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 độ tải onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong những trường hợp bạn có nhiều đối tượng và onboarding, và người dùng có kết nối internet yếu, việc tải onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống đó, bạn có thể muốn hiển thị onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement đã chỉ định cho đối tượng **All Users**. Tuy nhiên, cần lưu ý rằng cách được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như đã mô 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 app, đòi hỏi thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ 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ỏ khả năng nhắm mục tiêu theo quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ tải nhanh hơn quan trọng hơn những hạn chế này đối với trường hợp sử dụng của bạn, hãy dùng `getOnboardingForDefaultAudience` như minh họa bên dưới. Ngược lại, hãy dùng `getOnboarding` như đã mô tả [ở trên](#fetch-onboarding). ::: ```typescript showLineNumbers try { const onboarding = await adapty.getOnboardingForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache } }); console.log('Default audience onboarding fetched successfully'); } catch (error) { console.error('Failed to fetch default audience onboarding:', error); } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh bản địa hóa của onboarding. Tham số này phải là mã ngôn ngữ gồm một hoặc hai thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>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.</p> | | **params.fetchPolicy** | <p>tùy chọn</p><p>mặc định: `'reload_revalidating_cache_data'`</p> | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình hay gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng tốc độ tải sẽ nhanh hơn bất kể kết nối mạng của họ ra sao. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p> | --- # File: capacitor-present-onboardings --- --- title: "Hiển thị onboarding trong Capacitor SDK" description: "Khám phá cách hiển thị onboarding trên Capacitor để tăng tỷ lệ chuyển đổi và doanh thu." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flow](capacitor-get-pb-paywalls) thay thế: không giống như onboarding chạy trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](capacitor-get-pb-paywalls) và [Hiển thị flow & paywall](capacitor-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Onboarding đó đã bao gồm cả nội dung cần hiển thị lẫn cách thức hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã [tạo một onboarding](create-onboarding). 2. Bạn đã thêm onboarding vào một [placement](placements). ## Hiển thị onboarding \{#present-onboarding\} Để hiển thị onboarding, dùng phương thức `view.present()` trên `view` được tạo bởi phương thức `createOnboardingView`. Mỗi `view` chỉ có thể dùng một lần. Nếu cần hiển thị lại onboarding, hãy gọi `createOnboardingView` 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. ::: ```typescript showLineNumbers try { const view = await createOnboardingView(onboarding); view.setEventHandlers({ onClose: (actionId, meta) => { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, onCustom: (actionId, meta) => { console.log('Custom action:', actionId); return false; // Don't close the onboarding } }); await view.present(); console.log('Onboarding presented successfully'); } catch (error) { console.error('Failed to present onboarding:', error); } ``` ## Cấu hình kiểu trình bày 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 các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. ```typescript showLineNumbers await view.present({ iosPresentationStyle: 'page_sheet' }); ``` ## 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 trở lên. ::: Mặc định, các liên kết trong onboarding sẽ mở bằng trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị trang web ngay trong ứng dụng, không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết bằng trình duyệt bên ngoài, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `openIn` thành `browser_out_app`: ```typescript showLineNumbers await view.present({ openIn: 'browser_out_app' }); // default — browser_in_app ``` ## Các bước tiếp theo \{#next-steps\} Sau khi hiển thị onboarding, bạn sẽ muốn [xử lý các tương tác và sự kiện của người dùng](capacitor-handling-onboarding-events). Tìm hiểu cách xử lý các sự kiện onboarding để phản hồi hành động của người dùng và theo dõi analytics. --- # File: capacitor-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong Capacitor SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong Capacitor bằng Adapty." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị loại bỏ trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](capacitor-get-pb-paywalls) thay thế: khác với onboarding chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](capacitor-get-pb-paywalls) và [Hiển thị flows & paywalls](capacitor-present-paywalls) để bắt đầu. ::: Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể phản hồi. Sử dụng phương thức `setEventHandlers` để xử lý các sự kiện này khi hiển thị màn hình độc lập. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã [tạo một onboarding](create-onboarding). 2. Bạn đã thêm onboarding vào một [placement](placements). ## Thiết lập trình xử lý sự kiện \{#set-up-event-handlers\} Để xử lý các sự kiện cho onboarding, sử dụng phương thức `view.setEventHandlers`: ```typescript showLineNumbers try { const view = await createOnboardingView(onboarding); view.setEventHandlers({ onAnalytics(event, meta) { console.log('Analytics event:', event); }, onClose(actionId, meta) { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, onCustom(actionId, meta) { console.log('Custom action:', actionId); return false; // Don't close the onboarding }, onPaywall(actionId, meta) { console.log('Paywall action:', actionId); view.dismiss().then(() => { openPaywall(actionId); }); }, onStateUpdated(action, meta) { console.log('State updated:', action); }, onFinishedLoading(meta) { console.log('Onboarding finished loading'); }, onError(error) { console.error('Onboarding error:', error); }, }); await view.present(); } catch (error) { console.error('Failed to present onboarding:', error); } ``` ## Các loại sự kiện \{#event-types\} Các phần dưới đây mô tả các loại sự kiện khác nhau mà bạn có thể xử lý. ### Xử lý custom action \{#handle-custom-actions\} Trong builder, bạn có thể thêm action **custom** vào một nút và gán cho nó một ID. <img src="/assets/shared/img/ios-events-1.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. Ví dụ: nếu người dùng nhấn vào một nút tùy chỉnh như **Login** hoặc **Allow notifications**, trình xử lý sự kiện sẽ được kích hoạt với tham số `actionId` khớp với **Action ID** trong builder. Bạn có thể tự tạo ID theo ý muốn, chẳng hạn như "allowNotifications". ```typescript showLineNumbers view.setEventHandlers({ onCustom(actionId, meta) { switch (actionId) { case 'login': console.log('Login action triggered'); break; case 'allow_notifications': console.log('Allow notifications action triggered'); break; } return false; // Don't close the onboarding }, }); ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "actionId": "allow_notifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ``` </Details> ### Hoàn tất 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: ```typescript showLineNumbers view.setEventHandlers({ onFinishedLoading(meta) { console.log('Onboarding loaded:', meta.onboardingId); }, }); ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ``` </Details> ### Đó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. <img src="/assets/shared/img/ios-events-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important Lưu ý rằng bạn cần tự xử lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ, bạn cần dừng việc hiển thị onboarding đó. ::: ```typescript showLineNumbers view.setEventHandlers({ onClose(actionId, meta) { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, }); ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ``` </Details> ### 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 nó đóng lại, có một cách đơn giản hơn – xử lý hành động đóng và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với các 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ỉ có thể hiển thị một view (paywall hoặc onboarding) trên màn hình tại một thời điểm. Nếu bạn hiển thị một paywall chồng lên trên một onboarding, bạn không thể điều khiển onboarding ở nền theo cách lập trình. Việc cố gắng đóng onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn đóng view onboarding trước khi hiển thị paywall. ```typescript showLineNumbers view.setEventHandlers({ onPaywall(actionId, meta) { // Dismiss onboarding before presenting paywall view.dismiss().then(() => { openPaywall(actionId); }); }, }); async function openPaywall(placementId: string) { // Implement your paywall opening logic here } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ``` </Details> ### Theo dõi điều hướng \{#tracking-navigation\} Bạn nhận được sự kiện analytics khi các sự kiện liên quan đến điều hướng xảy ra trong flow onboarding: ```typescript showLineNumbers view.setEventHandlers({ onAnalytics(event, meta) { console.log('Analytics event:', event.type, meta.onboardingId); }, }); ``` Object `event` có thể là một trong các kiểu sau: | Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình hoàn thành. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn thành) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `secondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy [gán ID `final` cho màn hình cuối cùng](design-onboarding). | | `unknown` | Dành cho bất kỳ loại sự kiện không được nhận dạng nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (siêu dữ liệu bổ sung) | Mỗi sự kiện đều bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Mã định danh duy nhất của onboarding flow | | `screenClientId` | Mã đị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 | <Details> <summary>Ví dụ về sự kiện (Nhấn để mở rộng)</summary> ```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 } } ``` </Details> --- # File: capacitor-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong Capacitor SDK" description: "Lưu và sử dụng dữ liệu từ các onboarding trong ứng dụng Capacitor của bạn với Adapty SDK." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](capacitor-get-pb-paywalls) thay thế: không giống onboarding chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](capacitor-get-pb-paywalls) và [Hiển thị flows & paywalls](capacitor-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời câu hỏi trong quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `onStateUpdated` 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ụ: ```typescript view.setEventHandlers({ onStateUpdated(action, meta) { // Process data }, }); ``` Xem định dạng action [tại đây](https://capacitor.adapty.io/types/onboardingstateupdatedaction). <Details> <summary>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)</summary> ```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 } } } ``` </Details> ## Các trường hợp sử dụng \{#use-cases\} ### Làm phong phú hồ sơ người dùng bằng dữ liệu \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ hai lần cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](capacitor-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 `name` và muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code của ứng dụng, nó có thể trông như thế này: ```typescript showLineNumbers view.setEventHandlers({ onStateUpdated(action, meta) { // Store user preferences or responses if (action.elementType === 'input') { const profileParams: any = {}; // Map elementId to appropriate profile field switch (action.elementId) { case 'name': if (action.value.type === 'text') { profileParams.firstName = action.value.value; } break; case 'email': if (action.value.type === 'email') { profileParams.email = action.value.value; } break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile({ params: profileParams }).catch((error) => { // handle the error }); } } }, }); ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Bằng cách sử dụng quiz 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 thể thao của họ và hiển thị các CTA cùng sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) vào trình tạo onboarding và gán ID có ý nghĩa cho các lựa chọn. 2. Xử lý các phản hồi quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](capacitor-setting-user-attributes) cho người dùng. ```typescript showLineNumbers view.setEventHandlers({ onStateUpdated(action, meta) { // Handle quiz responses and set custom attributes if (action.elementType === 'select') { const profileParams: any = {}; // Map quiz responses to custom attributes switch (action.elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) profileParams.codableCustomAttributes = { experience: action.value.value }; break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile({ params: profileParams }).catch((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](capacitor-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 hành động của nút này](capacitor-handling-onboarding-events#opening-a-paywall). --- # File: capacitor-best-practices --- --- title: "Các phương pháp tốt nhất trong Capacitor SDK" description: "Các mẫu tham chiếu để tích hợp Adapty SDK trên Capacitor — thứ tự gọi, xử lý lỗi và các quy tắc sẵn sàng sản xuất khác." --- <CustomDocCardList /> --- # File: capacitor-sdk-call-order --- --- title: "Thứ tự gọi trong Capacitor SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution và lỗi #2002 ngắt quãng bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự." --- `adapty.activate()` phải hoàn thành trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi nó resolve, SDK chưa có trạng thái. Mọi lệnh gọi được thực hiện trước hoặc song song với `activate()` sẽ thất bại với lỗi [`#2002 notActivated`](capacitor-handle-errors#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 người dùng cho đến khi `identify` hoàn tất. Các lệnh gọi chạy đồng thời với nó sẽ thất bại với lỗi [`#3006 profileWasChanged`](capacitor-handle-errors#custom-network-codes), hoặc tác động lên hồ sơ người dùng ẩn danh được tạo lúc kích hoạt. Khi điều này xảy ra, attribution, các MMP ID như `appsflyer_id`, và quyền sở hữu lượt cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. SDK của MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) đều tuân theo quy tắc tương tự. Hãy khởi tạo chúng trước và chờ callback UID của chúng trước khi gọi `adapty.activate`. Nếu không, MMP ID sẽ gắn vào một hồ sơ người dùng ẩn danh tạm thời và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Để biết thêm chi tiết về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự thực hiện đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai yếu tố: thời điểm bạn biết customer user ID và việc bạn có sử dụng MMP hay analytics SDK hay không. - **Bước 2 và 5**: Bắt buộc với mọi ứng dụng. Khởi động SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ cần thiết nếu bạn tích hợp MMP hoặc analytics SDK (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ cần thiết nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn đã có customer user ID ngay khi ứng dụng khởi động, hãy truyền nó trực tiếp vào `activate()` (bước 2a). Cách này sẽ không tạo hồ sơ người dùng ẩn danh, nên bước 4 là không cần thiết. | Bước | Gọi hàm | Thời điểm | Ghi chú | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo MMP hoặc analytics SDK của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khi khởi động ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `adapty.activate({ apiKey: '...', params: { customerUserId: '...' } })` | Khi khởi động ứng dụng, sau bước 1, nếu bạn đã có customer user ID | Khuyến nghị. Không tạo hồ sơ người dùng ẩn danh nào cả. | | 2b | `adapty.activate({ apiKey: '...' })` không có `customerUserId` | Khi khởi động ứng dụng, sau bước 1, nếu bạn chưa có customer user ID (hoặc không thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. | | 3 | `adapty.setIntegrationIdentifier({ key: '...', value: '...' })` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi nào do người dùng thực hiện | Bắt buộc để ID MMP gắn đúng vào hồ sơ người dùng. | | 4 | `await adapty.identify({ customerUserId: 'YOUR_USER_ID' })` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên path 2b có xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong `identify` sẽ sinh ra lỗi `#3006 profileWasChanged`. | | 5 | `getPaywall` (`getFlow` trong SDK v4), `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này sẽ khiến người dùng quay lại mất quyền truy cập premium, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về không đúng đối tượng. ::: ## Cài đặt từ web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng qua web checkout (Stripe, Paddle) rồi sau đó cài đặt ứng dụng native, lần đầu tiên thiết bị gọi `activate()` sẽ tạo ra một hồ sơ người dùng ẩn danh mới. Hồ sơ này không được liên kết với hồ sơ người dùng trên web. Nếu bạn có thể xác định customer user ID trước khi ứng dụng khởi chạy (từ flow 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({ customerUserId: 'YOUR_USER_ID' })` rồi gọi `restorePurchases`. Để biết metadata cần gửi kèm mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: capacitor-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong Capacitor SDK" description: "Tải paywall Adapty một cách đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho Capacitor." --- Một lần tải paywall đáng tin cậy trên Capacitor cần đảm bảo ba điều: hiển thị nhanh, trả về paywall đúng với đối tượng mục tiêu, và tự động chuyển sang phương án dự phòng khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó. :::tip Các quy tắc dưới đây giả định rằng `adapty.activate()` và `adapty.identify()` đã hoàn thành. Xem [Thứ tự gọi trong Capacitor SDK](capacitor-sdk-call-order). ::: Nội dung bên dưới sử dụng tên phương thức của v3. Trong SDK v4, `getPaywall` được đổi tên thành `getFlow` (xem [hướng dẫn migration](migration-to-capacitor-sdk-v4)) — tất cả các quy tắc đều áp dụng như nhau. ## Quy tắc và lưu ý \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Fetch placement ngay trước khi hiển thị. | Prefetch tất cả placement cùng lúc khi khởi động. | Bulk prefetch chặn main thread và gây màn hình đen trong suốt thời gian đó. | | Fetch `getPaywall` sau khi attribution đã có cơ hội resolve — ví dụ: 1–2 giây sau `activate` hoặc sau khi listener `onLatestProfileLoad` kích hoạt. | Gọi `getPaywall` khi khởi động app trong `App.tsx`. | Attribution chưa kịp cập nhật. Paywall sẽ resolve theo đối tượng mặc định và âm thầm bỏ qua các phân khúc cùng tính năng cá nhân hóa ASA. | | Đặt `loadTimeoutMs` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` mãi không timeout. | Không có timeout, người dùng ở vù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 đóng app. | Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-capacitor) để tham khảo tham số `fetchPolicy` và `loadTimeoutMs`, và [Placements](placements) để chọn placement phù hợp. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối kém thường xuyên (vùng nông thôn, khi di chuyển, các khu vực bị ảnh hưởng bởi định tuyến): - Đặt `fetchPolicy: 'return_cache_data_else_load'` 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 `loadTimeoutMs` từ 3000–5000 milliseconds và chấp nhận paywall dự phòng khi timeout kích hoạt. - Đừng chặn việc hiển thị paywall bằng `adapty.getProfile()`. Gọi `getPaywall` độc lập để một profile chậm không cản trở giao diện. --- # File: capacitor-show-aa-targeted-paywall --- --- title: "Hiển thị paywall được nhắm mục tiêu AA khi khởi chạy lần đầu trong Capacitor SDK" description: "Hiển thị paywall ngay lập tức và nâng cấp cho người dùng Apple Ads sau khi attribution được áp dụng trong Capacitor, sử dụng AdaptyProfile.appliedAttributionSources." --- Attribution từ Apple Ads (AA) được gửi về 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 kịp về, nên `getFlow` sẽ trả về kết quả theo đối tượng mặc định và người dùng đến từ Apple Ads sẽ bỏ lỡ paywall được nhắm mục tiêu theo AA. Thay vì chờ attribution về rồi mới hiển thị paywall, hãy hiển thị ngay một paywall và làm mới nó khi attribution AA được áp dụng — như vậy người dùng Apple Ads sẽ thấy đúng biến thể được nhắm mục tiêu, còn những người dùng khác không phải chờ đợi gì. `AdaptyProfile.appliedAttributionSources` cho bạn biết khi nào attribution AA đã được áp dụng. ## Trước khi bắt đầu \{#before-you-start\} Bạn cần: - Adapty Capacitor SDK **3.17.1** trở lên. - Apple Ads được cấu hình cho ứng dụng trong Adapty. Xem [Apple Ads](apple-search-ads). ## Cách hoạt động \{#how-it-works\} Sau khi gọi `adapty.activate()`, SDK sẽ ngầm yêu cầu dữ liệu attribution từ Apple Ads và chuyển kết quả về backend của Adapty. Khi AA trở thành nguồn attribution đang hoạt động của hồ sơ người dùng, SDK sẽ gửi một `AdaptyProfile` đã cập nhật đến listener `onLatestProfileLoad` của bạn, với `'apple_search_ads'` trong mảng `appliedAttributionSources`. Nhờ đó, bạn có thể tải paywall theo hai bước: 1. Gọi `getFlow` ngay lập tức. Vì chưa có attribution nào được áp dụng, Adapty sẽ xử lý yêu cầu dựa trên đối tượng mặc định, do đó người dùng thấy paywall ngay lập tức. 2. Khi `'apple_search_ads'` xuất hiện, gọi lại `getFlow`. Adapty lúc này sẽ 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, thay thế paywall trước đó. `appliedAttributionSources` có thể rỗng hoặc vắng mặt. Điều đó có nghĩa là: - Attribution của Apple Ads chưa được xử lý cho hồ sơ người dùng này, hoặc - chưa có attribution nào được ghi nhận. Dù thế nào, bước 1 vẫn an toàn — Adapty xử lý yêu cầu dựa trên đối tượng phù hợp với trạng thái hồ sơ hiện tại, thường là đối tượng mặc định. Bước 2 chỉ chạy khi `'apple_search_ads'` xuất hiện. :::important Ở mọi lần khởi động tiếp theo, hồ sơ đã được cache sẵn sẽ có `'apple_search_ads'` trong `appliedAttributionSources`, nên lần `getFlow` đầu tiên đã trả về paywall được phân khúc theo Apple Ads — không có lần fetch thứ hai hay thay đổi nào hiển thị ra. Luồng hai bước chỉ quan trọng ở lần khởi động đầu tiên, khi attribution vẫn đang được xử lý. ::: ## Triển khai \{#implementation\} Hiển thị paywall ngay lập tức, sau đó lắng nghe sự kiện `'apple_search_ads'` và làm mới paywall khi nhận được. 1. **Kích hoạt SDK.** Xem [Cài đặt & cấu hình Capacitor SDK](sdk-installation-capacitor). 2. **Tải và hiển thị paywall** bằng `getFlow` như bình thường — đừng chặn luồng chờ attribution. 3. **Đăng ký nhận cập nhật hồ sơ người dùng** với `adapty.addListener('onLatestProfileLoad', …)` và theo dõi `'apple_search_ads'`. Khi xuất hiện, tải lại paywall và hiển thị phiên bản đã cập nhật. Nếu bạn chưa thiết lập listener, xem [Lắng nghe cập nhật gói đăng ký](capacitor-check-subscription-status#listen-to-subscription-updates): ```typescript const listener = await adapty.addListener('onLatestProfileLoad', async ({ profile }) => { if (!profile.appliedAttributionSources?.includes('apple_search_ads')) return; const targeted = await adapty.getFlow({ placementId }); // present the targeted flow in place of the first one }); // Call listener.remove() after the upgrade, or after a timeout (see below). ``` 4. **Dừng lắng nghe sau một khoảng thời gian chờ.** Hầu hết người dùng không có attribution từ Apple Ads, vì vậy hãy xóa listener sau một thời gian thay vì giữ nó suốt cả phiên. Hãy cấu hình [paywall dự phòng](capacitor-use-fallback-paywalls) cho placement đó để người dùng luôn thấy gì đó nếu yêu cầu thất bại. ## Ví dụ hoàn chỉnh \{#complete-example\} `onAppleAdsAttribution` sẽ resolve khi attribution của Apple Ads được áp dụng, hoặc reject sau `timeoutMs`. Ví dụ dưới đây tải paywall ngay lập tức, sau đó tải lại khi attribution được nhận — người dùng Apple Ads sẽ thấy paywall đúng mục tiêu, và nếu attribution không bao giờ đến thì paywall đầu tiên vẫn được giữ nguyên: ```typescript const APPLE_ADS_SOURCE = 'apple_search_ads'; const placementId = 'YOUR_PLACEMENT_ID'; function hasAppleAdsAttribution(profile: AdaptyProfile): boolean { return profile.appliedAttributionSources?.includes(APPLE_ADS_SOURCE) ?? false; } /** * Resolves once Apple Ads attribution is applied to the profile. * Rejects with a timeout error if attribution never arrives within `timeoutMs`. * Call after `adapty.activate()`. */ export function onAppleAdsAttribution(timeoutMs: number): Promise<void> { return new Promise((resolve, reject) => { let timer: ReturnType<typeof setTimeout> | undefined; let handle: { remove: () => void } | undefined; const stop = () => { clearTimeout(timer); handle?.remove(); }; adapty .addListener('onLatestProfileLoad', ({ profile }) => { if (!hasAppleAdsAttribution(profile)) return; stop(); resolve(); }) .then(listener => { handle = listener; }); timer = setTimeout(() => { stop(); reject(new Error(`Apple Ads attribution timed out after ${timeoutMs}ms`)); }, timeoutMs); }); } let flow = await adapty.getFlow({ placementId }); onAppleAdsAttribution(30_000) .then(() => adapty.getFlow({ placementId })) .then(updated => { flow = updated; }) .catch(() => { console.log('Apple Ads attribution or loading failed'); }); ``` Khi khởi động lần đầu, người dùng Apple Ads có thể thấy thoáng qua paywall mặc định trước khi nó được thay thế. Nếu bạn hiển thị paywall bằng Paywall Builder, hãy cân nhắc xem việc hiển thị lại có chấp nhận được không, hoặc chỉ áp dụng cập nhật trước khi paywall được hiển thị. Hãy điều chỉnh `timeoutMs` theo thời gian bạn sẵn sàng chờ — attribution nếu có thường đến trong vài giây sau khi khởi động. Nếu ứng dụng của bạn đã lắng nghe `onLatestProfileLoad` cho các mục đích khác (ví dụ: [kiểm tra trạng thái gói đăng ký](capacitor-check-subscription-status#listen-to-subscription-updates)), bạn không cần thay đổi gì. `adapty.addListener` hỗ trợ nhiều listener độc lập, vì vậy listener này sẽ được thêm vào mà không ảnh hưởng đến các listener khác. --- # File: capacitor-test --- --- title: "Test & release in Capacitor SDK" description: "Tìm hiểu cách kiểm tra và phát hành ứng dụng Capacitor của bạn với Adapty SDK." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng Capacitor của mình, bạn sẽ muốn 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 tích hợp SDK và luồng mua hàng thực tế với môi trường sandbox của Apple và môi trường kiểm tra của Google Play. ## Kiểm tra ứng dụng của bạn \{#test-your-app\} Để kiểm tra toàn diện các in-app purchase, hãy xem hướng dẫn kiểm tra theo từng nền tảng của chúng tôi: [Hướng dẫn kiểm tra iOS](test-purchases-in-sandbox) và [Hướng dẫn kiểm tra Android](testing-on-android). ## Chuẩn bị để phát hành \{#prepare-for-release\} Trước khi gửi ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối với cửa hàng và thông báo máy chủ đã được cấu hình - Các giao dịch mua hoàn tất và được báo cáo về Adapty - Quyền truy cập được mở khóa và khôi phục đúng cách - Yêu cầu về quyền riêng tư và xét duyệt đã được đáp ứng --- # File: kids-mode-capacitor --- --- title: "Chế độ Trẻ em trong Capacitor 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 Capacitor SDK." --- Nếu ứng dụng Capacitor 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 bạn đang sử dụng Adapty SDK, chỉ cần vài bước đơn giản là có thể 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 trên cửa hàng ứng dụng. ## Cần chuẩn bị gì? \{#whats-required\} Bạn cần cấu hình SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách thận trọng. User ID theo định dạng `<FirstName.LastName>` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc sử dụng email. Đối với Kids Mode, phương pháp tốt nhất là dùng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã được băm hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ quy định. ## Bật Chế độ Trẻ em \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\} Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để làm điều này, hãy vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** trong phần **Collect users' IP address**. ### Cập nhật trong mã ứng dụng di động của bạn \{#updates-in-your-mobile-app-code\} Để tuân thủ các chính sách, hãy tắt tính năng thu thập IDFA, GAID và địa chỉ IP của người dùng: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // Disable IP address collection ipAddressCollectionDisabled: true, // Disable IDFA collection on iOS ios: { idfaCollectionDisabled: true }, // Disable Google Advertising ID collection on Android android: { adIdCollectionDisabled: true } } }); console.log('Adapty activated with Kids Mode enabled'); } catch (error) { console.error('Failed to activate Adapty with Kids Mode:', error); } ``` ### Cấu hình theo nền tảng \{#platform-specific-configurations\} #### iOS \{#ios\} <SDKv4> Ngay cả khi bạn đã tắt thu thập IDFA trong code (như trên), bản build vẫn bao gồm các framework `AdSupport` và `AppTrackingTransparency`. Danh mục Kids Category trên App Store không cho phép điều này. Và vì SDK v4 cài đặt iOS SDK native thông qua Swift Package Manager, không có bước Podfile nào để loại bỏ chúng. Để tuân thủ yêu cầu của Apple, hãy thêm lệnh `adapty-kids-mode` đi kèm với SDK vào `postinstall` của ứng dụng. Lệnh này bật trait `KidsMode` của SDK, giúp biên dịch loại bỏ đoạn code đó. Lệnh sẽ được áp dụng lại mỗi khi cài đặt: ```json showLineNumbers title="package.json" { "scripts": { "postinstall": "adapty-kids-mode" } } ``` Sau đó cài đặt lại và re-resolve các gói iOS, rồi build với **Xcode 26** trở lên: ```sh showLineNumbers title="Shell" npm install npx cap sync ios ``` Để tắt Kids Mode, chạy `adapty-kids-mode disable` và sync lại. </SDKv4> <SDKv3> Nếu bạn đang dùng CocoaPods cho iOS, bạn cũng có thể bật Kids Mode ở cấp độ native: 1. Cập nhật Podfile của bạn: - Nếu bạn **chưa có** phần `post_install`, hãy thêm toàn bộ đoạn code bên dưới. - Nếu bạn **đã có** phần `post_install`, hãy gộp các dòng được đánh dấu vào đó. ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. Chạy lệnh sau để áp dụng các thay đổi: ```sh showLineNumbers title="Shell" pod install ``` </SDKv3> #### Android: xóa quyền advertising ID \{#android-remove-the-advertising-id-permission\} Đặt `adIdCollectionDisabled: true` (ở trên) sẽ ngăn Adapty thu thập advertising ID, nhưng SDK vẫn khai báo quyền `AD_ID`. Nếu ứng dụng của bạn chỉ nhắm đến trẻ em **và** được biên dịch với Android 13 (API 33) trở lên, Google Play sẽ cấm bạn yêu cầu quyền này. Thêm hai khai báo sau vào phần tử `<manifest>` của bạn: 1. Khai báo namespace `tools` (manifest mặc định của Capacitor không có namespace này). 2. Thêm một entry `<uses-permission>` cho `AD_ID` với `tools:node="remove"` để loại bỏ nó. ```xml showLineNumbers title="android/app/src/main/AndroidManifest.xml" <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" /> </manifest> ``` ## Các bước tiếp theo \{#next-steps\} Sau khi bật Chế độ Trẻ em, hãy đảm bảo: 1. Kiểm thử ứng dụng kỹ lưỡng để đảm bảo tất cả các tính năng hoạt động đúng 2. Xem lại chính sách quyền riêng tư của ứng dụng để phản ánh việc đã tắt thu thập dữ liệu 3. Nộp ứng dụng để xét duyệt kèm theo tài liệu rõ ràng về việc tuân thủ Chế độ Trẻ em Để biết thêm thông tin về yêu cầu theo từng nền tảng: - [Chế độ Trẻ em trong iOS SDK](kids-mode) để biết thêm chi tiết cấu hình iOS - [Chế độ Trẻ em trong Android SDK](kids-mode-android) để biết thêm chi tiết cấu hình Android --- # File: capacitor-reference --- --- title: "Reference" description: "Tài liệu tham khảo cho Adapty Capacitor SDK." --- Trang này chứa tài liệu tham khảo cho Adapty Capacitor SDK. Chọn chủ đề bạn cần: - **[Mô hình SDK](https://capacitor.adapty.io/)** - Các mô hình dữ liệu và cấu trúc được SDK sử dụng - **[Xử lý lỗi](capacitor-handle-errors)** - Xử lý lỗi và khắc phục sự cố --- # File: capacitor-handle-errors --- --- title: "Xử lý lỗi trong Capacitor SDK" description: "Xử lý lỗi trong Capacitor SDK." --- Mọi lỗi được SDK trả về đều là một instance của `AdaptyError`. Dưới đây là ví dụ: :::tip **Bật verbose logs trước khi debug.** Hầu hết các `AdaptyError` đều bao bọc một lỗi cơ bản từ StoreKit, Play Billing, mạng hoặc backend. Khi bật verbose logs (`adapty.setLogLevel({ logLevel: 'verbose' })` — xem [Logging](sdk-installation-capacitor#logging)), lỗi đó sẽ được in ra console, thường cho bạn biết nguyên nhân thực sự. Thuộc tính `detail` trên `AdaptyError` luôn được điền bất kể mức log — verbose logs chỉ giúp hiển thị nó ra console. ::: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product }); // Handle purchase result if (result.type === 'success') { console.log('Purchase successful:', result.profile); } else if (result.type === 'user_cancelled') { console.log('User cancelled the purchase'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { if (error instanceof AdaptyError) { console.error('Adapty error:', error.adaptyCode, error.localizedDescription); // Handle specific error codes switch (error.adaptyCode) { case ErrorCodeName.cantMakePayments: console.log('In-app purchases are not allowed on this device'); break; case ErrorCodeName.notActivated: console.log('Adapty SDK is not activated'); break; case ErrorCodeName.productPurchaseFailed: console.log('Purchase failed:', error.detail); break; default: console.log('Other error occurred:', error.detail); } } else { console.error('Non-Adapty error:', error); } } ``` ## Thuộc tính của lỗi \{#error-properties\} Lớp `AdaptyError` cung cấp các thuộc tính sau: | Thuộc tính | Kiểu | Mô tả | |----------|------|-------------| | `adaptyCode` | `number` | Mã lỗi dạng số (ví dụ: `1003` cho cantMakePayments) | | `localizedDescription` | `string` | Thông báo lỗi thân thiện với người dùng | | `detail` | `string \| undefined` | Thông tin chi tiết bổ sung về lỗi (tùy chọn) | | `message` | `string` | Thông báo lỗi đầy đủ bao gồm mã và mô tả | ## Mã lỗi \{#error-codes\} SDK xuất các hằng số và tiện ích để làm việc với mã lỗi: ### Hằng số ErrorCodeName \{#errorcodename-constant\} Ánh xạ các định danh chuỗi sang mã số: ```typescript ErrorCodeName.cantMakePayments // 1003 ErrorCodeName.notActivated // 2002 ErrorCodeName.networkFailed // 2005 ``` ### Hằng số ErrorCode \{#errorcode-constant\} Ánh xạ mã số sang định danh chuỗi: ```typescript ErrorCode[1003] // 'cantMakePayments' ErrorCode[2002] // 'notActivated' ErrorCode[2005] // 'networkFailed' ``` ### Các hàm hỗ trợ \{#helper-functions\} ```typescript // Get numeric code from string name: getErrorCode('cantMakePayments') // 1003 // Get string name from numeric code: getErrorPrompt(1003) // 'cantMakePayments' ``` ### So sánh mã lỗi \{#comparing-error-codes\} **Lưu ý quan trọng:** `error.adaptyCode` là một **số**, vì vậy hãy so sánh trực tiếp với mã số: ```typescript // Option 1: Use ErrorCodeName constant (recommended) ✅ if (error.adaptyCode === ErrorCodeName.cantMakePayments) { console.log('Cannot make payments'); } // Option 2: Compare with numeric literal ✅ if (error.adaptyCode === 1003) { console.log('Cannot make payments'); } // NOT like this ❌ - compares number to string and will never match if (error.adaptyCode === ErrorCode[1003]) { } ``` ## Trình xử lý lỗi toàn cục \{#global-error-handler\} Bạn có thể thiết lập một trình xử lý lỗi toàn cục để bắt tất cả các lỗi từ Adapty: ```typescript showLineNumbers // Set up global error handler AdaptyError.onError = (error: AdaptyError) => { console.error('Global Adapty error:', { code: error.adaptyCode, message: error.localizedDescription, detail: error.detail }); // Handle specific error types globally if (error.adaptyCode === ErrorCodeName.notActivated) { // SDK not activated - maybe retry activation console.log('SDK not activated, attempting to reactivate...'); } }; ``` ## Các mẫu xử lý lỗi phổ biến \{#common-error-handling-patterns\} ### Xử lý lỗi mua hàng \{#handle-purchase-errors\} ```typescript showLineNumbers async function handlePurchase(product: AdaptyPaywallProduct) { try { const result = await adapty.makePurchase({ product }); if (result.type === 'success') { console.log('Purchase successful:', result.profile); } else if (result.type === 'user_cancelled') { console.log('User cancelled the purchase'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { if (error instanceof AdaptyError) { switch (error.adaptyCode) { case ErrorCodeName.cantMakePayments: console.log('In-app purchases not allowed'); break; case ErrorCodeName.productPurchaseFailed: console.log('Purchase failed:', error.detail); break; default: console.error('Purchase error:', error.localizedDescription); } } } } ``` ### Xử lý lỗi mạng \{#handle-network-errors\} ```typescript showLineNumbers async function fetchFlow(placementId: string) { try { const flow = await adapty.getFlow({ placementId }); return flow; } catch (error) { if (error instanceof AdaptyError) { switch (error.adaptyCode) { case ErrorCodeName.networkFailed: console.log('Network error, retrying...'); // Implement retry logic break; case ErrorCodeName.serverError: console.log('Server error:', error.detail); break; case ErrorCodeName.notActivated: console.log('SDK not activated'); break; default: console.error('Paywall fetch error:', error.localizedDescription); } } throw error; } } ``` ## Mã StoreKit hệ thống \{#system-storekit-codes\} | Lỗi | Mã | Mô tả | |-----|----|-----------| | unknown | 0 | Lỗi này cho biết đã xảy ra lỗi không xác định hoặc không mong muốn. | | clientInvalid | 1 | Mã lỗi này cho biết client không được phép thực hiện hành động đã thử. | | paymentCancelled | 2 | <p>Mã lỗi này cho biết người dùng đã hủy yêu cầu thanh toán.</p><p>Không cần thực hiện hành động nào, nhưng về mặt logic nghiệp vụ, bạn có thể cung cấp ưu đãi cho người dùng hoặc nhắc nhở họ sau.</p> | | paymentInvalid | 3 | Lỗi này cho biết một trong các tham số thanh toán không được cửa hàng nhận dạng. | | paymentNotAllowed | 4 | <p>Mã lỗi này cho biết người dùng không được phép ủy quyền thanh toán. Nguyên nhân có thể:</p><p></p><p>- Thanh toán không được hỗ trợ ở quốc gia của người dùng.</p><p>- Người dùng chưa đủ tuổi.</p> | | storeProductNotAvailable | 5 | Mã lỗi này cho biết sản phẩm được yêu cầu không có trên App Store. Hãy đảm bảo sản phẩm khả dụng ở quốc gia đang sử dụng. | | cloudServicePermissionDenied | 6 | Mã lỗi này cho biết người dùng chưa cho phép truy cập thông tin dịch vụ Cloud. | | cloudServiceNetworkConnectionFailed | 7 | Mã lỗi này cho biết thiết bị không thể kết nối mạng. | | cloudServiceRevoked | 8 | Mã lỗi này cho biết người dùng đã thu hồi quyền sử dụng dịch vụ cloud này. | | privacyAcknowledgementRequired | 9 | Mã lỗi này cho biết người dùng chưa xác nhận chính sách bảo mật của cửa hàng. | | unauthorizedRequestData | 10 | Mã lỗi này cho biết yêu cầu được tạo không đúng cách. | | invalidOfferIdentifier | 11 | <p>Mã định danh ưu đãi không hợp lệ. Nguyên nhân có thể:</p><p></p><p>- Bạn chưa thiết lập ưu đãi với mã định danh đó trên App Store.</p><p>- Bạn đã thu hồi ưu đãi.</p><p>- Bạn nhập sai mã ưu đãi.</p> | | invalidSignature | 12 | Mã lỗi này cho biết chữ ký trong ưu đãi thanh toán không hợp lệ. Hãy đảm bảo bạn đã điền trường **In-app purchase Key ID** và tải lên tệp **In-App Purchase Private Key**. Tham khảo phần [Cấu hình tích hợp App Store](app-store-connection-configuration) để biết thêm chi tiết. | | missingOfferParams | 13 | <p>Lỗi này cho biết có vấn đề với tích hợp Adapty hoặc với các ưu đãi.</p><p>Tham khảo phần [Cấu hình tích hợp App Store](app-store-connection-configuration) và [Ưu đãi](offers) để biết cách thiết lập chúng.</p> | | invalidOfferPrice | 14 | Mã lỗi này cho biết giá bạn chỉ định trong cửa hàng không còn hợp lệ. Các ưu đãi luôn phải thể hiện mức giá được giảm. | ## Mã Android tùy chỉnh \{#custom-android-codes\} | Lỗi | Mã | Mô tả | |-----|----|-----------| | adaptyNotInitialized | 20 | Bạn cần cấu hình đúng Adapty SDK bằng phương thức `Adapty.activate`. Tìm hiểu cách thực hiện [cho React Native](sdk-installation-reactnative). | | productNotFound | 22 | Lỗi này cho biết sản phẩm được yêu cầu mua không có sẵn trong cửa hàng. | | invalidJson | 23 | JSON của paywall không hợp lệ. Hãy sửa nó trong Adapty Dashboard. Tham khảo phần [Tùy chỉnh paywall với remote config](customize-paywall-with-remote-config) để biết cách khắc phục. | | currentSubscriptionToUpdateNotFoundInHistory | 24 | Không tìm thấy gói đăng ký gốc cần được gia hạn. | | pendingPurchase | 25 | Lỗi này cho biết trạng thái mua hàng đang ở trạng thái chờ xử lý thay vì đã mua. Tham khảo trang [Xử lý giao dịch đang chờ](https://developer.android.com/google/play/billing/integrate#pending) trong tài liệu Android Developer để biết thêm chi tiết. | | billingServiceTimeout | 97 | Lỗi này cho biết yêu cầu đã đạt đến thời gian chờ tối đa trước khi Google Play có thể phản hồi. Điều này có thể do, ví dụ, sự chậm trễ trong việc thực hiện hành động được yêu cầu bởi lệnh gọi Play Billing Library. | | featureNotSupported | 98 | Tính năng được yêu cầu không được Play Store hỗ trợ trên thiết bị hiện tại. | | billingServiceDisconnected | 99 | Lỗi nghiêm trọng này cho biết kết nối của ứng dụng client với dịch vụ Google Play Store qua `BillingClient` đã bị ngắt. | | billingServiceUnavailable | 102 | Lỗi tạm thời này cho biết dịch vụ Google Play Billing hiện không khả dụng. Trong hầu hết các trường hợp, điều này có nghĩa là có sự cố kết nối mạng ở đâu đó giữa thiết bị client và dịch vụ Google Play Billing. | | billingUnavailable | 103 | <p>Lỗi này cho biết đã xảy ra lỗi thanh toán của người dùng trong quá trình mua hàng. Ví dụ về khi điều này có thể xảy ra:</p><p></p><p>1\. Ứng dụng Play Store trên thiết bị của người dùng đã lỗi thời.</p><p>2. Người dùng ở quốc gia không được hỗ trợ.</p><p>3. Người dùng là người dùng doanh nghiệp và quản trị viên doanh nghiệp của họ đã vô hiệu hóa việc mua hàng của người dùng.</p><p>4. Google Play không thể tính phí phương thức thanh toán của người dùng. Ví dụ, thẻ tín dụng của người dùng có thể đã hết hạn.</p><p>5. Người dùng chưa đăng nhập vào ứng dụng Play Store.</p> | | developerError | 105 | Đây là lỗi nghiêm trọng cho biết bạn đang sử dụng API không đúng cách. | | billingError | 106 | Đây là lỗi nghiêm trọng cho biết có vấn đề nội bộ với chính Google Play. | | itemAlreadyOwned | 107 | Sản phẩm consumable đã được mua trước đó. | | itemNotOwned | 108 | Lỗi này cho biết hành động được yêu cầu trên mặt hàng đã thất bại | ## Mã StoreKit tùy chỉnh \{#custom-storekit-codes\} | Lỗi | Mã | Mô tả | |-----|----|-----------| | noProductIDsFound | 1000 | <p>Lỗi này cho biết không có sản phẩm nào trong paywall khả dụng trong cửa hàng.</p><p>Nếu bạn gặp lỗi này, hãy làm theo các bước dưới đây để khắc phục:</p><p></p><p>1. Kiểm tra xem tất cả các sản phẩm đã được thêm vào Adapty Dashboard chưa.</p><p>2. Đảm bảo rằng Bundle ID của ứng dụng khớp với Bundle ID từ Apple Connect.</p><p>3. Xác minh rằng mã định danh sản phẩm từ các cửa hàng ứng dụng khớp với những mã bạn đã thêm vào Dashboard. Lưu ý rằng các mã định danh không nên chứa Bundle ID, trừ khi Bundle ID đã được bao gồm trong cửa hàng.</p><p>4. Xác nhận rằng trạng thái thanh toán của ứng dụng đang hoạt động trong cài đặt thuế Apple của bạn. Đảm bảo thông tin thuế của bạn được cập nhật và chứng chỉ của bạn còn hợp lệ.</p><p>5. Kiểm tra xem tài khoản ngân hàng có được liên kết với ứng dụng hay không để ứng dụng đủ điều kiện kiếm tiền.</p><p>6. Kiểm tra xem các sản phẩm có khả dụng ở tất cả các khu vực hay không. Ngoài ra, hãy đảm bảo rằng các sản phẩm của bạn ở trạng thái **"Ready to Submit"**.</p> | | productRequestFailed | 1002 | <p>Không thể tải các sản phẩm khả dụng vào lúc này. Nguyên nhân có thể:</p><p></p><p>- Chưa có cache nào được tạo và đồng thời không có kết nối internet.</p> | | cantMakePayments | 1003 | In-app purchase không được phép trên thiết bị này. | | noPurchasesToRestore | 1004 | Lỗi này cho biết Google Play không tìm thấy giao dịch mua để khôi phục. | | cantReadReceipt | 1005 | <p>Không có biên lai hợp lệ nào trên thiết bị. Điều này có thể là vấn đề trong quá trình kiểm tra sandbox.</p><p>Không cần thực hiện hành động nào, nhưng về mặt logic nghiệp vụ, bạn có thể cung cấp ưu đãi cho người dùng hoặc nhắc nhở họ sau.</p> | | productPurchaseFailed | 1006 | Mua sản phẩm thất bại. Lỗi này bao gồm một lỗi StoreKit bên dưới — đọc lỗi được bao gồm (hoặc bật nhật ký chi tiết để xem nó trong console) để biết lý do thực sự. Lỗi được bao gồm thường là một trong các mã StoreKit 0–14 trong bảng trên — thường gặp nhất là `paymentCancelled`, `paymentInvalid`, `paymentNotAllowed`, hoặc `invalidOfferPrice`. Nếu bạn không thể xác định lý do cụ thể, hãy thử một [hồ sơ sandbox](test-purchases-in-sandbox) mới; nếu vẫn thất bại, hãy liên hệ bộ phận hỗ trợ Apple. | | refreshReceiptFailed | 1010 | Lỗi này cho biết biên lai không được nhận. Chỉ áp dụng cho StoreKit 1. | | receiveRestoredTransactionsFailed | 1011 | Khôi phục giao dịch mua thất bại. | ## Mã mạng tùy chỉnh \{#custom-network-codes\} | Lỗi | Mã | Mô tả | | :------------------- | :--- | :----------------------------------------------------------- | | notActivated | 2002 | Bạn cần cấu hình đúng Adapty SDK bằng phương thức `Adapty.activate`. Tìm hiểu cách thực hiện [cho React Native](sdk-installation-reactnative). | | badRequest | 2003 | Yêu cầu không hợp lệ. | | serverError | 2004 | Lỗi máy chủ. | | networkFailed | 2005 | Yêu cầu mạng thất bại. | | decodingFailed | 2006 | Lỗi này cho biết việc giải mã phản hồi đã thất bại. | | encodingFailed | 2009 | Lỗi này cho biết việc mã hóa yêu cầu đã thất bại. | | analyticsDisabled | 3000 | Chúng tôi không thể xử lý các sự kiện analytics vì bạn đã tắt tính năng này. Tham khảo phần [Tích hợp Analytics](analytics-integration) để biết thêm chi tiết. | | wrongParam | 3001 | Lỗi này cho biết một số tham số của bạn không đúng: để trống khi không được phép để trống hoặc sai kiểu, v.v. | | activateOnceError | 3005 | Không thể gọi phương thức `.activate` nhiều hơn một lần. | | profileWasChanged | 3006 | Hồ sơ người dùng đã bị thay đổi trong quá trình thực hiện thao tác. | | fetchTimeoutError | 3101 | Lỗi này có nghĩa là paywall không thể được tải trong giới hạn thời gian đã đặt. Để tránh tình huống này, [hãy thiết lập paywall dự phòng cục bộ](fetch-paywalls-and-products). | | operationInterrupted | 9000 | Thao tác này đã bị hệ thống ngắt. | --- # File: capacitor-sdk-migration-guides --- --- title: "Hướng dẫn Migration Capacitor SDK" description: "Hướng dẫn migration cho các phiên bản Adapty Capacitor SDK." --- Trang này chứa tất cả các hướng dẫn migration cho Adapty Capacitor SDK. Chọn phiên bản bạn muốn migrate đến để xem hướng dẫn chi tiết: - **[Migrate lên v4.0 (beta)](migration-to-capacitor-sdk-v4)** - [**Migrate lên v3.16**](migration-to-capacitor-316) --- # File: migration-to-capacitor-sdk-v4 --- --- title: "Migrate Adapty Capacitor SDK sang v. 4.0" description: "Migrate lên Adapty Capacitor SDK v4.0 (beta) bằng cách thay thế các API paywall bằng API flow, tương thích với cả Flow Builder và Paywall Builder." --- Adapty Capacitor SDK 4.0 (beta) giới thiệu flows và đổi tên các API paywall tương ứng. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi cấu hình nào trên Adapty Dashboard. ## Tham khảo nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `adapty.getPaywall({ placementId, locale?, params? })` | `adapty.getFlow({ placementId, params? })` | | `adapty.getPaywallForDefaultAudience({ placementId, locale?, params? })` | `adapty.getFlowForDefaultAudience({ placementId, params? })` | | `adapty.getPaywallProducts({ paywall })` | `adapty.getPaywallProducts({ flow })` | | `adapty.logShowPaywall({ paywall })` | `adapty.logShowFlow({ flow })` | | `AdaptyPaywall` (kiểu) | `AdaptyFlow` + `AdaptyFlowPaywall` | | `createPaywallView(paywall, params?)` | `createFlowView(flow, params?)` | | `PaywallViewController` | `FlowViewController` | | `EventHandlers` (kiểu) | `FlowEventHandlers` | | `CreatePaywallViewParamsInput` | `CreateFlowViewParamsInput` | | `onRenderingFailed` | `onError` | `AdaptyPaywallProduct` giữ nguyên tên — sản phẩm vẫn thuộc về một flow, và `getPaywallProducts` cũng giữ nguyên tên, nhưng giờ nhận vào một `AdaptyFlow`. Các phương thức `getFlow` và `getFlowForDefaultAudience` không còn nhận tham số `locale` nữa. Các API mua hàng và hồ sơ người dùng (`makePurchase`, `restorePurchases`, `getProfile`, `identify`, `updateProfile`) và fallback qua `setFallback` không thay đổi. Các phương thức view `present`, `dismiss`, `setEventHandlers`, và `showDialog`, cùng các event handler `onCloseButtonPress`, `onUrlPress`, `onCustomAction`, `onProductSelected`, `onPurchaseStarted`, `onPurchaseCompleted`, `onPurchaseFailed`, `onRestoreStarted`, `onRestoreCompleted`, `onRestoreFailed`, `onLoadingProductsFailed`, `onWebPaymentNavigationFinished`, và `onAndroidSystemBack` giữ nguyên tên như trong v3. Các phương thức onboarding vẫn hoạt động nhưng đã bị deprecated — xem [Deprecation API Onboarding](#onboarding-api-deprecation). Một số hành vi mặc định đã thay đổi — xem [Thay đổi hành vi mặc định](#default-behavior-changes). ## Phiên bản tối thiểu \{#minimum-versions\} Yêu cầu runtime không thay đổi so với v3.16+: **iOS 15.0**, **Android minSdk 24**, và **Capacitor 8**. Không cần thay đổi deployment target. Có một yêu cầu build mới: **Xcode 26 trở lên** — native Adapty iOS SDK 4.0.0-beta.2 đi kèm với bản phát hành này sử dụng Swift tools 6.2. v4 đi kèm với native Adapty SDK iOS 4.0.0-beta.2 và Android BOM 4.0.0-beta.1. ## Cài đặt \{#installation\} ### Cập nhật gói v4.0 là bản phát hành thử nghiệm (pre-release), vì vậy hãy chỉ định chính xác phiên bản — npm sẽ không tự chọn các phiên bản pre-release qua cú pháp caret/tilde: ```bash showLineNumbers npm install @adapty/capacitor@4.0.0-beta.2 ``` Sau đó đồng bộ các dự án native: ```bash showLineNumbers npx cap sync ``` ### iOS: Chỉ dùng Swift Package Manager \{#ios-swift-package-manager-only\} [Repo spec của CocoaPods sẽ chuyển sang chế độ chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/), vì vậy kể từ v4, `AdaptyCapacitor.podspec` đã bị xóa và SDK chỉ cài đặt trên iOS **thông qua Swift Package Manager (SPM)**. Dự án iOS của ứng dụng bạn phải sử dụng tích hợp SPM của Capacitor: - Ứng dụng mới: thêm platform iOS với package manager SPM: ```bash showLineNumbers npx cap add ios --packagemanager SPM ``` - Ứng dụng đang dùng CocoaPods: migrate dự án iOS theo [hướng dẫn của Capacitor về việc dùng SPM trong dự án hiện có](https://capacitorjs.com/docs/ios/spm#using-spm-in-an-existing-capacitor-project). Xem [Cài đặt Adapty SDK](sdk-installation-capacitor) để biết toàn bộ các bước thiết lập. ## Tải flow \{#fetching-flows\} ### getPaywall → getFlow Kiểu trả về thay đổi từ `AdaptyPaywall` thành `AdaptyFlow`, và tùy chọn `locale` bị loại bỏ — khi bạn render một flow, locale sẽ được tự động xác định; đối với paywall tùy chỉnh, tất cả các locale được trả về trong `flow.remoteConfigs`: ```diff showLineNumbers - const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en' }); + const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID' }); ``` `getPaywallForDefaultAudience` được đổi tên theo cách tương tự: ```diff showLineNumbers - const paywall = await adapty.getPaywallForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en' }); + const flow = await adapty.getFlowForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID' }); ``` ### getPaywallProducts(paywall) → getPaywallProducts(flow) `getPaywallProducts` giữ nguyên tên nhưng giờ nhận vào một `AdaptyFlow`: ```diff showLineNumbers - const products = await adapty.getPaywallProducts({ paywall }); + const products = await adapty.getPaywallProducts({ flow }); ``` ## Mô hình dữ liệu \{#data-model\} `getFlow` trả về một `AdaptyFlow` thay vì `AdaptyPaywall`, và cấu trúc đối tượng đã thay đổi: | Trường `AdaptyPaywall` v3 | Trường `AdaptyFlow` v4 | Hành động | |---|---|---| | `remoteConfig?` (đơn) | `remoteConfigs?: AdaptyRemoteConfig[]` (mảng) | Một flow mang một Remote Config cho mỗi ngôn ngữ đã cấu hình. Đọc ngôn ngữ phù hợp với người dùng: `flow.remoteConfigs?.find((c) => c.lang === 'en')`. | | `products` | `flow.paywalls[i].productIdentifiers` | Mã định danh sản phẩm giờ nằm trong từng biến thể flow, không nằm trên flow nữa. | | `webPurchaseUrl?` | `flow.paywalls[i].webPurchaseUrl` | Chuyển từ flow sang từng biến thể paywall. | | `version?: number` | `flowVersionId?: string` | Đổi tên, và kiểu dữ liệu thay đổi từ `number` sang `string`. | | `hasViewConfiguration` | đã xóa | Xóa mọi kiểm tra `hasViewConfiguration` khỏi code — `createFlowView` giờ sẽ throw thay thế (xem [Hiển thị flow](#displaying-flows)). | | `requestLocale` | đã xóa | Locale không còn là một phần của model nữa. | | _(mới)_ | `paywalls: AdaptyFlowPaywall[]` | Mỗi mục là một biến thể paywall trong flow. | | _(mới)_ | `responseCreatedAt: number` | Timestamp phản hồi từ server, tính bằng mili giây. | `hasViewConfiguration` và `requestLocale` vẫn nằm trên `AdaptyOnboarding` — chỉ có flow model là bỏ chúng. Mã định danh sản phẩm được chuyển từ flow sang từng variation: ```diff showLineNumbers - const ids = paywall.products; + const ids = flow.paywalls[0].productIdentifiers; ``` ## Phương thức Web Paywall \{#web-paywall-methods\} `openWebPaywall` và `createWebPaywallUrl` giữ nguyên tên, nhưng tùy chọn `paywallOrProduct` giờ nhận một `AdaptyFlowPaywall` (biến thể flow) thay vì `AdaptyPaywall`. Bạn vẫn có thể truyền vào `AdaptyPaywallProduct`. Hãy kiểm tra `flow.paywalls` không rỗng trước khi đọc phần tử đầu tiên: ```diff showLineNumbers const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID' }); - await adapty.openWebPaywall({ paywallOrProduct: paywall }); + await adapty.openWebPaywall({ paywallOrProduct: flow.paywalls[0] }); ``` ## Theo dõi lượt xem flow \{#tracking-flow-views\} ### logShowPaywall → logShowFlow `logShowPaywall` được đổi tên thành `logShowFlow` và giờ nhận vào một `AdaptyFlow`. Sự kiện vẫn được ghi lại theo cùng một variation, do đó các chỉ số funnel và A/B test hiện có vẫn hoạt động bình thường mà không cần thay đổi trên dashboard. ```diff showLineNumbers - await adapty.logShowPaywall({ paywall }); + await adapty.logShowFlow({ flow }); ``` Cũng giống như trong v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được dựng bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) — Adapty sẽ tự động theo dõi các lượt xem đó. ## Hiển thị flows \{#displaying-flows\} ### createPaywallView → createFlowView Đổi tên hàm factory và truyền vào `AdaptyFlow`. Controller trả về được đổi tên từ `PaywallViewController` thành `FlowViewController`, nhưng các phương thức của nó (`present`, `dismiss`, `setEventHandlers`, `showDialog`) vẫn giữ nguyên. Kiểu params được đổi tên từ `CreatePaywallViewParamsInput` thành `CreateFlowViewParamsInput`: ```diff showLineNumbers - import { createPaywallView } from '@adapty/capacitor'; + import { createFlowView } from '@adapty/capacitor'; - const view = await createPaywallView(paywall); + const view = await createFlowView(flow); await view.present(); ``` `createFlowView` sẽ throw một `AdaptyError` nếu flow chưa được cấu hình view — thay thế cho việc kiểm tra `hasViewConfiguration` ở v3: ```diff showLineNumbers - if (paywall.hasViewConfiguration) { - const view = await createPaywallView(paywall); - await view.present(); - } + try { + const view = await createFlowView(flow); + await view.present(); + } catch (error) { + // the flow has no view configured, or view creation failed + } ``` :::note Flow view chỉ dùng một lần: sau khi bạn gọi `dismiss()`, view sẽ bị hủy và các event handler của nó sẽ bị xóa, vì vậy hãy gọi lại `createFlowView` nếu muốn hiển thị flow thêm một lần nữa. ::: ### Vùng an toàn trên Android \{#android-safe-area-paddings\} `CreateFlowViewParamsInput` bổ sung một tham số mới: `enableSafeArea`, kiểm soát padding vùng an toàn trên Android khi chạy ứng dụng. Tham số này nằm trong khóa `android` và mặc định là `true`: ```typescript showLineNumbers const view = await createFlowView(flow, { android: { enableSafeArea: true }, }); ``` ## Xử lý sự kiện \{#handling-events\} Giao diện event-handler được đổi tên từ `EventHandlers` sang `FlowEventHandlers`, và một callback cũng được đổi tên. Phần thân handler hiện tại không cần thay đổi code — chỉ cần đổi tên: ```diff showLineNumbers - onRenderingFailed: (error) => { /* … */ }, + onError: (error) => { /* … */ }, ``` Tất cả các event handler khác vẫn giữ nguyên tên. Hai handler sau cũng nhận thêm tham số thứ hai: `onPurchaseCompleted` trở thành `(purchaseResult, product)` và `onPurchaseFailed` trở thành `(error, product)`, trong đó `product` là `AdaptyPaywallProduct` liên quan. Xem [Xử lý sự kiện flow & paywall](capacitor-handling-events) để biết danh sách đầy đủ. v4 cũng bổ sung thêm một số tính năng bạn có thể tùy chọn sử dụng: - Phương thức `adapty.openWebUrl({ url, openIn })` và `adapty.requestAppReview()` — hỗ trợ các handler mặc định `onUrlPress` và `onRequestAppReview`, giúp xử lý URL và yêu cầu đánh giá ứng dụng ngay mà không cần cấu hình thêm. Chỉ gọi trực tiếp khi bạn override các handler đó. - Xử lý giao dịch trong Observer mode bên trong flow thông qua các handler mới `onObserverPurchaseInitiated` / `onObserverRestoreInitiated`. Xem [Hiển thị flow trong Observer mode](capacitor-present-flows-in-observer-mode). ## Thay đổi hành vi mặc định \{#default-behavior-changes\} Những thay đổi này không gây ra lỗi biên dịch, vì vậy hãy kiểm tra chúng trong thời gian chạy: - **`onAndroidSystemBack`**: Mặc định đã thay đổi từ đóng view sang giữ nguyên. Để khôi phục hành vi cũ, hãy trả về `true` từ handler. - **`onPurchaseCompleted`**: Mặc định đã thay đổi từ đóng view (trừ khi người dùng hủy mua) sang luôn giữ nguyên. Để khôi phục hành vi cũ, hãy trả về `purchaseResult.type !== 'user_cancelled'` từ handler. - **`onRestoreCompleted`**: Mặc định đã thay đổi từ đóng view sau khi khôi phục thành công sang giữ nguyên. Để khôi phục hành vi cũ, hãy trả về `true` từ handler. - **`onUrlPress`**: Mặc định hiện mở URL thông qua native layer, tuân theo cài đặt trình duyệt in-app hoặc bên ngoài từ dashboard. Ghi đè handler để tự mở URL. - **View chỉ dùng một lần**: Sau khi gọi `dismiss()`, view sẽ bị hủy. Gọi lại `createFlowView` để hiển thị flow một lần nữa. ## Các API đã bị xóa \{#removed-apis\} ### Các export đã bị xóa \{#removed-exports\} Các symbol này không còn được export từ `@adapty/capacitor` nữa. Hãy xóa các import tương ứng: - **`AdaptyPaywall`**: Dùng `AdaptyFlow` và `AdaptyFlowPaywall` thay thế. - **`ProductReference`**: Dùng `AdaptyProductIdentifier`, đọc từ `flow.paywalls[i].productIdentifiers`. - **`AdaptyPaywallBuilder`**: Đã bị xóa. Flow và paywall được render trực tiếp trên thiết bị. - **`AdaptyAndroidSubscriptionUpdateParameters`**: Dùng cấu trúc tham số mua hàng `android` lồng nhau (xem bên dưới). ### activate: lockMethodsUntilReady `lockMethodsUntilReady` (đã bị deprecated và không hoạt động từ v3) đã được xóa. Hãy xóa nó khỏi lời gọi `activate` — giữ lại sẽ không thể biên dịch: ```diff showLineNumbers - await adapty.activate({ apiKey: 'PUBLIC_SDK_KEY', params: { lockMethodsUntilReady: true } }); + await adapty.activate({ apiKey: 'PUBLIC_SDK_KEY' }); ``` ### makePurchase: Tham số Android \{#makepurchase-android-parameters\} Kiểu dữ liệu phẳng (flat) đã bị deprecated của `MakePurchaseParamsInput` dành cho Android đã bị xóa — chỉ còn dạng lồng nhau (nested). Hãy chuyển các tham số mua hàng Android vào `params: { android: { ... } }`. Xem [Thực hiện mua hàng](capacitor-making-purchases) để xem ví dụ đầy đủ. ## API onboarding cũ không còn được hỗ trợ \{#onboarding-api-deprecation\} API onboarding cũ đã bị deprecated trong v4.0, thay thế bằng [Flow Builder](adapty-flow-builder). API này vẫn hoạt động, nhưng sẽ bị xóa trong phiên bản tương lai, vì vậy hãy lên kế hoạch migration các onboarding của bạn sang Flow Builder. Các symbol bị deprecated: `getOnboarding`, `getOnboardingForDefaultAudience`, `createOnboardingView`, và `OnboardingViewController`. --- # File: migration-to-capacitor-316 --- --- title: "Migrate Adapty Capacitor SDK to v3.16" description: "Migrate sang Adapty Capacitor SDK v3.16 để có hiệu suất tốt hơn và các tính năng kiếm tiền mới." --- Bắt đầu từ Adapty SDK v3.16.0, Capacitor 8 là bắt buộc. Nếu bạn cần Capacitor 7, hãy dùng Adapty SDK v3.15. Để nâng cấp lên Capacitor SDK v3.16, hãy đảm bảo dự án của bạn đang sử dụng Capacitor 8. Nếu bạn vẫn đang dùng Capacitor 7, bạn có hai lựa chọn: 1. **Nâng cấp lên Capacitor 8**: Làm theo [hướng dẫn migration chính thức của Capacitor](https://capacitorjs.com/docs/updating/8-0) để cập nhật dự án, sau đó cài đặt Adapty SDK v3.16. 2. **Tiếp tục dùng Adapty SDK v3.15**: Nếu việc nâng cấp lên Capacitor 8 chưa khả thi, hãy tiếp tục sử dụng Adapty SDK v3.15, phiên bản này hỗ trợ Capacitor 7. --- # End of Documentation _Generated on: 2026-07-24T13:01:43.409Z_ _Successfully processed: 45/45 files_ # FLUTTER - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.412Z Total files: 44 --- # File: sdk-installation-flutter --- --- title: "Cài đặt & cấu hình Flutter SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên Flutter cho các ứng dụng dựa trên gói đăng ký." --- Adapty SDK bao gồm hai module chính để tích hợp liền mạch vào ứng dụng Flutter của bạn: - **Core Adapty**: Đây là SDK cốt lõi bắt buộc để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), công cụ no-code thân thiện với người dùng giúp tạo paywall đa nền tảng một cách dễ dàng. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Yêu cầu \{#requirements\} Adapty SDK hỗ trợ iOS 13.0+, nhưng yêu cầu iOS 15.0+ để hoạt động đúng với các paywall được tạo trong Paywall Builder. Adapty Flutter SDK 4.0 — bổ sung hỗ trợ [Flow Builder](adapty-flow-builder) — nâng yêu cầu tối thiểu lên **iOS 15.0+**, **Xcode 26+** và **Flutter 3.32.0+** (Dart 3.8.0+). Xem [Adapty SDK 4.0](#adapty-sdk-40-swift-package-manager) bên dưới để biết chi tiết cài đặt. :::info Adapty tương thích với Google Play Billing Library lên đến phiên bản 8.x. Theo mặc định, Adapty hoạt động với Google Play Billing Library v7.0.0, nhưng nếu bạn muốn sử dụng phiên bản mới hơn, bạn có thể tự [thêm dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Flutter.svg?style=flat&logo=flutter)](https://github.com/adaptyteam/AdaptySDK-Flutter/releases) :::important Các bước dưới đây cài đặt SDK ổn định mới nhất (3.x). Nếu bạn cần v4 — bắt buộc cho [Flow Builder](adapty-flow-builder) và được sử dụng bởi [quickstart](flutter-quickstart-paywalls) — hãy làm theo [Adapty SDK 4.0: Swift Package Manager](#adapty-sdk-40-swift-package-manager) bên dưới. ::: 1. Thêm Adapty vào file `pubspec.yaml` của bạn: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: ^<the latest SDK version> ``` 2. Chạy lệnh sau để cài đặt các dependencies: ```bash showLineNumbers title="Terminal" flutter pub get ``` 3. Import Adapty SDK vào ứng dụng của bạn: ```dart showLineNumbers title="main.dart" import 'package:adapty_flutter/adapty_flutter.dart'; ``` ### Adapty SDK 4.0: Swift Package Manager Thêm Adapty Flutter SDK 4.0 — hỗ trợ [Flow Builder](adapty-flow-builder) — vào `pubspec.yaml` của bạn: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: 4.0.0 ``` Từ v4 trở đi, iOS SDK gốc không còn được phân phối qua CocoaPods nữa — plugin sẽ tải về thông qua **Swift Package Manager** ([kho spec của CocoaPods sẽ chuyển sang chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/)). Nếu bạn đang dùng Flutter 3.32–3.43, hãy bật hỗ trợ Swift Package Manager một lần: ```bash showLineNumbers title="Terminal" flutter config --enable-swift-package-manager ``` Flutter 3.44 trở lên đã bật Swift Package Manager theo mặc định, nên bạn không cần thực hiện thêm bước nào. Để xem các thay đổi API trong v4, tham khảo [hướng dẫn migration](migration-to-flutter-sdk-v4). ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Kích hoạt Adapty SDK trong code ứng dụng của bạn. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: Để lấy **Public SDK Key**: 1. Truy cập Adapty Dashboard và điều hướng đến [**App settings → General**](https://app.adapty.io/settings/general). 2. Trong phần **Api keys**, sao chép **Public SDK Key** (KHÔNG phải Secret Key). 3. Thay thế `"YOUR_PUBLIC_SDK_KEY"` trong code. Hoặc lấy theo cách lập trình, sử dụng [Adapty CLI](developer-cli): ``` npm install -g adapty adapty auth login adapty apps list ``` Hoặc, trực tiếp: ``` npx adapty auth login adapty apps list ``` - Đảm bảo bạn sử dụng **Public SDK key** để khởi tạo Adapty, **Secret key** chỉ nên dùng cho [server-side API](getting-started-with-server-side-api). - **SDK keys** là duy nhất cho mỗi ứng dụng, vì vậy nếu bạn có nhiều ứng dụng, hãy đảm bảo chọn đúng key. ```dart showLineNumbers title="main.dart" void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override void initState() { _initializeAdapty(); super.initState(); } Future<void> _initializeAdapty() async { try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY'), ); } catch (e) { // handle the error } } Widget build(BuildContext context) { return Text("Hello"); } } ``` :::important Hãy đợi `activate` hoàn tất trước khi gọi bất kỳ phương thức nào khác của Adapty SDK. Xem [Thứ tự gọi trong Flutter SDK](flutter-sdk-call-order) để biết toàn bộ trình tự. ::: Bây giờ hãy thiết lập paywall trong ứng dụng của bạn: - Nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), trước tiên hãy [kích hoạt module AdaptyUI](#activate-adaptyui-module-of-adapty-sdk) bên dưới, sau đó làm theo [hướng dẫn nhanh về Paywall Builder](flutter-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, xem [hướng dẫn nhanh cho paywall tùy chỉnh](flutter-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder) và đã [cài đặt module AdaptyUI](sdk-installation-flutter#install-adapty-sdk), bạn cũng cần kích hoạt AdaptyUI: :::note Các dependency liên quan đến AdaptyUI sẽ được liên kết với ứng dụng của bạn bất kể AdaptyUI có được kích hoạt hay không. ::: :::important Trong code của bạn, bạn phải kích hoạt module Adapty core trước khi kích hoạt AdaptyUI. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withActivateUI(true), // This automatically activates AdaptyUI ); ``` ## Thiết lập tùy chọn \{#optional-setup\} ### Ghi log \{#logging\} #### Thiết lập hệ thống ghi log \{#set-up-the-logging-system\} Adapty ghi lại các lỗi và thông tin quan trọng khác để giúp bạn hiểu điều gì đang xảy ra. Có các cấp độ sau: | Level | Mô tả | | :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | | `AdaptyLogLevel.error` | Chỉ ghi lại các lỗi. | | `AdaptyLogLevel.warn` | Ghi lại các lỗi và thông báo từ SDK không gây ra lỗi nghiêm trọng nhưng đáng chú ý. | | `AdaptyLogLevel.info` | Ghi lại các lỗi, cảnh báo và nhiều thông báo thông tin khác nhau. Giá trị mặc định. | | `AdaptyLogLevel.verbose` | Ghi lại mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như các lời gọi hàm, truy vấn API, v.v. | | `AdaptyLogLevel.debug` | Ghi lại thông tin debug. | Bạn có thể đặt mức độ log trong ứng dụng trước khi cấu hình Adapty: ```dart showLineNumbers title="main.dart" // Set log level before activation. // 'verbose' is recommended for development and the first production release await Adapty().setLogLevel(AdaptyLogLevel.verbose); // Or set it during configuration await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withLogLevel(AdaptyLogLevel.verbose), ); ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi lên, nhưng bạn có thể áp dụng các chính sách bảo mật dữ liệu bổ sung để tuân thủ quy định của cửa hàng hoặc từng quốc gia. #### Tắt tính năng thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi khởi tạo module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư cho người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm thiểu việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên địa chỉ IP không được yêu cầu trong ứng dụng của bạn. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withIpAddressCollectionDisabled(true), ); ``` #### Tắt tính năng thu thập và chia sẻ advertising ID \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `appleIdfaCollectionDisabled` (iOS) hoặc `googleAdvertisingIdCollectionDisabled` (Android) thành `true` để tắt việc thu thập mã định danh quảng cáo. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách của App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc nếu ứng dụng của bạn không cần attribution quảng cáo hay phân tích dựa trên ID quảng cáo. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleIdfaCollectionDisabled(true) // iOS ..withGoogleAdvertisingIdCollectionDisabled(true), // Android ); ``` #### Cấu hình bộ nhớ đệm media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Module này được kích hoạt tự động cùng với Adapty SDK. Nếu bạn không sử dụng Paywall Builder và muốn tắt module AdaptyUI, hãy truyền `withActivateUI(false)` trong quá trình kích hoạt. Theo mặc định, AdaptyUI lưu bộ nhớ đệm cho các tệp media (như hình ảnh và video) để cải thiện hiệu suất và giảm lưu lượng mạng. Bạn có thể tùy chỉnh cài đặt bộ nhớ đệm bằng cách cung cấp một cấu hình tùy chỉnh. Sử dụng `withMediaCacheConfiguration` để ghi đè các giới hạn bộ nhớ đệm mặc định. Đây là tùy chọn — nếu bạn không gọi phương thức này, các giá trị mặc định sẽ được dùng (100MB dung lượng đĩa, không giới hạn số lượng trong bộ nhớ). Tuy nhiên, nếu bạn tạo đối tượng cấu hình, tất cả các tham số của nó đều là bắt buộc. ```dart showLineNumbers title="main.dart" final mediaCacheConfig = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 200 * 1024 * 1024, // 200 MB memoryStorageCountLimit: 2147483647, // max int value diskStorageSizeLimit: 200 * 1024 * 1024, // 200 MB ); await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withMediaCacheConfiguration(mediaCacheConfig), ); ``` **Tham số:** | Tham số | Bắt buộc | Mô tả | |-------------------------|----------|-----------------------------------------------------------------------------| | memoryStorageTotalCostLimit | bắt buộc | Tổng kích thước cache trong bộ nhớ tính bằng byte. Mặc định là 100 MB. | | memoryStorageCountLimit | bắt buộc | Giới hạn số lượng item trong bộ nhớ. Mặc định là giá trị int tối đa. | | diskStorageSizeLimit | bắt buộc | Giới hạn kích thước file trên đĩa tính bằng byte. Mặc định là 100 MB. | ### Bật mức độ truy cập cục bộ (Android) \{#enable-local-access-levels-android\} Theo mặc định, [mức độ truy cập cục bộ](local-access-levels) được bật trên iOS và tắt trên Android. Để bật trên Android, hãy đặt `withGoogleLocalAccessLevelAllowed` thành `true`: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleLocalAccessLevelAllowed(true), ); ``` ### Xóa dữ liệu khi khôi phục từ backup \{#clear-data-on-backup-restore\} Khi `appleClearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ bản backup iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng đã cache, chi tiết sản phẩm và paywall. Sau đó SDK sẽ khởi tạo lại với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ có bộ nhớ cache cục bộ của SDK bị xóa. Lịch sử giao dịch với Apple và dữ liệu người dùng trên máy chủ Adapty vẫn không thay đổi. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleClearDataOnBackup(true) // default – false ); ``` ## Khắc phục sự cố \{#troubleshooting\} #### Quy tắc sao lưu Android (cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `<manifest>` có chứa tools: ```xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app"> ... </manifest> ``` #### 2. Ghi đè các thuộc tính backup trong `<application>` \{#2-override-backup-attributes-in-application\} Trong cùng file `AndroidManifest.xml`, cập nhật thẻ `<application>` để ứ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 <application android:name=".App" android:allowBackup="true" android:fullBackupContent="@xml/sample_backup_rules" android:dataExtractionRules="@xml/sample_data_extraction_rules" tools:replace="android:fullBackupContent,android:dataExtractionRules"> ... </application> ``` 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" <?xml version="1.0" encoding="utf-8"?> <data-extraction-rules> <cloud-backup> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </cloud-backup> <device-transfer> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </device-transfer> </data-extraction-rules> ``` **Dành cho Android 11 trở xuống** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" <?xml version="1.0" encoding="utf-8"?> <full-backup-content> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> #### Giao dịch mua thất bại sau khi quay lại từ ứng dụng khác trên Android \{#purchases-fail-after-returning-from-another-app-in-android\} Nếu Activity khởi động luồng mua hàng sử dụng `launchMode` không phải mặc định, Android có thể tạo lại hoặc tái sử dụng Activity đó không đúng cách khi người dùng quay lại từ Google Play, ứng dụng ngân hàng, hoặc trình duyệt. Điều này có thể khiến kết quả giao dịch mua bị mất hoặc bị coi là đã hủy. Để đảm bảo giao dịch mua hoạt động đúng, chỉ sử dụng chế độ khởi chạy `standard` hoặc `singleTop` cho Activity khởi động luồng mua hàng, và tránh các chế độ khác. Trong `AndroidManifest.xml`, hãy đảm bảo Activity khởi chạy flow mua hàng được đặt thành `standard` hoặc `singleTop`: ```xml <activity android:name=".MainActivity" android:launchMode="standard" /> ``` #### Lỗi build Swift 6 do Podfile ghi đè SWIFT_VERSION \{#swift-6-build-errors-caused-by-podfile-swift_version-override\} Khi build ứng dụng Flutter cho iOS, bạn có thể gặp lỗi biên dịch Swift 6 trên các target pod của Adapty. Các triệu chứng thường gặp bao gồm: lỗi không khớp `@Sendable` trong `AdaptyUIBuilderLogic`, thiếu conformance `Sendable` trên các kiểu của Adapty, hoặc lỗi actor isolation. Pod Adapty khai báo `s.swift_version = '6.0'` và yêu cầu Swift 6 để build. Code của app bạn vẫn có thể dùng Swift 5 — chỉ các pod target của Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`, `AdaptyLogger`, `AdaptyPlugin`) mới cần build với Swift 6. Nguyên nhân phổ biến nhất là hook `post_install` trong `ios/Podfile` ghi đè `SWIFT_VERSION` cho tất cả các pod target: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` **Cách khắc phục**: Loại trừ các pod target của Adapty khỏi phần ghi đè: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| next if %w[Adapty AdaptyUI AdaptyUIBuilder AdaptyLogger AdaptyPlugin].include?(target.name) target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` Sau đó chạy `pod install` từ thư mục `ios/` và build lại. Để kiểm tra, mở `ios/Pods/Pods.xcodeproj`, chọn target pod `Adapty` → **Build Settings** → **Swift Language Version**. Giá trị phải là **Swift 6**. --- # File: flutter-quickstart-paywalls --- --- title: "Kích hoạt mua hàng với Flow Builder trong Flutter SDK" description: "Hướng dẫn nhanh để kích hoạt in-app purchase với Adapty Flow Builder." --- Để kích hoạt in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – những gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flow**](adapty-flow-builder) – chuỗi màn hình hiển thị sản phẩm cho người dùng, được xây dựng bằng Flow Builder không cần code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện bằng code, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](flutter-quickstart-manual). - [**Placement**](placements) – vị trí và thời điểm hiển thị flow trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trên dashboard, sau đó yêu cầu chúng theo placement ID trong code. Cách này giúp bạn dễ dàng chạy A/B test và hiển thị các flow khác nhau cho từng nhóm người dùng. Adapty cung cấp cho bạn ba cách để kích hoạt tính năng mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Triển khai | Độ phức tạp | Khi nào nên dùng | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng để mua hàng trong trình tạo không cần code](quickstart-paywalls). Adapty tự động hiển thị và xử lý toàn bộ luồng mua hàng phức tạp, xác thực biên lai và quản lý gói đăng ký. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự xây dựng giao diện paywall trong code ứng dụng, nhưng vẫn lấy đối tượng flow từ Adapty để duy trì sự linh hoạt trong danh sách sản phẩm. Xem [hướng dẫn](flutter-quickstart-manual). | | Chế độ Observer | 🔴 Khó | Bạn đã có hạ tầng xử lý mua hàng riêng và muốn tiếp tục dùng nó. Lưu ý rằng chế độ observer có một số hạn chế trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, hãy xem [Triển khai paywall thủ công](flutter-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code của ứng dụng, bạn chỉ cần: 1. **Lấy flow**: Lấy flow đó từ Adapty. 2. **Hiển thị flow và Adapty sẽ xử lý các giao dịch mua cho bạn**: Hiển thị view trong ứng dụng của bạn. 3. **Xử lý các hành động nút bấm**: Liên kết các tương tác của người dùng với phản hồi tương ứng trong ứng dụng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. Kết nối ứng dụng của bạn với [App Store](initial_ios) và/hoặc [Google Play](initial-android) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo flow và thêm sản phẩm vào đó](create-paywall). 4. [Tạo placement và thêm flow vào đó](create-placement). 5. [Cài đặt và kích hoạt Adapty SDK](sdk-installation-flutter) trong mã ứng dụng của bạn. Hướng dẫn này sử dụng API của Adapty Flutter SDK v4. :::tip Cách nhanh nhất để hoàn thành các bước này là làm theo [hướng dẫn quickstart](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy flow \{#1-get-the-flow\} Các flow của bạn được liên kết với các placement được cấu hình trong dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc để chạy [A/B test](ab-tests). Để lấy một flow đã tạo trong Adapty Flow Builder, bạn cần: 1. Lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow` và kiểm tra xem nó có được tạo trong builder hay không thông qua thuộc tính `hasViewConfiguration`. 2. Tạo flow view bằng phương thức `createFlowView`. View chứa các phần tử UI và style cần thiết để hiển thị flow. :::important Để lấy cấu hình view, bạn phải bật toggle **Show on device** trong builder. Nếu không, bạn sẽ nhận được cấu hình view rỗng và flow sẽ không được hiển thị. ::: ```dart showLineNumbers try { // the requested flow final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final view = await AdaptyUI().createFlowView( flow: flow, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## 2. Hiển thị flow \{#display-the-flow\} Bây giờ, khi bạn đã có flow view, chỉ cần thêm vài dòng code để hiển thị nó. Để hiển thị flow, sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức `createFlowView`. Mỗi `view` chỉ có thể được hiển thị một lần: sau khi bạn đóng nó, view sẽ được giải phóng khỏi bộ nhớ. Nếu bạn cần hiển thị lại flow, hãy gọi `createFlowView` thêm một lần nữa để tạo một `view` instance mới. ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip Để biết thêm chi tiết về cách hiển thị một flow, hãy xem [hướng dẫn](flutter-present-paywalls) của chúng tôi. ::: ## 3. Xử lý các hành động nút bấm \{#handle-button-actions\} Khi người dùng nhấn các nút trong flow, Flutter SDK tự động xử lý các thao tác mua hàng, khôi phục, đóng màn hình và mở URL. Tuy nhiên, một số nút có ID tùy chỉnh hoặc được định sẵn và cần bạn xử lý trong code. Để kiểm soát hoặc theo dõi các tiến trình trên màn hình flow, hãy implement các phương thức của `AdaptyUIFlowsEventsObserver` và thiết lập observer trước khi hiển thị bất kỳ màn hình nào. Khi người dùng thực hiện một hành động, `flowViewDidPerformAction` sẽ được gọi, và ứng dụng của bạn cần phản hồi tương ứng với action ID đó. Có ba phương thức observer **bắt buộc**: `flowViewDidFinishPurchase`, `flowViewDidFinishRestore`, và `flowViewDidReceiveError` — class của bạn sẽ không thể biên dịch nếu thiếu chúng. :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [actions](flutter-handle-paywall-actions) và [events](flutter-handling-events) của nút bấm. ::: Triển khai observer dưới dạng một đối tượng độc lập, tồn tại lâu dài thay vì gắn vào widget. Vì toàn bộ ứng dụng chỉ dùng chung một slot observer duy nhất, nếu gắn vào `State` sẽ gây rò rỉ màn hình (SDK giữ một strong reference đến nó) và sẽ bị thay thế âm thầm khi màn hình tiếp theo đăng ký. Sử dụng `extends` cũng kế thừa hành vi mặc định của SDK, vì vậy ngoài ba phương thức bắt buộc, bạn chỉ cần override những callback mà bạn quan tâm. ```dart showLineNumbers title="Flutter" // A dedicated, long-lived handler for flow events. // It does NOT live inside a Widget/State, so it never leaks and is never // silently replaced when screens are pushed or popped. class FlowEventsHandler extends AdaptyUIFlowsEventsObserver { // A single, app-wide instance — same idiom as Adapty() and AdaptyUI(). static final FlowEventsHandler _instance = FlowEventsHandler._(); factory FlowEventsHandler() => _instance; FlowEventsHandler._(); // This method is called when user performs an action on the flow UI. // Overriding it replaces the default behavior (dismiss on close, open URLs), // so keep those cases if you want to preserve it. @override void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): // close the flow on the Android back button view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } // Required: decide what happens after a purchase finishes @override void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { if (purchaseResult is! AdaptyPurchaseResultUserCancelled) { view.dismiss(); } } // Required: dismiss the flow once a restore succeeds @override void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { view.dismiss(); } // Required: handle rendering and other view errors @override void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { print('Flow error: $error'); view.dismiss(); } } ``` Đăng ký handler **một lần** khi khởi động ứng dụng, trước khi hiển thị bất kỳ flow nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setFlowsEventsObserver(FlowEventsHandler()); ``` ## Bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra mua hàng trong [sandbox App Store](test-purchases-in-sandbox) hoặc [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một lần mua thử từ paywall. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](flutter-check-subscription-status) để đảm bảo bạn hiển thị paywall hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Dưới đây là cách tích hợp tất cả các bước trên vào ứng dụng của bạn. ```dart void main() { // Register a single, long-lived observer once, before any flow is shown. // It is intentionally a plain object (NOT a Widget/State): its lifetime is the // whole app, so it never leaks and is never silently replaced when screens are // pushed or popped. AdaptyUI().setFlowsEventsObserver(FlowEventsHandler()); runApp(MaterialApp(home: FlowScreen())); } /// A dedicated handler for AdaptyUI flow events. /// /// It `extends` [AdaptyUIFlowsEventsObserver] (rather than being implemented /// by a `State`), which gives you two things for free: /// * the SDK's sensible defaults for optional callbacks, so besides the three /// required methods you only override what you actually care about; /// * a lifecycle that is independent of the widget tree — there is no strong /// reference back into a `Widget`, so nothing leaks and there is nothing to /// unregister. /// /// Every callback receives the [AdaptyUIFlowView] it relates to, so handling /// flow actions never requires a `BuildContext` or widget state. class FlowEventsHandler extends AdaptyUIFlowsEventsObserver { // A single, app-wide instance — same idiom as Adapty() and AdaptyUI(). static final FlowEventsHandler _instance = FlowEventsHandler._(); factory FlowEventsHandler() => _instance; FlowEventsHandler._(); // Called when the user performs an action on the flow UI. @override void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): // close the flow on the Android back button view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): // Open the URL natively, honoring the dashboard browser setting. AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } // Required: decide what happens after a purchase finishes. @override void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { if (purchaseResult is! AdaptyPurchaseResultUserCancelled) { view.dismiss(); } } // Required: dismiss the flow once a restore succeeds. @override void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { view.dismiss(); } // Required: handle rendering and other view errors. @override void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { print('Flow error: $error'); view.dismiss(); } } class FlowScreen extends StatefulWidget { const FlowScreen({super.key}); @override State<FlowScreen> createState() => _FlowScreenState(); } class _FlowScreenState extends State<FlowScreen> { @override void initState() { super.initState(); _showFlowIfNeeded(); } Future<void> _showFlowIfNeeded() async { try { final flow = await Adapty().getFlow( placementId: 'YOUR_PLACEMENT_ID', ); if (!flow.hasViewConfiguration) return; final view = await AdaptyUI().createFlowView(flow: flow); await view.present(); } catch (_) { // Handle any errors (network, SDK issues, etc.) } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Adapty Flow Example')), body: Center( // Add a button to re-trigger the flow for testing purposes. child: ElevatedButton( onPressed: _showFlowIfNeeded, child: const Text('Show Flow'), ), ), ); } } ``` --- # File: flutter-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong Flutter SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Flutter của bạn với Adapty." --- Để quyết định xem người dùng có thể truy cập nội dung trả phí hay cần xem paywall, bạn cần kiểm tra [mức độ truy cập](access-level) của họ trong hồ sơ người dùng. Bài viết này hướng dẫn bạn cách truy cập trạng thái hồ sơ người dùng để quyết định nên hiển thị gì cho họ — paywall hay nội dung trả phí. ## Lấy trạng thái gói đăng ký \{#get-subscription-status\} Khi quyết định hiển thị paywall hay nội dung trả phí cho người dùng, bạn kiểm tra [mức độ truy cập](access-level) trong hồ sơ của họ. Có hai lựa chọn: - Gọi `getProfile` khi cần dữ liệu hồ sơ mới nhất ngay lập tức (ví dụ: khi khởi động ứng dụng) hoặc muốn buộc cập nhật. - Thiết lập **cập nhật hồ sơ tự động** để giữ một bản sao cục bộ được tự động làm mới mỗi khi trạng thái gói đăng ký thay đổi. ### Lấy hồ sơ người dùng \{#get-profile\} Cách đơn giản nhất để lấy trạng thái gói đăng ký là dùng phương thức `getProfile`: ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Lắng nghe các cập nhật gói đăng ký \{#listen-to-subscription-updates\} Để tự động nhận cập nhật hồ sơ người dùng trong ứng dụng: 1. Sử dụng `Adapty().didUpdateProfileStream.listen()` để lắng nghe các thay đổi của hồ sơ người dùng — Adapty sẽ tự động gọi phương thức này mỗi khi trạng thái gói đăng ký của người dùng thay đổi. 2. Lưu dữ liệu hồ sơ người dùng đã cập nhật khi phương thức này được gọi, để bạn có thể sử dụng trong toàn bộ ứng dụng mà không cần thực hiện thêm các yêu cầu mạng. ```dart class SubscriptionManager { AdaptyProfile? _currentProfile; SubscriptionManager() { // Listen for profile updates Adapty().didUpdateProfileStream.listen((profile) { _currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() bool hasAccess() { return _currentProfile?.accessLevels['premium']?.isActive ?? false; } } ``` :::note Adapty tự động gọi stream listener cập nhật hồ sơ người dùng khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký được lưu trong bộ nhớ cache ngay cả khi thiết bị ngoại tuyến. ::: ## Kết nối hồ sơ người dùng với logic paywall \{#connect-profile-with-paywall-logic\} Khi bạn cần đưa ra quyết định ngay lập tức về việc hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách tiếp cận này hữu ích trong các tình huống như khi khởi chạy ứng dụng, khi người dùng truy cập vào các phần premium, hoặc trước khi hiển thị nội dung cụ thể. ```dart Future<bool> _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<void> _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. ::: <img src="/assets/shared/img/identify-diagram.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Trong quá trình đăng nhập/đăng ký \{#during-loginsignup\} Nếu bạn xác định người dùng sau khi ứng dụng khởi chạy (ví dụ: sau khi họ đăng nhập hoặc đăng ký), hãy sử dụng phương thức `identify` để thiết lập customer user ID của họ. - Nếu bạn **chưa sử dụng customer user ID này trước đây**, Adapty sẽ tự động liên kết nó với hồ sơ hiện tại. - Nếu bạn **đã sử dụng customer user ID này để xác định người dùng trước đây**, Adapty sẽ chuyển sang làm việc với hồ sơ được liên kết với customer user ID này. :::important Customer user ID phải là duy nhất cho mỗi người dùng. Nếu bạn hardcode giá trị tham số, tất cả người dùng sẽ được coi là một người. ::: Luôn `await` `identify` trước khi gọi các phương thức SDK khác. Các lệnh gọi đồng thời sẽ tạo ra lỗi `#3006 profileWasChanged` hoặc sẽ trỏ đến hồ sơ ẩn danh. Xem [Thứ tự gọi trong Flutter SDK](flutter-sdk-call-order). ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Trong quá trình kích hoạt SDK \{#during-the-sdk-activation\} Nếu bạn đã biết customer user ID khi kích hoạt SDK, bạn có thể gửi nó trong phương thức `activate` thay vì gọi `identify` riêng. Nếu bạn biết customer user ID nhưng chỉ thiết lập nó sau khi kích hoạt, điều đó có nghĩa là khi kích hoạt, Adapty sẽ tạo một hồ sơ ẩn danh mới và chỉ chuyển sang hồ sơ hiện có sau khi bạn gọi `identify`. Bạn có thể truyền một customer user ID hiện có (cái bạn đã sử dụng trước đây) hoặc một cái mới. Nếu bạn truyền một cái mới, hồ sơ mới được tạo khi kích hoạt sẽ tự động được liên kết với customer user ID đó. :::note Theo mặc định, việc tạo hồ sơ ẩn danh không ảnh hưởng đến các dashboard analytics, vì số lần cài đặt được tính dựa trên device ID. Một device ID đại diện cho một lần cài đặt ứng dụng từ cửa hàng trên thiết bị và chỉ được tạo lại sau khi ứng dụng được cài đặt lại. Nó không phụ thuộc vào việc đây là lần cài đặt đầu tiên hay lần cài đặt lại, hoặc liệu có sử dụng customer user ID hiện có hay không. Việc tạo hồ sơ (khi kích hoạt SDK hoặc đăng xuất), đăng nhập, hoặc nâng cấp ứng dụng mà không cài đặt lại không tạo ra thêm sự kiện cài đặt. Nếu bạn muốn đếm số lần cài đặt dựa trên người dùng duy nhất thay vì thiết bị, hãy vào **App settings** và cấu hình [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```dart showLineNumbers" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ); } catch (e) { // handle the error } ``` ### Đăng xuất người dùng \{#log-users-out\} Nếu bạn có nút để đăng xuất người dùng, hãy sử dụng phương thức `logout`. :::important Đăng xuất người dùng sẽ tạo một hồ sơ ẩn danh mới cho người dùng đó. ::: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::info Để đăng nhập lại người dùng vào ứng dụng, hãy sử dụng phương thức `identify`. ::: ### Cho phép mua hàng mà không cần đăng nhập \{#allow-purchases-without-login\} Nếu người dùng của bạn có thể thực hiện giao dịch mua cả trước và sau khi đăng nhập vào ứng dụng, bạn cần đảm bảo rằng họ sẽ vẫn giữ được quyền truy cập sau khi đăng nhập: 1. Khi người dùng chưa đăng nhập thực hiện giao dịch mua, Adapty liên kết nó với ID hồ sơ ẩn danh của họ. 2. Khi người dùng đăng nhập vào tài khoản của họ, Adapty chuyển sang làm việc với hồ sơ đã xác định của họ. - Nếu đây là customer user ID mới (ví dụ: giao dịch mua được thực hiện trước khi đăng ký), Adapty sẽ gán customer user ID cho hồ sơ hiện tại, do đó toàn bộ lịch sử giao dịch mua được duy trì. - Nếu đây là customer user ID hiện có (customer user ID đã được liên kết với một hồ sơ), bạn cần lấy mức độ truy cập thực tế sau khi chuyển hồ sơ. Bạn có thể gọi [`getProfile`](flutter-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe các cập nhật hồ sơ](flutter-check-subscription-status) để dữ liệu tự động đồng bộ. ## Các bước tiếp theo \{#next-steps\} Chúc mừng! Bạn đã triển khai logic thanh toán trong ứng dụng! Chúc bạn thành công với việc kiếm tiền từ ứng dụng! Để tận dụng Adapty nhiều hơn, bạn có thể khám phá các chủ đề sau: - [**Kiểm thử**](troubleshooting-test-purchases): Đảm bảo mọi thứ hoạt động như mong đợi - [**Onboarding**](flutter-onboardings): Thu hút người dùng bằng onboarding và tăng tỷ lệ giữ chân - [**Tích hợp**](configuration): Tích hợp với các dịch vụ attribution marketing và analytics chỉ trong một dòng code - [**Thiết lập thuộc tính hồ sơ tùy chỉnh**](flutter-setting-user-attributes): Thêm thuộc tính tùy chỉnh vào hồ sơ người dùng và tạo phân khúc, giúp bạn có thể chạy A/B test hoặc hiển thị các paywall khác nhau cho các nhóm người dùng khác nhau --- # File: adapty-sdk-integration-skill-flutter --- --- title: "Tích hợp Adapty vào ứng dụng Flutter với kỹ năng tích hợp SDK" description: "Sử dụng kỹ năng adapty-sdk-integration để tích hợp Adapty SDK vào ứng dụng Flutter của bạn từ đầu đến cuối với công cụ lập trình AI." --- :::important Kỹ năng này đang trong giai đoạn beta. Nếu nó bị treo hoặc hoạt động không như mong đợi, hãy làm theo [hướng dẫn tích hợp từng bước](adapty-cursor-flutter) — hướng dẫn này sẽ dẫn dắt công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn form phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt xong, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-flutter --- --- title: "Tích hợp Adapty vào ứng dụng Flutter của bạn với sự hỗ trợ của AI" description: "Hướng dẫn từng bước tích hợp Adapty vào ứng dụng Flutter của bạn bằng Cursor, Context7, ChatGPT, Claude hoặc các công cụ AI khác." --- Hướng dẫn này sẽ giúp bạn tích hợp Adapty vào ứng dụng Flutter từng bước với công cụ lập trình AI — bạn cung cấp cho nó đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ code SDK nào. Bạn có thể thực hiện điều này bằng một LLM skill tương tác, hoặc thủ công thông qua Dashboard. ### Cách dùng Skill (khuyến nghị) \{#skill-approach-recommended\} Adapty CLI skill cho phép LLM của bạn thiết lập app, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối các cửa hàng](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm skill, chạy `/adapty-cli` trong agent của bạn. Nó sẽ hướng dẫn bạn qua từng bước — bao gồm cả lúc cần mở Dashboard để kết nối các cửa hàng. ### Cách thiết lập thủ công trên Dashboard \{#dashboard-approach\} Nếu bạn muốn tự cấu hình mọi thứ, đây là những gì cần có trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard — bạn sẽ cần tự cung cấp chúng. 1. **Kết nối các cửa hàng ứng dụng**: Trong Adapty Dashboard, vào **App settings → General**. Kết nối cả App Store và Google Play nếu ứng dụng Flutter của bạn hỗ trợ cả hai nền tảng. Đây là điều bắt buộc để các giao dịch mua hoạt động. [Kết nối cửa hàng ứng dụng](integrate-payments) 2. **Sao chép Public SDK key**: Trong Adapty Dashboard, vào **App settings → General**, sau đó tìm phần **API keys**. Trong code, đây là chuỗi bạn truyền vào cấu hình Adapty. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu sản phẩm trực tiếp trong code — Adapty cung cấp chúng thông qua paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo một paywall và một placement**: Trong Adapty Dashboard, tạo paywall trên trang **Paywalls**, sau đó gán nó vào một placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty().getPaywall()`. [Tạo paywall](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình cho từng sản phẩm trên trang **Products**. Trong code, chuỗi được kiểm tra trong `profile.accessLevels['premium']?.isActive`. Mức độ truy cập `premium` mặc định phù hợp với hầu hết các ứng dụng. Nếu người dùng trả phí có quyền truy cập vào các tính năng khác nhau tùy theo sản phẩm (ví dụ: gói `basic` so với gói `pro`), hãy [tạo thêm mức độ truy cập](assigning-access-level-to-a-product) trước khi bắt đầu viết code. :::tip Khi đã có đủ năm điều trên, bạn đã sẵn sàng để viết code. Hãy nói với LLM của bạn: "Public SDK key của tôi là X, placement ID của tôi là Y" để nó có thể tạo code khởi tạo và lấy paywall chính xác. ::: ### Thiết lập khi sẵn sàng \{#set-up-when-ready\} Những mục này không bắt buộc để bắt đầu viết code, nhưng bạn sẽ cần chúng khi tích hợp trưởng thành hơn: - **A/B test**: Cấu hình trên trang **Placements**. Không cần thay đổi code. [A/B test](ab-tests) - **Thêm paywall và placement**: Thêm các lời gọi `getPaywall` với các placement ID khác nhau. - **Tích hợp analytics**: Cấu hình trên trang **Integrations**. Cách thiết lập khác nhau tùy theo tích hợp. Xem [tích hợp analytics](analytics-integration) và [tích hợp attribution](attribution-integration). ## Cung cấp tài liệu Adapty cho LLM của bạn \{#feed-adapty-docs-to-your-llm\} ### Dùng Context7 (khuyến nghị) \{#use-context7-recommended\} [Context7](https://context7.com) là một MCP server cung cấp cho LLM của bạn quyền truy cập trực tiếp vào tài liệu Adapty luôn cập nhật. LLM của bạn tự động lấy đúng tài liệu dựa trên những gì bạn hỏi — không cần dán URL thủ công. Context7 hoạt động với **Cursor**, **Claude Code**, **Windsurf** và các công cụ tương thích MCP khác. Để thiết lập, chạy: ``` npx ctx7 setup ``` Lệnh này phát hiện trình soạn thảo của bạn và cấu hình Context7 server. Để thiết lập thủ công, xem [kho GitHub Context7](https://github.com/upstash/context7). Sau khi cấu hình, tham chiếu thư viện Adapty trong các prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the Flutter SDK ``` :::warning Dù Context7 không còn cần dán link tài liệu thủ công, thứ tự triển khai vẫn quan trọng. Hãy làm theo [hướng dẫn triển khai](#implementation-walkthrough) bên dưới từng bước để đảm bảo mọi thứ hoạt động đúng. ::: ### Dùng tài liệu dạng plain text \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng Markdown thuần túy. Thêm `.md` vào cuối URL, hoặc nhấn **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor-flutter.md](https://adapty.io/docs/vi/adapty-cursor-flutter.md). Mỗi bước trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới đều có khối "Gửi cho LLM của bạn" với các link `.md` để dán vào. Để xem thêm tài liệu cùng lúc, xem [các file index và tập hợp theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này sẽ đi qua việc tích hợp Adapty theo thứ tự triển khai. Mỗi bước bao gồm tài liệu cần gửi cho LLM, những gì bạn cần thấy khi hoàn thành và các vấn đề thường gặp. ### Lên kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết code, hãy yêu cầu LLM phân tích dự án của bạn và tạo kế hoạch triển khai. Nếu công cụ AI của bạn hỗ trợ chế độ lập kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy sử dụng nó để LLM có thể đọc cả cấu trúc dự án lẫn tài liệu Adapty trước khi viết bất kỳ code nào. Hãy cho LLM biết cách bạn xử lý giao dịch mua — điều này ảnh hưởng đến các hướng dẫn mà nó cần theo dõi: - [**Adapty Paywall Builder**](adapty-paywall-builder): Bạn tạo paywall trong trình xây dựng không cần code của Adapty, và SDK tự động hiển thị chúng. - [**Paywall tự tạo**](flutter-making-purchases): Bạn tự xây dựng giao diện paywall trong code nhưng vẫn dùng Adapty để lấy sản phẩm và xử lý giao dịch mua. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên hạ tầng mua hàng hiện có và chỉ dùng Adapty cho analytics và tích hợp. Chưa biết chọn cái nào? Đọc [bảng so sánh trong quickstart](flutter-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm dependency Adapty SDK bằng `flutter pub add` và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — không có gì khác hoạt động được nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-flutter) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-flutter.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Ứng dụng build và chạy được trên cả iOS và Android. Console debug hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra xem bạn đã thay thế placeholder bằng key thực của mình từ App settings chưa. ::: ### Hiển thị paywall và xử lý giao dịch mua \{#show-paywalls-and-handle-purchases\} Lấy paywall theo placement ID, hiển thị nó và xử lý các sự kiện mua hàng. Các hướng dẫn bạn cần phụ thuộc vào cách bạn xử lý giao dịch mua. Kiểm thử từng giao dịch mua trong sandbox khi bạn làm — đừng đợi đến cuối. Xem [Kiểm thử giao dịch mua trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. <Tabs groupId="paywall-approach"> <TabItem value="builder" label="Paywall Builder" default> **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. ::: </TabItem> <TabItem value="manual" label="Paywall tự tạo"> **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. ::: </TabItem> <TabItem value="observer" label="Observer mode"> **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. ::: </TabItem> </Tabs> ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua, kiểm tra hồ sơ người dùng để xác nhận mức độ truy cập đang hoạt động nhằm chặn nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](flutter-check-subscription-status) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/flutter-check-subscription-status.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau giao dịch mua sandbox, `profile.accessLevels['premium']?.isActive` trả về `true`. - **Lưu ý:** `accessLevels` trống sau khi mua → kiểm tra xem sản phẩm đã được gán mức độ truy cập trong dashboard chưa. ::: ### Xác định người dùng \{#identify-users\} Liên kết tài khoản người dùng trong ứng dụng của bạn với hồ sơ người dùng Adapty để giao dịch mua được duy trì trên nhiều thiết bị. :::important Bỏ qua bước này nếu ứng dụng của bạn không có xác thực. ::: **Hướng dẫn:** [Xác định người dùng](flutter-quickstart-identify) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/flutter-quickstart-identify.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi gọi `Adapty().identify()`, phần **Profiles** trên dashboard hiển thị custom user ID của bạn. - **Lưu ý:** Gọi `identify` sau khi kích hoạt nhưng trước khi lấy paywall để tránh attribution hồ sơ ẩn danh. ::: ### Chuẩn bị phát hành \{#prepare-for-release\} Khi tích hợp của bạn hoạt động tốt trong sandbox, hãy đi qua danh sách kiểm tra phát hành để đảm bảo mọi thứ đã sẵn sàng cho môi trường sản xuất. **Hướng dẫn:** [Danh sách kiểm tra phát hành](release-checklist) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/vi/release-checklist.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Tất cả các mục trong danh sách đã được xác nhận: kết nối cửa hàng, thông báo server, flow mua hàng, kiểm tra mức độ truy cập và yêu cầu quyền riêng tư. - **Lưu ý:** Thiếu thông báo server → cấu hình App Store Server Notifications trong **App settings → iOS SDK** và Google Play Real-Time Developer Notifications trong **App settings → Android SDK**. ::: ## Các file index tài liệu dạng plain text \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM nhiều ngữ cảnh hơn ngoài các trang riêng lẻ, chúng tôi cung cấp các file index liệt kê hoặc tổng hợp toàn bộ tài liệu Adapty: - [`llms.txt`](https://adapty.io/docs/vi/llms.txt): Liệt kê tất cả các trang với link `.md`. Đây là [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho các website dễ tiếp cận với LLM. Lưu ý rằng với một số AI agent (ví dụ: ChatGPT), bạn sẽ cần tải `llms.txt` về và tải lên chat dưới dạng file. - [`llms-full.txt`](https://adapty.io/docs/vi/llms-full.txt): Toàn bộ trang tài liệu Adapty được gộp vào một file duy nhất. Rất lớn — chỉ dùng khi bạn cần toàn bộ thông tin. - Tập hợp dành riêng cho Flutter [`flutter-llms.txt`](https://adapty.io/docs/vi/flutter-llms.txt) và [`flutter-llms-full.txt`](https://adapty.io/docs/vi/flutter-llms-full.txt): Tập hợp theo nền tảng giúp tiết kiệm token so với toàn bộ site. --- # File: flutter-get-pb-paywalls --- --- title: "Lấy flows & paywalls - Flutter" description: "Lấy flows và paywalls từ Adapty trong ứng dụng Flutter của bạn." --- <SDKv4> <MethodPromo method="getFlow" /> Sau khi [bạn đã thiết kế flow hoặc paywall bằng Paywall Builder](adapty-paywall-builder), bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên là lấy flow hoặc paywall liên kết với placement cùng cấu hình view như mô tả bên dưới. Lưu ý rằng chủ đề này đề cập đến các flow và paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall theo cách thủ công, vui lòng tham khảo chủ đề [Lấy paywall và sản phẩm cho paywall remote config trong ứng dụng di động của bạn](fetch-paywalls-and-products-flutter). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu hiển thị flow và paywall trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow/paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-flutter) vào ứng dụng mobile của bạn. </details> ## Lấy flow/paywall \{#fetch-flowpaywall\} Nếu bạn đã thiết kế một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới hiển thị nó trong ứng dụng. Tải flow hoặc paywall và tạo [view](flutter-get-pb-paywalls#fetch-the-view-configuration) cho nó càng sớm càng tốt — lý tưởng nhất là trước khi hiển thị. Phương thức `createFlowView` tải cấu hình view và bắt đầu tải xuống, lưu cache hình ảnh ở nền. Bạn gọi nó càng sớm thì hình ảnh càng có nhiều thời gian để tải xong. Đến lúc hiển thị flow hoặc paywall, cấu hình và hình ảnh của nó có thể đã được cache sẵn sàng hiển thị. Để lấy flow hoặc paywall, dùng phương thức `getFlow`: ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow/paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Các tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu người dùng của bạn thường xuyên gặp vấn đề về kết nối internet, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet của họ như thế nào. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</p> | | **loadTimeout** | mặc định: 5 giây | <p>Một `Duration` giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | ## Tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Flow | Một đối tượng `AdaptyFlow` chứa các định danh của flow (`instanceIdentity`, `variationId`), tên, placement, các biến thể paywall (`paywalls`), và các Remote Config (`remoteConfigs`). | ## Lấy cấu hình view \{#fetch-the-view-configuration\} :::important Hãy đảm bảo bạn đã bật toggle **Show on device** trong builder. Nếu tùy chọn này chưa được bật, cấu hình view sẽ không thể truy xuất được. ::: Nếu placement được thiết kế trong **Flow Builder** hoặc **Paywall Builder**, Adapty sẽ tự động render giao diện — thuộc tính `hasViewConfiguration` của flow được tải về sẽ là `true`. Tạo view bằng `createFlowView`, sau đó [hiển thị flow hoặc paywall](flutter-present-paywalls). Nếu placement là một paywall tùy chỉnh không có giao diện Builder (`hasViewConfiguration` là `false`), hãy [xử lý nó như một remote config paywall](present-remote-config-paywalls-flutter). :::warning Kết quả của phương thức `createFlowView` chỉ có thể được hiển thị một lần. Nếu bạn cần hiển thị lại, hãy gọi lại phương thức `createFlowView` từ đầu. ::: ```dart showLineNumbers try { final view = await AdaptyUI().createFlowView(flow: flow); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :------------------- | :------- | :----------------------------------------------------------- | | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` để lấy view cho flow/paywall mong muốn. | | **customTags** | tùy chọn | Định nghĩa một map các custom tag và giá trị tương ứng. Custom tag đóng vai trò là placeholder trong nội dung, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong flow/paywall. Xem thêm tại chủ đề [Custom tags in paywall builder](custom-tags-in-paywall-builder). | | **preloadProducts** | tùy chọn | Bật tùy chọn này để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Khi đặt là `true`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. Mặc định: `false`. | | **loadTimeout** | tùy chọn | Một `Duration` giới hạn thời gian tải cấu hình view. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được sử dụng. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa flow](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](flutter-localizations-and-locale-codes). ::: Sau khi có view, hãy [hiển thị flow/paywall](flutter-present-paywalls). ## Lấy flow hoặc paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, flow và paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow hoặc paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị flow hoặc paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow hoặc paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow hoặc paywall bằng phương thức `getFlow`, như được mô tả chi tiết trong phần [Lấy flow/paywall](#fetch-flowpaywall) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (phiên bản hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (legacy) hoặc chấp nhận rằng người dùng có phiên bản hiện tại (legacy) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow hoặc paywall nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` được mô tả [ở trên](#fetch-flowpaywall). ::: ```dart showLineNumbers try { final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow/paywall } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu người dùng của bạn thường gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn dù kết nối mạng chập chờn. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các request mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh asset \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong flow/paywall của bạn, hãy triển khai custom asset. Hero image và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị ảnh xem trước trước khi phát video. Here's an example of how you can provide custom assets via a simple dictionary: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createFlowView( flow: flow, customAssets: customAssets, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note Nếu không tìm thấy tài nguyên, flow/paywall sẽ trở về giao diện mặc định của nó. ::: ## Cài đặt timer tùy chỉnh do developer định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng timer tùy chỉnh trong ứng dụng di động của bạn, hãy truyền một map `customTimers` vào phương thức `createFlowView`. Mỗi key trong map là một timer ID, và giá trị tương ứng là một đối tượng `DateTime` xác định thời điểm timer kết thúc. Dưới đây là ví dụ: ```dart showLineNumbers try { final view = await AdaptyUI().createFlowView( flow: flow, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2027, 1, 1), // New Year 2027 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là **Timer ID** của các timer do developer định nghĩa mà bạn đã thiết lập trong Adapty Dashboard. Map `customTimers` đảm bảo ứng dụng của bạn cập nhật động từng timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như ngày Năm Mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng thời gian 6 giờ bắt đầu khi người dùng mở flow. </SDKv4> <SDKv3> Sau khi [bạn đã thiết kế phần giao diện cho paywall của mình](adapty-paywall-builder) bằng Paywall Builder mới trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động. Bước đầu tiên trong quá trình này là lấy paywall được liên kết với placement và cấu hình view của nó như mô tả bên dưới. :::warning Paywall Builder mới hoạt động với Flutter SDK phiên bản 3.3.0 trở lên. ::: Trước khi bắt đầu hiển thị paywall trong ứng dụng của bạn, hãy lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall thủ công, hãy tham khảo chủ đề [Lấy paywall và sản phẩm cho paywall Remote Config trong ứng dụng mobile](fetch-paywalls-and-products-flutter). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu hiển thị paywall trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và tích hợp các sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và tích hợp paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-flutter) vào ứng dụng di động của bạn. </details> ## Lấy paywall được thiết kế bằng Paywall Builder \{#fetch-paywall-designed-with-paywall-builder\} Nếu bạn đã [thiết kế paywall bằng Paywall Builder](adapty-paywall-builder), bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó qua placement, cấu hình view, rồi mới hiển thị trong ứng dụng. Để đảm bảo hiệu suất tối ưu, điều quan trọng là phải lấy paywall và [cấu hình view](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, cho phép đủ thời gian để tải hình ảnh trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `getPaywall`: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này được kỳ vọng là mã ngôn ngữ bao gồm một hoặc hai thẻ phụ được phân tách bằng ký tự dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ phụ thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha Brazil.</p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ có chập chờn đến đâu. Cache được cập nhật thường xuyên, vì vậy việc sử dụng trong phiên làm việc để tránh các yêu cầu mạng là hoàn toàn an toàn.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet yếu.</p> | | **loadTimeout** | mặc định: 5 giây | <p>Giá trị này giới hạn thời gian chờ của phương thức này. Nếu hết thời gian chờ, dữ liệu cache hoặc fallback cục bộ sẽ được trả về.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p><p>Đối với Android: Bạn có thể tạo `TimeInterval` bằng các hàm mở rộng (như `5.seconds`, trong đó `.seconds` là từ `import com.adapty.utils.seconds`), hoặc `TimeInterval.seconds(5)`. Để không giới hạn, sử dụng `TimeInterval.INFINITE`.</p> | ## Tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) chứa danh sách ID sản phẩm, mã định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình hiển thị của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bật nút **Show on device** trong Paywall Builder. Nếu tùy chọn này chưa được bật, cấu hình hiển thị sẽ không thể truy xuất được. ::: Sau khi tải paywall về, hãy kiểm tra xem nó có chứa `ViewConfiguration` hay không — đây là dấu hiệu cho biết paywall được tạo bằng Paywall Builder. Điều này sẽ giúp bạn xác định cách hiển thị paywall. Nếu `ViewConfiguration` có mặt, hãy xử lý nó như paywall Paywall Builder; nếu không, [xử lý nó như paywall Remote Config](present-remote-config-paywalls-flutter). ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Sau khi có view, hãy [hiển thị paywall](flutter-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được tải về gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng của bạn có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ lấy paywall của placement được chỉ định dành cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị vẫn là lấy paywall bằng phương thức `getPaywall`, như đã trình bày trong phần [Lấy thông tin paywall](flutter-get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Tại sao chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng có phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không hiển thị được. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa nhắm mục tiêu (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` như đã mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder). ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Flutter SDK phiên bản 3.2.0 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự gạch nối (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha của Brazil.</p><p></p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>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.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet của họ kém đến đâu. Cache được cập nhật thường xuyên, vì vậy có thể dùng an toàn trong suốt phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets. Hero image và video có ID được định nghĩa sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh xem trước trước khi phát video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty Flutter SDK lên phiên bản 3.8.0 trở lên. ::: Dưới đây là ví dụ về cách bạn có thể cung cấp custom assets thông qua một dictionary đơn giản: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customAssets: customAssets, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note Nếu không tìm thấy asset, paywall sẽ trở về giao diện mặc định. ::: ## Thiết lập timer do nhà phát triển định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng custom timer trong ứng dụng di động, hãy truyền một map `customTimers` vào phương thức `createPaywallView`. Mỗi key trong map là một timer ID, và giá trị tương ứng là một đối tượng `DateTime` xác định thời điểm timer kết thúc. Dưới đây là ví dụ: ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2025, 1, 1), // New Year 2025 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là các **Timer ID** của timer do developer định nghĩa, được cài đặt trong Adapty Dashboard. Map `customTimers` đảm bảo ứng dụng của bạn cập nhật động từng timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như Ngày Năm Mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng thời gian 6 giờ bắt đầu khi người dùng mở paywall. </SDKv3> --- # File: flutter-present-paywalls --- --- title: "Hiển thị flows & paywalls - Flutter" description: "Hiển thị flows và paywalls trong ứng dụng Flutter sử dụng các tính năng monetization của Adapty." --- <SDKv4> Nếu bạn đã thiết kế một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow hoặc paywall như vậy đã chứa cả nội dung cần hiển thị lẫn cách hiển thị nó. :::warning Hướng dẫn này dành cho flow và paywall được tạo bằng Paywall Builder. Để hiển thị **paywall Remote Config**, xem [Render paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-flutter). ::: Adapty Flutter SDK cung cấp hai cách để hiển thị flow và paywall: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị một flow hoặc paywall dưới dạng màn hình độc lập, hãy dùng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createFlowView`](flutter-get-pb-paywalls#fetch-the-view-configuration). Mỗi `view` chỉ có thể được hiển thị một lần: sau khi bạn đóng nó, view sẽ được giải phóng khỏi bộ nhớ. Nếu cần hiển thị lại flow hoặc paywall, hãy gọi lại `createFlowView` một lần nữa để tạo một `view` mới. ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Đóng flow hoặc paywall \{#dismiss-the-flow-or-paywall\} Khi cần đóng flow hoặc paywall theo cách lập trình, hãy dùng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note Đóng một view sẽ giải phóng nó khỏi bộ nhớ — view đã đóng không thể hiển thị lại. Hãy tạo view mới bằng `createFlowView` thay thế. ::: ### Hiển thị hộp thoại \{#show-dialog\} Dùng phương thức này thay cho các hộp thoại alert thông thường khi hiển thị flow hoặc paywall view trên Android. Trên Android, các alert thông thường sẽ xuất hiện phía sau view, khiến người dùng không nhìn thấy. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng phía trên flow hoặc paywall trên mọi nền tảng. ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách flow hoặc paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng một flow hoặc paywall vào cây widget hiện có của bạn, hãy sử dụng trực tiếp widget `AdaptyUIFlowPlatformView` trong cây widget Flutter của bạn. ```dart showLineNumbers title="Flutter" AdaptyUIFlowPlatformView( flow: flow, // The flow object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidReceiveError: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note Để Android platform view hoạt động, hãy đảm bảo `MainActivity` của bạn kế thừa `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này chỉ áp dụng cho **paywall Paywall Builder mới** yêu cầu SDK v3.2.0 trở lên. Quy trình hiển thị paywall khác nhau tùy theo phiên bản Paywall Builder được sử dụng để thiết kế paywall và paywall Remote Config. - Để hiển thị **paywall Remote Config**, xem [Render paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-flutter). ::: Adapty Flutter SDK cung cấp hai cách hiển thị paywall: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị một paywall dưới dạng màn hình độc lập, hãy sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createPaywallView`](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo instance `view` mới. :::warning Việc tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Đóng paywall \{#dismiss-the-paywall\} Khi cần đóng paywall theo chương trình, sử dụng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Hiển thị hộp thoại \{#show-dialog\} Dùng phương thức này thay cho hộp thoại cảnh báo gốc khi paywall view đang được hiển thị trên Android. Trên Android, các alert thông thường sẽ xuất hiện phía sau paywall view, khiến người dùng không thể nhìn thấy. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng vị trí phía trên paywall trên mọi nền tảng. ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này nhận giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng paywall vào cây widget hiện có của bạn, hãy sử dụng widget `AdaptyUIPaywallPlatformView` trực tiếp trong cây widget Flutter của bạn. ```dart showLineNumbers title="Flutter" AdaptyUIPaywallPlatformView( paywall: paywall, // The paywall object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidFailRendering: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note Để Android platform view hoạt động, hãy đảm bảo `MainActivity` của bạn extends `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: </SDKv3> --- # File: flutter-handle-paywall-actions --- --- title: "Xử lý hành động nút trong Flutter SDK" description: "Xử lý các hành động nút paywall trong Flutter bằng Adapty để tối ưu hóa doanh thu ứng dụng." --- <SDKv4> Nếu bạn đang xây dựng flow hoặc paywall bằng Adapty builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm [nút trong builder](paywall-buttons) và gán cho nút đó một action có sẵn hoặc tạo custom action ID. 2. Viết code trong ứng dụng để xử lý từng action bạn đã gán. Hướng dẫn này trình bày cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Đóng màn hình và mở URL được xử lý tự động** bởi cài đặt mặc định của `flowViewDidPerformAction`, và SDK tự xử lý các giao dịch mua và khôi phục. Tất cả các hành động nút khác, chẳng hạn như đăng nhập hoặc mở một flow khác, cần được xử lý trong mã ứng dụng. Lưu ý rằng việc phản hồi các giao dịch mua và khôi phục *đã hoàn thành* diễn ra trong các callback observer bắt buộc — xem [Xử lý sự kiện flow & paywall](flutter-handling-events). ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall, trong builder, hãy thêm một nút và gán cho nó hành động **Close**. Không cần viết code: implementation mặc định của `flowViewDidPerformAction` sẽ tự động đóng view khi nhận được `CloseAction`. :::info Nút **Back** của hệ thống Android không còn tự động đóng view theo mặc định nữa. Nó được chuyển đến `flowViewDidPerformAction` dưới dạng `AndroidSystemBackAction` — bạn cần tự xử lý nếu muốn nút back đóng flow hoặc paywall. ::: Ghi đè `flowViewDidPerformAction` nếu bạn cần hành vi tùy chỉnh — ví dụ: đóng view khi người dùng nhấn nút back của hệ thống Android, như trong v3: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } ``` :::warning Việc ghi đè `flowViewDidPerformAction` sẽ thay thế hoàn toàn phần cài đặt mặc định — hãy giữ lại các case `CloseAction` và `OpenUrlAction` nếu bạn muốn giữ nguyên hành vi đóng và mở URL mặc định. ::: ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch mua), hãy thêm phần tử **Link** trong builder và xử lý giống như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết (ví dụ: **Terms of use** hoặc **Privacy policy**), trong builder, hãy thêm một nút, gán cho nó hành động **Open URL**, rồi nhập URL bạn muốn mở. Không cần viết code: phần triển khai mặc định của `flowViewDidPerformAction` sẽ mở URL theo cách native thông qua `AdaptyUI().openUrl`, tuân theo cài đặt mở trong trình duyệt trong ứng dụng hoặc trình duyệt ngoài từ dashboard. Hành vi mặc định là đủ cho hầu hết các trường hợp. Nếu bạn vẫn muốn tự xử lý việc mở URL, hãy ghi đè handler: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL in whatever way fits your app break; default: break; } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng: 1. Trong builder, thêm một nút và gán cho nó action **Login**. 2. Trong code ứng dụng, triển khai handler cho action `login` để xác định người dùng của bạn. ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Navigate to your login screen in whatever way fits your app break; default: break; } } ``` ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm nút xử lý các hành động khác: 1. Trong builder, thêm một nút, gán cho nó hành động **Custom** và đặt cho nó một ID. 2. Trong code của ứng dụng, triển khai một handler cho ID hành động bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút để hiển thị một flow hoặc paywall khác: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another flow or paywall break; default: break; } } ``` </SDKv4> <SDKv3> Nếu bạn đang xây dựng paywall bằng Adapty paywall builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm một [nút trong paywall builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một ID hành động tùy chỉnh. 2. Viết code trong ứng dụng của bạn để xử lý từng hành động bạn đã gán. Hướng dẫn này chỉ cách xử lý các hành động tùy chỉnh và có sẵn trong code của bạn. :::warning **Chỉ có giao dịch mua và khôi phục là được xử lý tự động.** Tất cả các hành động nút khác, chẳng hạn như đóng paywall hoặc mở liên kết, đều cần bạn tự triển khai xử lý trong code của ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code ứng dụng, triển khai handler cho các hành động `CloseAction` và `AndroidSystemBackAction`. :::info Trong Flutter SDK, các hành động `CloseAction` và `AndroidSystemBackAction` mặc định sẽ kích hoạt việc đóng paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một paywall có thể kích hoạt mở một paywall khác. ::: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; default: break; } } ``` ## Mở URL từ paywall \{#open-urls-from-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch), hãy thêm phần tử **Link** trong paywall builder và xử lý nó theo cách tương tự như các nút với hành động **Open URL**. ::: Để thêm một nút mở liên kết từ paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong paywall builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng của bạn, triển khai một handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. ```dart // You have to install url_launcher plugin in order to handle urls: // https://pub.dev/packages/url_launcher void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case OpenUrlAction(url: final url): final Uri uri = Uri.parse(url); launchUrl(uri, mode: LaunchMode.inAppBrowserView); break; default: break; } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng: 1. Trong paywall builder, thêm một nút và gán cho nó hành động **Login**. 2. Trong code ứng dụng, triển khai một handler cho hành động `login` để xác định người dùng. ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Navigate to your login screen in whatever way fits your app break; default: break; } } ``` ## Xử lý custom action \{#handle-custom-actions\} Để thêm nút xử lý bất kỳ hành động nào khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và đặt cho nó một ID. 2. Trong code của ứng dụng, hãy triển khai handler cho action ID mà bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đãi đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút để hiển thị paywall khác: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another paywall break; default: break; } } ``` </SDKv3> --- # File: flutter-handling-events --- --- title: "Flutter - Xử lý sự kiện flow & paywall" description: "Khám phá cách xử lý các sự kiện liên quan đến gói đăng ký trong Flutter bằng Adapty để theo dõi tương tác người dùng hiệu quả." --- <SDKv4> :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, lựa chọn sản phẩm và hiển thị. Việc đóng view và mở liên kết được xử lý bởi implementation mặc định của `flowViewDidPerformAction` — xem [hướng dẫn xử lý hành động button](flutter-handle-paywall-actions) để ghi đè chúng hoặc xử lý các hành động button tùy chỉnh. ::: Flows và paywalls được cấu hình bằng builder không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Những sự kiện này bao gồm các lần nhấn nút (nút đóng, URL, lựa chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua diễn ra trên flow hoặc paywall. Tìm hiểu cách phản hồi các sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình flow hoặc paywall trong ứng dụng di động của bạn, hãy triển khai các phương thức `AdaptyUIFlowsEventsObserver` và thiết lập observer trước khi hiển thị bất kỳ màn hình nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setFlowsEventsObserver(this); ``` Ba phương thức observer là **bắt buộc** — class của bạn sẽ không biên dịch được nếu thiếu chúng: `flowViewDidFinishPurchase`, `flowViewDidFinishRestore`, và `flowViewDidReceiveError`. Tất cả các phương thức còn lại là tùy chọn. Để hủy kết nối một observer đã được thiết lập trước đó, truyền `null` vào `setFlowsEventsObserver`. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: Các ví dụ sự kiện dưới đây hiển thị các thuộc tính có sẵn trên từng đối tượng, với các giá trị minh họa trong phần chú thích. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### View appeared \{#view-appeared\} Phương thức này được gọi khi flow hoặc màn hình paywall xuất hiện trên màn hình. :::note Trên iOS, phương thức này cũng được gọi khi người dùng nhấn vào [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall, và web paywall mở ra trong trình duyệt trong ứng dụng. ::: ```dart showLineNumbers title="Flutter" void flowViewDidAppear(AdaptyUIFlowView view) { } ``` #### View disappeared \{#view-disappeared\} Phương thức này được gọi khi flow hoặc màn hình paywall bị đóng khỏi màn hình. :::note Trên iOS, cũng được gọi khi một [web paywall](web-paywall#step-2a-add-a-web-purchase-button) mở từ paywall trong trình duyệt trong ứng dụng biến mất khỏi màn hình. ::: ```dart showLineNumbers title="Flutter" void flowViewDidDisappear(AdaptyUIFlowView view) { } ``` #### Chọn sản phẩm \{#product-selection\} Nếu một sản phẩm được chọn để mua (bởi người dùng hoặc hệ thống), phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) { // productId is a String: productId; // 'premium_monthly' } ``` </Details> #### Đã bắt đầu mua hàng \{#started-purchase\} Nếu người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' product.localizedTitle; // 'Premium Monthly' product.localizedDescription; // 'Premium subscription for 1 month' product.price.amount; // 9.99 (double) product.price.currencyCode; // 'USD' product.price.localizedString; // '$9.99' } ``` </Details> #### Hoàn tất giao dịch mua \{#finished-purchase\} Phương thức này là **bắt buộc**. Nó được gọi khi một giao dịch mua thành công, người dùng hủy giao dịch, hoặc giao dịch có vẻ đang chờ xử lý: ```dart showLineNumbers title="Flutter" void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) break; case AdaptyPurchaseResultPending(): // no additional data break; case AdaptyPurchaseResultUserCancelled(): // no additional data break; } } ``` </Details> :::info Khác với v3, phương thức này không có hành vi mặc định — view sẽ không tự động bị đóng sau khi mua thành công. Bạn tự quyết định bước tiếp theo: tiếp tục flow hoặc gọi `view.dismiss()`. Tham khảo [Xử lý hành động nút](flutter-handle-paywall-actions) để biết thêm chi tiết về cách đóng màn hình. ::: #### Hoàn tất điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi có một lần thử mở [web paywall](web-paywall) cho một sản phẩm cụ thể. Điều này bao gồm cả các lần điều hướng thành công lẫn thất bại: ```dart showLineNumbers title="Flutter" void flowViewDidFinishWebPaymentNavigation(AdaptyUIFlowView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **Tham số:** | Tham số | Mô tả | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | Một `AdaptyPaywallProduct` mà web paywall đã được mở. Có thể là `null`. | | **error** | Một đối tượng `AdaptyError` nếu điều hướng web paywall thất bại; `null` nếu điều hướng thành công. | #### Giao dịch thất bại \{#failed-purchase\} Phương thức này được gọi khi một giao dịch mua thất bại (ví dụ: do sự cố thanh toán hoặc lỗi mạng). Nó **không** kích hoạt khi người dùng tự hủy hoặc khi giao dịch đang chờ xử lý—những trường hợp đó được xử lý bởi `flowViewDidFinishPurchase`: ```dart showLineNumbers title="Flutter" void flowViewDidFailPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyError error) { } ``` #### Bắt đầu khôi phục \{#started-restore\} Nếu người dùng bắt đầu quá trình khôi phục, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidStartRestore(AdaptyUIFlowView view) { } ``` #### Khôi phục thành công \{#successful-restore\} Phương thức này là **bắt buộc**. Nếu việc khôi phục mua hàng thành công, nó sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) profile.subscriptions['premium_monthly']?.isActive; // true profile.subscriptions['premium_monthly']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) } ``` </Details> Chúng tôi khuyến nghị đóng màn hình nếu người dùng đã có `accessLevel` yêu cầu. Tham khảo chủ đề [Trạng thái gói đăng ký](flutter-listen-subscription-changes) để tìm hiểu cách kiểm tra và chủ đề [Phản hồi các hành động nút](flutter-handle-paywall-actions) để tìm hiểu cách đóng màn hình. #### Khôi phục thất bại \{#failed-restore\} Nếu việc khôi phục giao dịch mua thất bại, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidFailRestore(AdaptyUIFlowView view, AdaptyError error) { } ``` ### Tải dữ liệu và hiển thị \{#data-fetching-and-rendering\} #### Lỗi tải sản phẩm \{#product-loading-errors\} Nếu bạn không truyền mảng sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ máy chủ. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức sau: ```dart showLineNumbers title="Flutter" void flowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError error) { } ``` #### Lỗi view \{#view-errors\} Phương thức này là **bắt buộc**. Nó thay thế phương thức `paywallViewDidFailRendering` của v3: các lỗi xảy ra trong quá trình render giao diện, cũng như các lỗi view khác, sẽ được báo cáo thông qua lần gọi này. Sau khi bạn implement nó, việc đóng view là tùy bạn quyết định — chúng tôi khuyến nghị đóng view khi gặp các lỗi như vậy, đây cũng chính là hành vi mặc định tích hợp sẵn của SDK khi không có observer nào được thiết lập: ```dart showLineNumbers title="Flutter" void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { // log the error and dismiss the broken view view.dismiss(); } ``` Trong trường hợp bình thường, lỗi hiển thị không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết. ### Sự kiện phân tích \{#analytics-events\} Phương thức tùy chọn `flowViewDidReceiveAnalyticEvent` được dành riêng cho các sự kiện phân tích tùy chỉnh từ một flow. Hiện tại, các flow chưa phát các sự kiện này đến code của bạn, vì vậy bạn chưa cần triển khai nó. ### Xử lý giao dịch mua hàng trong Observer mode \{#handle-purchases-in-observer-mode\} Nếu bạn đã kích hoạt SDK ở chế độ [Observer mode](implement-observer-mode-flutter) và hiển thị một flow hoặc paywall do Adapty render, SDK sẽ không thực hiện giao dịch mua hàng thay bạn. Khi người dùng nhấn nút mua hàng hoặc khôi phục, SDK sẽ gọi `AdaptyUIObserverModeResolver` của bạn. Xem [Hiển thị flow trong Observer mode](flutter-present-flows-in-observer-mode) để biết cách thiết lập đầy đủ. ### Xử lý các yêu cầu hệ thống \{#handle-system-requests\} `AdaptyUISystemRequestsHandler` (đăng ký qua `AdaptyUI().setSystemRequestsHandler(...)`) được dành riêng cho các yêu cầu hệ thống từ một flow: các lời nhắc cấp quyền của hệ điều hành (như thông báo đẩy hoặc quyền truy cập camera) và yêu cầu đánh giá trên App Store. Hiện tại các flow chưa kích hoạt những yêu cầu này, nên bạn chưa cần đăng ký handler. Nếu bạn có đăng ký một cái, hãy lưu ý rằng `handlePermission` là phương thức bắt buộc của class — hãy yêu cầu quyền bằng code của riêng bạn, sau đó trả về `AdaptyUIPermissionResult.granted()` hoặc `AdaptyUIPermissionResult.denied()`; còn `handleAppReviewRequest` là tùy chọn. </SDKv4> <SDKv3> :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút](flutter-handle-paywall-actions) để biết thêm chi tiết. ::: Các paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm các lần nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện trên paywall. Hãy xem cách phản hồi các sự kiện này bên dưới. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** yêu cầu Adapty SDK v3.0 trở lên. ::: Để kiểm soát hoặc theo dõi các sự kiện xảy ra trên màn hình paywall trong ứng dụng di động của bạn, hãy implement các phương thức `AdaptyUIPaywallsEventsObserver` và đặt observer trước khi hiển thị bất kỳ màn hình nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setPaywallsEventsObserver(this); ``` :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: Các ví dụ về sự kiện bên dưới hiển thị các thuộc tính có sẵn trên mỗi đối tượng, cùng với các giá trị minh họa trong phần chú thích. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### Paywall xuất hiện \{#paywall-appeared\} Phương thức này được gọi khi màn hình paywall hiển thị trên màn hình. :::note Trên iOS, cũng được gọi khi người dùng nhấn [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall, và web paywall mở ra trong trình duyệt trong ứng dụng. ::: ```dart showLineNumbers title="Flutter" void paywallViewDidAppear(AdaptyUIPaywallView view) { } ``` #### Paywall biến mất \{#paywall-disappeared\} Phương thức này được gọi khi màn hình paywall bị đóng khỏi màn hình. :::note Trên iOS, cũng được gọi khi một [web paywall](web-paywall#step-2a-add-a-web-purchase-button) mở từ một paywall trong trình duyệt trong ứng dụng biến mất khỏi màn hình. ::: ```dart showLineNumbers title="Flutter" void paywallViewDidDisappear(AdaptyUIPaywallView view) { } ``` #### Chọn sản phẩm \{#product-selection\} Nếu một sản phẩm được chọn để mua (bởi người dùng hoặc bởi hệ thống), phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { // productId is a String: productId; // 'premium_monthly' } ``` </Details> #### Bắt đầu mua hàng \{#started-purchase\} Khi người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' product.localizedTitle; // 'Premium Monthly' product.localizedDescription; // 'Premium subscription for 1 month' product.price.amount; // 9.99 (double) product.price.currencyCode; // 'USD' product.price.localizedString; // '$9.99' } ``` </Details> #### Hoàn tất thanh toán \{#finished-purchase\} Phương thức này được gọi khi giao dịch mua thành công, người dùng hủy giao dịch, hoặc giao dịch đang ở trạng thái chờ xử lý: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) break; case AdaptyPurchaseResultPending(): // no additional data break; case AdaptyPurchaseResultUserCancelled(): // no additional data break; } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình trong trường hợp đó. Tham khảo [Phản hồi các hành động nút](flutter-handle-paywall-actions) để biết chi tiết về cách đóng màn hình paywall. #### Hoàn tất điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi có lần thử mở [web paywall](web-paywall) cho một sản phẩm cụ thể. Điều này bao gồm cả các lần điều hướng thành công và thất bại: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **Tham số:** | Tham số | Mô tả | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | Một `AdaptyPaywallProduct` mà web paywall được mở cho. Có thể là `null`. | | **error** | Một đối tượng `AdaptyError` nếu điều hướng web paywall thất bại; `null` nếu điều hướng thành công. | <Details> <summary>Ví dụ về sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { // product — AdaptyPaywallProduct?: product?.vendorProductId; // 'premium_monthly' if (error == null) { // navigation succeeded } else { // error — AdaptyError: error.code; // AdaptyErrorCode.networkFailed (2005) error.message; // 'Network request failed' error.detail; // platform-specific underlying error, or null } } ``` </Details> #### Mua hàng thất bại \{#failed-purchase\} Phương thức này được gọi khi một giao dịch mua thất bại (ví dụ: do lỗi thanh toán hoặc lỗi mạng). Nó **không** kích hoạt khi người dùng chủ động huỷ hoặc giao dịch đang chờ xử lý — những trường hợp đó được xử lý bởi `paywallViewDidFinishPurchase`: ```dart showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' // error — AdaptyError: error.code; // AdaptyErrorCode.productPurchaseFailed (1006) error.message; // 'Product purchase failed.' error.detail; // platform-specific underlying error, or null } ``` </Details> #### Bắt đầu khôi phục \{#started-restore\} Nếu người dùng khởi động quá trình khôi phục, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidStartRestore(AdaptyUIPaywallView view) { } ``` #### Khôi phục thành công \{#successful-restore\} Nếu việc khôi phục giao dịch thành công, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) profile.subscriptions['premium_monthly']?.isActive; // true profile.subscriptions['premium_monthly']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) } ``` </Details> Chúng tôi khuyến nghị đóng màn hình nếu người dùng đã có `accessLevel` yêu cầu. Tham khảo chủ đề [Trạng thái gói đăng ký](flutter-listen-subscription-changes) để biết cách kiểm tra và chủ đề [Xử lý hành động nút](flutter-handle-paywall-actions) để biết cách đóng màn hình paywall. #### Khôi phục thất bại \{#failed-restore\} Nếu việc khôi phục giao dịch thất bại, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```dart void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.receiveRestoredTransactionsFailed (1011) error.message; // 'Error occurred in the process of restoring purchases.' error.detail; // platform-specific underlying error, or null } ``` </Details> ### Tải dữ liệu và hiển thị \{#data-fetching-and-rendering\} #### Lỗi tải sản phẩm \{#product-loading-errors\} Nếu bạn không truyền mảng sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ server. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức sau: ```dart showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.productRequestFailed (1002) error.message; // 'Unable to fetch available In-App Purchase products at the moment.' error.detail; // platform-specific underlying error, or null } ``` </Details> #### Lỗi khi hiển thị \{#rendering-errors\} Nếu xảy ra lỗi trong quá trình render giao diện, lỗi đó sẽ được báo cáo thông qua phương thức này. Theo mặc định (từ v3.15.2), paywall sẽ tự động bị đóng khi gặp lỗi render, nhưng bạn có thể ghi đè hành vi này nếu cần. ```dart showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // Default behavior: view.dismiss() // Override with custom logic if needed, for example: // - Log the error // - Show an error message to the user } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```dart void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.jsException (4105) error.message; // 'An exception was thrown from JS during AdaptyUI flow execution.' error.detail; // platform-specific underlying error, or null // Default behavior: view.dismiss() } ``` </Details> Trong trường hợp bình thường, các lỗi này không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết. </SDKv3> --- # File: flutter-use-fallback-paywalls --- --- title: "Flutter - Sử dụng paywall dự phòng" description: "Xử lý các trường hợp người dùng ngoại tuyến hoặc máy chủ Adapty không khả dụng" --- :::warning Paywall dự phòng được hỗ trợ từ Flutter SDK v2.11 trở lên. ::: Để duy trì trải nghiệm người dùng mượt mà, điều quan trọng là phải thiết lập [paywall dự phòng](/fallback-paywalls) cho các flow, [paywall](paywalls) và [onboarding](onboardings) của bạn. Biện pháp phòng ngừa này giúp mở rộng khả năng của ứng dụng trong trường hợp mất kết nối internet một phần hoặc hoàn toàn. * **Nếu ứng dụng không thể kết nối đến máy chủ Adapty:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng, và truy cập cấu hình onboarding đã lưu cục bộ. * **Nếu ứng dụng không thể kết nối internet:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng. Onboarding chứa nội dung từ xa và cần có kết nối internet để hoạt động. :::important Trước khi thực hiện các bước trong hướng dẫn này, hãy [tải xuống](/local-fallback-paywalls) các file cấu hình dự phòng từ Adapty. ::: ## Cấu hình \{#configuration\} 1. Thêm các file cấu hình dự phòng vào thư mục `assets` của ứng dụng tại thư mục gốc của dự án. 2. Gọi phương thức `.setFallback` **trước khi** bạn tải paywall hoặc onboarding đích. ```dart showLineNumbers title="Flutter" final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { await Adapty().setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số: | Tham số | Mô tả | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **assetId** | Đường dẫn đến file cấu hình dự phòng. | :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: --- # File: flutter-localizations-and-locale-codes --- --- title: "Sử dụng localizations và locale codes trong Flutter SDK" description: "Quản lý localizations và locale codes của ứng dụng để tiếp cận người dùng toàn cầu." --- <SDKv4> ## Tại sao điều này quan trọng \{#why-this-is-important\} Mã ngôn ngữ (locale code) được sử dụng khi Adapty chọn bản dịch cho một flow, và khi bạn đọc Remote Config cho một paywall tùy chỉnh. Mã ngôn ngữ khá phức tạp và có thể khác nhau giữa các nền tảng, vì vậy Adapty sử dụng một tiêu chuẩn nội bộ thống nhất trên mọi nền tảng được hỗ trợ. Hiểu được tiêu chuẩn đó sẽ giúp bạn dự đoán bản dịch nào người dùng sẽ nhận được. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng một phiên bản điều chỉnh nhỏ của [tiêu chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi mã bao gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung Giản thể), `zh-hant` (tiếng Trung Phồn thể). ## Khớp mã ngôn ngữ \{#locale-code-matching\} Khi Adapty tìm kiếm bản địa hóa phù hợp với ngôn ngữ của người dùng, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Adapty tìm kiếm bản địa hóa có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy, Adapty lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tìm kiếm bản địa hóa phù hợp 4. Nếu vẫn không tìm thấy, Adapty trả về bản địa hóa mặc định `en` Nhờ cách này, `'pt_BR'`, `pt-BR` và `pt-br` đều được phân giải về cùng một bản địa hóa. ## Triển khai localization \{#implementing-localizations\} Trong SDK v4, bạn không cần truyền mã locale khi lấy một flow. - **Paywall dùng Flow Builder và Paywall Builder**: Adapty tự động xác định localization dựa trên thiết bị và các localization bạn đã cấu hình trong builder. Render flow bằng `createFlowView` — không cần mã locale. - **Paywall tùy chỉnh (remote config)**: `getFlow` trả về tất cả các localization đã cấu hình trong `flow.remoteConfigs`. Mỗi mục có mã `locale` và nội dung config (chuỗi `data`, hoặc `dictionary` đã được parse). Hãy chọn mục phù hợp với người dùng, kèm theo logic fallback của riêng bạn: ```dart showLineNumbers final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final config = flow.remoteConfigs.firstWhereOrNull((c) => c.locale == 'en') ?? flow.remoteConfig; // the first remote config, if present // read your values from config?.dictionary ``` Các quy tắc khớp mã locale ở trên mô tả cách Adapty chuẩn hóa các mã `locale` được lưu trữ trên mỗi Remote Config. </SDKv4> <SDKv3> ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một vài tình huống mà mã ngôn ngữ (locale code) trở nên quan trọng — ví dụ, khi bạn cần lấy đúng paywall phù hợp với ngôn ngữ hiện tại của ứng dụng. Vì mã ngôn ngữ khá phức tạp và có thể khác nhau tùy từng nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ thống nhất cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng bản ngôn ngữ mong muốn — và điều gì xảy ra tiếp theo — để luôn nhận được kết quả như kỳ vọng. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng chuẩn [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) được chỉnh sửa nhẹ: mỗi mã bao gồm các subtag viết thường, ngăn cách nhau bằng dấu gạch ngang. Một số ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Đối sánh mã ngôn ngữ \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client kèm theo mã ngôn ngữ và bắt đầu tìm kiếm bản bản địa hóa tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Chúng tôi tìm kiếm bản bản địa hóa có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy kết quả khớp, chúng tôi lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` trong `pt-br`) và tìm kiếm bản bản địa hóa tương ứng 4. Nếu vẫn không tìm thấy kết quả khớp, chúng tôi trả về bản bản địa hóa mặc định `en` Bằng cách này, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` đều sẽ nhận được cùng một kết quả. ## Triển khai bản địa hóa: cách được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang tìm hiểu về bản địa hóa, có thể bạn đã làm việc với các file chuỗi đã được bản địa hóa trong dự án của mình. Trong trường hợp đó, chúng tôi khuyến nghị bạn thêm một cặp key-value với mã locale Adapty tương ứng vào mỗi file cho từng bản địa hóa. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như ví dụ dưới đây: ```dart showLineNumbers // 1. Modify your app_en.arb, app_es.arb, app_pt_br.arb files /* app_en.arb */ "adapty_paywalls_locale": "en", /* app_es.arb */ "adapty_paywalls_locale": "es", /* app_pt_br.arb */ "adapty_paywalls_locale": "pt-br", // 2. Extract and use the locale code final locale = AppLocalizations.of(context)!.adapty_paywalls_locale; // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Bằng cách này, bạn có thể đảm bảo kiểm soát hoàn toàn ngôn ngữ nào sẽ được tải về cho từng người dùng trong ứng dụng của bạn. ## Triển khai bản địa hóa theo cách khác \{#implementing-localizations-the-other-way\} Bạn có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống) mà không cần định nghĩa rõ ràng mã ngôn ngữ cho từng bản địa hóa. Điều đó có nghĩa là trích xuất mã ngôn ngữ từ các đối tượng khác mà nền tảng của bạn cung cấp, như sau: ```dart showLineNumbers final locale = Localizations.localeOf(context).languageCode; // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Lưu ý rằng chúng tôi không khuyến nghị cách tiếp cận này vì một số lý do: 1. Trên iOS, ngôn ngữ ưu tiên và locale hiện tại không giống nhau. Nếu muốn bản địa hóa được chọn đúng, bạn cần dựa vào logic của Apple — hoạt động tự động nếu bạn dùng cách tiếp cận được khuyến nghị với các file string đã bản địa hóa — hoặc tự tái tạo lại logic đó. 2. Rất khó dự đoán chính xác những gì server của Adapty sẽ nhận được. Ví dụ: trên iOS, có thể lấy được một locale như `ar_OM@numbers='latn'` từ thiết bị và gửi lên server. Khi đó, thay vì nhận được bản địa hóa `ar-om` như mong muốn, bạn sẽ nhận được `ar` — điều này có thể nằm ngoài kỳ vọng. Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. </SDKv3> --- # File: flutter-web-paywall --- --- title: "Triển khai web paywall trong Flutter SDK" description: "Thiết lập web paywall để nhận thanh toán mà không phải chịu phí và kiểm duyệt của App Store." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trong dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywall bằng phương thức SDK. Phương thức `.openWebPaywall`: 1. Tạo một URL duy nhất cho phép Adapty liên kết paywall cụ thể đang hiển thị cho người dùng với trang web họ được chuyển hướng đến. 2. Theo dõi khi người dùng quay lại ứng dụng, sau đó gọi `.getProfile` theo chu kỳ ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không. Như vậy, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ kích hoạt trong ứng dụng gần như ngay lập tức. ```dart showLineNumbers title="Flutter" try { await Adapty().openWebPaywall(product: <YOUR_PRODUCT>); // The web paywall will be opened } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall(product)` tạo URL theo paywall và thêm dữ liệu sản phẩm vào URL. 2. `openWebPaywall(paywall)` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng khi các sản phẩm trong Adapty paywall khác với các sản phẩm trong web paywall. Trong SDK v4, tham số `paywall` nhận vào một `AdaptyFlowPaywall` — một biến thể paywall của flow đã được tải về. Hãy kiểm tra `flow.paywalls` không rỗng trước khi truy cập theo chỉ số, ví dụ `flow.paywalls[0]`. ::: #### Xử lý lỗi \{#handle-errors\} | Lỗi | Mô tả | Hành động khuyến nghị | |-----------------------------------------|-----------------------------------------------------------------------|----------------------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | Paywall chưa được cấu hình URL mua hàng qua web | Kiểm tra xem paywall đã được cấu hình đúng cách trong Adapty Dashboard chưa | | AdaptyError.productWithoutPurchaseUrl | Sản phẩm chưa có URL mua hàng qua web | Xác minh cấu hình sản phẩm trong Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Không thể mở URL trong trình duyệt | Kiểm tra cài đặt thiết bị hoặc cung cấp phương thức mua hàng thay thế | | AdaptyError.failedDecodingWebPaywallUrl | Không thể mã hóa các tham số trong URL đúng cách | Xác minh các tham số URL hợp lệ và được định dạng đúng | ## Mở web paywall trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} :::important Mở web paywall trong trình duyệt trong ứng dụng được hỗ trợ từ Adapty SDK v3.15 trở lên. ::: Theo mặc định, web paywall sẽ mở trong trình duyệt ngoài. Để mang lại trải nghiệm liền mạch cho người dùng, bạn có thể mở web paywall trong trình duyệt trong ứng dụng. Cách này hiển thị trang mua hàng ngay bên trong ứng dụng, giúp người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, hãy đặt tham số `in` thành `.inAppBrowser`: ```dart showLineNumbers try { await Adapty().openWebPaywall( product: <YOUR_PRODUCT>, openIn: AdaptyWebPresentation.inAppBrowser, ); // The web paywall will be opened in the in-app browser } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` --- # File: flutter-troubleshoot-paywall-builder --- --- title: "Khắc phục sự cố Paywall Builder trong Flutter SDK" description: "Khắc phục sự cố Paywall Builder trong Flutter SDK" --- Hướng dẫn này giúp bạn giải quyết các sự cố thường gặp khi sử dụng paywall được thiết kế trong Adapty Paywall Builder với Flutter SDK. ## Lấy cấu hình paywall thất bại \{#getting-a-paywall-configuration-fails\} **Vấn đề**: Phương thức `createPaywallView` không lấy được cấu hình paywall. **Nguyên nhân**: Paywall chưa được bật hiển thị trên thiết bị trong Paywall Builder. **Giải pháp**: Bật toggle **Show on device** trong Paywall Builder. <img src="/assets/shared/img/show-on-device.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Số lượt xem paywall quá lớn \{#the-paywall-view-number-is-too-big\} **Vấn đề**: Số lượt xem paywall hiển thị gấp đôi con số mong đợi. **Nguyên nhân**: Bạn có thể đang gọi `logShowFlow` (Flutter SDK v4+) / `logShowPaywall` trong code, khiến số lượt xem bị nhân đôi nếu bạn đang dùng Paywall Builder hoặc Flow Builder. Với các flow và paywall được xây dựng bằng các công cụ này, analytics được theo dõi tự động, vì vậy bạn không cần dùng phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowFlow` (Flutter SDK v4+) / `logShowPaywall` trong code nếu đang sử dụng Paywall Builder hoặc Flow Builder. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp phải các sự cố liên quan đến Paywall Builder không được đề cập ở trên. **Giải pháp**: Migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](flutter-sdk-migration-guides) nếu cần. Nhiều vấn đề đã được giải quyết trong các phiên bản SDK mới hơn. --- # File: flutter-present-flows-in-observer-mode --- --- title: "Hiển thị flow trong Observer mode trong Flutter SDK" description: "Hiển thị flow và paywall được tạo bằng Paywall Builder trong Observer mode trong ứng dụng Flutter của bạn trong khi xử lý các giao dịch mua bằng code của riêng bạn." --- Nếu bạn đã tùy chỉnh flow hoặc paywall bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách hiển thị. :::warning Phần này chỉ áp dụng cho [Observer mode](observer-vs-full-mode). Nếu bạn không làm việc ở Observer mode, hãy tham khảo chủ đề [Hiển thị flows & paywalls](flutter-present-paywalls). ::: :::info Tính năng này yêu cầu Adapty Flutter SDK 4.0 trở lên — trước đây chỉ có trong các SDK gốc iOS và Android. Xem [hướng dẫn migration](migration-to-flutter-sdk-v4) để nâng cấp. ::: <details> <summary>Trước khi bắt đầu hiển thị flow (Nhấp để mở rộng)</summary> 1. Thiết lập tích hợp ban đầu của Adapty [với App Store](initial_ios) và [với Google Play](initial-android). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo [hướng dẫn cài đặt Flutter SDK](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình flow hoặc paywall trong các builder](create-paywall) và gán sản phẩm vào chúng. 5. [Tạo placement và gán flow hoặc paywall vào chúng](create-placement). 6. [Lấy flow và cấu hình của chúng](flutter-get-pb-paywalls) trong code ứng dụng di động của bạn. </details> Trong Observer mode, SDK không thực hiện giao dịch mua hàng thay cho bạn. Khi người dùng nhấn nút mua hàng hoặc khôi phục trong một flow hoặc paywall do Adapty hiển thị, SDK sẽ gọi `AdaptyUIObserverModeResolver` của bạn — hãy thực hiện việc mua hàng hoặc khôi phục bằng code của chính bạn tại đó. 1. Implement `AdaptyUIObserverModeResolver`: ```dart showLineNumbers title="Flutter" class MyObserverModeResolver extends AdaptyUIObserverModeResolver { @override void observerModeDidInitiatePurchase( AdaptyUIFlowView view, AdaptyPaywallProduct product, void Function() onStartPurchase, void Function() onFinishPurchase, ) { onStartPurchase(); // the view shows its loading indicator // make the purchase with your own code, then: onFinishPurchase(); // the view hides the loading indicator } @override void observerModeDidInitiateRestore( AdaptyUIFlowView view, void Function() onStartRestore, void Function() onFinishRestore, ) { onStartRestore(); // restore purchases with your own code, then: onFinishRestore(); } } ``` `observerModeDidInitiatePurchase` thông báo cho bạn biết rằng người dùng đã khởi tạo một giao dịch mua, và `observerModeDidInitiateRestore` — rằng người dùng đã khởi tạo thao tác khôi phục. Hãy kích hoạt flow mua hàng hoặc khôi phục tùy chỉnh của bạn để phản hồi. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về quá trình mua hàng hoặc khôi phục. Điều này cần thiết để đảm bảo flow hoạt động đúng, chẳng hạn như hiển thị loader, v.v.: | Callback | Mô tả | | :----------------- | :------------------------------------------------------------------------------------------- | | onStartPurchase() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã bắt đầu. | | onFinishPurchase() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã hoàn thành. | | onStartRestore() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã bắt đầu. | | onFinishRestore() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã hoàn thành. | 2. Đăng ký resolver trước khi hiển thị bất kỳ màn hình nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setObserverModeResolver(MyObserverModeResolver()); ``` 3. Tạo và hiển thị flow view như bình thường: [lấy flow và tạo view của nó](flutter-get-pb-paywalls), sau đó [hiển thị nó](flutter-present-paywalls). Không cần thêm tham số nào — một khi resolver đã được đăng ký, mọi flow hoặc paywall do Adapty render đều sẽ xử lý việc mua và khôi phục thông qua nó. :::warning Đừng quên [báo cáo giao dịch và liên kết nó với paywall](report-transactions-observer-mode-flutter). Nếu không, Adapty sẽ không nhận ra giao dịch và sẽ không xác định được paywall nguồn của lần mua. ::: --- # File: flutter-quickstart-manual --- --- title: "Bật tính năng mua hàng trong paywall tùy chỉnh với Flutter SDK" description: "Tích hợp Adapty SDK vào các paywall Flutter tùy chỉnh của bạn để bật tính năng in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Giữ toàn quyền kiểm soát việc triển khai paywall, trong khi SDK của Adapty tự động lấy sản phẩm, xử lý các giao dịch mới và khôi phục các giao dịch trước đó. Hướng dẫn này sử dụng API của Adapty Flutter SDK v4 — nếu bạn đang dùng v3, hãy xem [hướng dẫn migration](migration-to-flutter-sdk-v4) để biết tên các phương thức tương ứng. :::important **Hướng dẫn này dành cho các nhà phát triển đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để kích hoạt tính năng mua hàng, hãy sử dụng [Adapty Paywall Builder](flutter-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall bằng trình chỉnh sửa trực quan không cần code, Adapty xử lý toàn bộ logic mua hàng tự động, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để kích hoạt tính năng in-app purchase, bạn cần nắm vững ba khái niệm cốt lõi: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – cấu hình xác định sản phẩm nào được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi sản phẩm, giá và ưu đãi mà không cần chỉnh sửa code. Trong SDK v4, các biến thể paywall cho một placement được mang bởi đối tượng **flow** — bạn fetch một flow và truy vấn các sản phẩm của nó. - [**Placements**](placements) – nơi và thời điểm bạn hiển thị paywall trong app (ví dụ: `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trên dashboard, sau đó yêu cầu chúng bằng placement ID trong code. Cách này giúp bạn dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho từng nhóm người dùng. Hãy đảm bảo bạn hiểu rõ những khái niệm này ngay cả khi bạn tự xây dựng paywall theo cách riêng. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm mà bạn bán trong ứng dụng. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy danh sách sản phẩm. Để hiểu những gì cần làm trên dashboard, hãy xem hướng dẫn quickstart [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc với hoặc không có xác thực backend ở phía mình. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo những cách khác nhau. Đọc [hướng dẫn quickstart về định danh người dùng](flutter-quickstart-identify) để hiểu rõ sự khác biệt và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy danh sách sản phẩm \{#step-1-get-products\} Để lấy danh sách sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền ID [placement](placements) vào phương thức `getFlow`. 2. Lấy mảng sản phẩm của flow đó bằng phương thức `getPaywallProducts`. ```dart showLineNumbers Future<void> loadPaywall() async { try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final products = await Adapty().getPaywallProducts(flow: flow); // Use products to build your custom paywall UI } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 2. Xử lý thanh toán \{#step-2-accept-purchases\} Khi người dùng chọn một sản phẩm trong paywall tùy chỉnh của bạn, hãy gọi phương thức `makePurchase` với sản phẩm đã chọn. Phương thức này sẽ xử lý luồng thanh toán và trả về hồ sơ người dùng đã được cập nhật. ```dart showLineNumbers Future<void> purchaseProduct(AdaptyPaywallProduct product) async { try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // Purchase successful, profile updated break; case AdaptyPurchaseResultUserCancelled(): // User canceled the purchase break; case AdaptyPurchaseResultPending(): // Purchase is pending (e.g., user will pay offline with cash) break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 3. Khôi phục giao dịch mua \{#step-3-restore-purchases\} Các cửa hàng ứng dụng yêu cầu tất cả ứng dụng có gói đăng ký phải cung cấp cách để người dùng khôi phục giao dịch mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Thao tác này sẽ đồng bộ lịch sử giao dịch mua của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```dart showLineNumbers Future<void> restorePurchases() async { try { final profile = await Adapty().restorePurchases(); // Restore successful, profile updated } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 4. Kiểm tra trạng thái gói đăng ký \{#step-4-check-the-subscription-status\} Sau khi mua hàng hoặc khôi phục, hãy kiểm tra [mức độ truy cập](access-level) của người dùng để quyết định có hiển thị paywall hay mở khóa tính năng trả phí. Các phương thức `makePurchase` và `restorePurchases` đã trả về hồ sơ người dùng được cập nhật; bất cứ khi nào bạn cần trạng thái hiện tại ở nơi khác trong ứng dụng, hãy dùng phương thức `getProfile`: ```dart showLineNumbers Future<bool> hasPremiumAccess() async { try { final profile = await Adapty().getProfile(); return profile.accessLevels['premium']?.isActive ?? false; } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } return false; } ``` Để biết thêm các cách kiểm tra và theo dõi trạng thái gói đăng ký, bao gồm lắng nghe các cập nhật theo thời gian thực, xem [Kiểm tra trạng thái gói đăng ký](flutter-check-subscription-status). ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra các giao dịch mua hàng trong [sandbox App Store](test-purchases-in-sandbox) hoặc [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử từ paywall. Để xem cách hoạt động này trong một triển khai thực tế, hãy xem [PurchasesObserver](https://github.com/adaptyteam/AdaptySDK-Flutter/blob/master/example/lib/purchase_observer.dart) trong ứng dụng mẫu của chúng tôi — nơi minh họa việc xử lý giao dịch mua hàng với cách xử lý lỗi phù hợp, các UI observer, và tích hợp SDK toàn diện. --- # File: fetch-paywalls-and-products-flutter --- --- title: "Lấy paywall và sản phẩm cho remote config paywalls trong Flutter SDK" description: "Lấy paywall và sản phẩm trong Adapty Flutter SDK để tối ưu hóa doanh thu." --- <SDKv4> Trước khi hiển thị Remote Config và custom paywall, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và custom paywall. Để biết hướng dẫn lấy flow và paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo [Lấy flow & paywall](flutter-get-pb-paywalls). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu lấy paywall và sản phẩm trong ứng dụng của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall của bạn](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-flutter) vào ứng dụng di động của bạn. </details> ## Lấy thông tin flow \{#fetch-flow-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào paywall, giúp bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một `AdaptyFlow` từ một trong các [placement](placements) của mình bằng phương thức `getFlow`. :::important **Đừng hardcode product ID.** ID duy nhất bạn nên hardcode là placement ID. Paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay paywall trả về hai sản phẩm và ngày mai là ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp vấn đề về kết nối mạng, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối mạng có kém đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](flutter-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một server dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p></p><p>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.</p> | :::note Trong v4, `getFlow` không nhận tham số `locale`. Đối với paywall tùy chỉnh, tất cả các bản dịch có sẵn đều được trả về trong Remote Config của flow (`flow.remoteConfigs`) — chọn bản phù hợp với thiết bị hoặc cài đặt ngôn ngữ của người dùng. Xem [Localizations và mã locale](flutter-localizations-and-locale-codes). ::: Các tham số trả về: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` chứa các định danh của flow (`instanceIdentity`, `variationId`), tên, placement, các biến thể paywall (`paywalls`), và các Remote Config (`remoteConfigs`). | ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có flow, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(flow: flow); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số trả về: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi triển khai thiết kế paywall của riêng bạn, bạn có thể sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html). Dưới đây là các thuộc tính được sử dụng phổ biến nhất, nhưng hãy tham khảo tài liệu được liên kết để biết đầy đủ chi tiết về tất cả các thuộc tính hiện có. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, chứ không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, dùng `product.price.localizedString`. Việc bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số thông qua `product.price.amount`. Giá trị sẽ được trả về theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedPeriod`. Việc bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo cách lập trình, dùng `product.subscription?.period`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là day, week, month, year hoặc unknown). Giá trị `numberOfUnits` cho biết số đơn vị chu kỳ. Ví dụ: với gói đăng ký theo quý, thuộc tính unit sẽ là `AdaptyPeriodUnit.month` và numberOfUnits sẽ là `3`. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ báo cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi giới thiệu. Trong mỗi đối tượng phase có các thuộc tính hữu ích sau:<br/>• `paymentMode`: một enum với các giá trị `AdaptyPaymentMode.freeTrial`, `AdaptyPaymentMode.payAsYouGo`, `AdaptyPaymentMode.payUpFront` và `AdaptyPaymentMode.unknown`. Dùng thử miễn phí sẽ có kiểu `AdaptyPaymentMode.freeTrial`.<br/>• `price`: Giá giảm dưới dạng số. Với dùng thử miễn phí, giá trị này sẽ là `0`.<br/>• `localizedNumberOfPeriods`: một chuỗi được bản địa hóa theo ngôn ngữ của thiết bị, mô tả độ dài của ưu đãi. Ví dụ: ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết từng phần của chu kỳ ưu đãi bằng thuộc tính này. Cách hoạt động tương tự như mô tả ở phần trước.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký của ưu đãi đã được định dạng theo ngôn ngữ của người dùng. | ## Tăng tốc độ tải flow với flow đối tượng mặc định \{#speed-up-flow-fetching-with-default-audience-flow\} Thông thường, flow được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tối ưu quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này lấy flow của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow bằng phương thức `getFlow`, như được mô tả chi tiết trong phần [Lấy thông tin flow](fetch-paywalls-and-products-flutter#fetch-flow-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị sử dụng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không hiển thị được. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng targeting cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã được mô tả [ở trên](fetch-paywalls-and-products-flutter#fetch-flow-information). ::: ```dart showLineNumbers try { final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình hay gặp tình trạng mất mạng, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng tốc độ tải sẽ nhanh hơn bất kể kết nối mạng có kém đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | </SDKv4> <SDKv3> Trước khi hiển thị Remote Config và paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng phần này đề cập đến Remote Config và paywall tùy chỉnh. Để biết cách lấy paywall cho các paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo [Lấy paywall Paywall Builder và cấu hình của chúng](flutter-get-pb-paywalls). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu lấy paywall và sản phẩm trong ứng dụng di động của bạn (nhấp để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và đưa các sản phẩm vào paywall của bạn](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và đưa paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-flutter) vào ứng dụng di động của bạn. </details> ## Lấy thông tin paywall \{#fetch-paywall-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của mình bằng phương thức `getPaywall`. :::important **Đừng hardcode product ID.** ID duy nhất bạn cần hardcode là placement ID. Paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất cứ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha của Brazil.</p><p></p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối mạng của họ ra sao. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi sử dụng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](flutter-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối mạng kém.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | Đừng hardcode ID sản phẩm! Vì các paywall được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những tình huống này. Ví dụ: nếu ban đầu bạn nhận được 2 sản phẩm, ứng dụng của bạn nên hiển thị 2 sản phẩm đó. Tuy nhiên, nếu sau này bạn nhận được 3 sản phẩm, ứng dụng của bạn nên hiển thị cả 3 mà không cần thay đổi bất kỳ dòng code nào. Thứ duy nhất bạn phải hardcode là placement ID. Các tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) bao gồm: danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có paywall, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký, và một số thuộc tính khác. | Khi tự thiết kế paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html). Dưới đây là các thuộc tính thường dùng nhất — xem tài liệu được liên kết để biết đầy đủ thông tin về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề của sản phẩm, sử dụng `product.localizedTitle`. Lưu ý rằng việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, chứ không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, sử dụng `product.price.localizedString`. Việc bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price.amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, sử dụng `product.price.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), sử dụng `product.subscription?.localizedPeriod`. Việc bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo cách lập trình, sử dụng `product.subscription?.period`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là ngày, tuần, tháng, năm hoặc không xác định). Giá trị `numberOfUnits` sẽ cho bạn biết số đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `AdaptyPeriodUnit.month` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ báo rằng gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `paymentMode`: một enum với các giá trị `AdaptyPaymentMode.freeTrial`, `AdaptyPaymentMode.payAsYouGo`, `AdaptyPaymentMode.payUpFront` và `AdaptyPaymentMode.unknown`. Bản dùng thử miễn phí sẽ có kiểu `AdaptyPaymentMode.freeTrial`.<br/>• `price`: Giá đã giảm dưới dạng số. Với bản dùng thử miễn phí, giá trị này là `0`.<br/>• `localizedNumberOfPeriods`: một chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết riêng lẻ của chu kỳ ưu đãi bằng thuộc tính này. Nó hoạt động theo cách tương tự như mô tả ở phần trước.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo ngôn ngữ của người dùng. | ## Tăng tốc độ tải paywall bằng paywall mặc định cho đối tượng \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ này. Tuy nhiên, trong các trường hợp bạn có nhiều đối tượng và paywall, hoặc người dùng có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà, thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này tải paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là tải paywall bằng phương thức `getPaywall`, như đã trình bày chi tiết trong phần [Lấy thông tin Paywall](fetch-paywalls-and-products-flutter#fetch-paywall-information) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng có phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` đã được mô tả [ở trên](fetch-paywalls-and-products-flutter#fetch-paywall-information). ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Flutter SDK phiên bản 3.2.0 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều subtag được phân tách bằng dấu gạch ngang (**-**). Subtag đầu tiên là mã ngôn ngữ, subtag thứ hai là mã vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong bộ nhớ đệm nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã lưu trong bộ nhớ đệm nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn, bất kể chất lượng kết nối internet của họ. Bộ nhớ đệm được cập nhật thường xuyên, nên có thể dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng bộ nhớ đệm vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc dọn dẹp thủ công.</p> | </SDKv3> --- # File: present-remote-config-paywalls-flutter --- --- title: "Hiển thị paywall được thiết kế bằng Remote Config trong Flutter SDK" description: "Khám phá cách hiển thị paywall Remote Config trong Adapty Flutter SDK để cá nhân hóa trải nghiệm người dùng." --- <SDKv4> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng có thể thấy nó. Vì Remote Config linh hoạt theo nhu cầu của bạn, bạn toàn quyền quyết định nội dung hiển thị và giao diện của paywall. Chúng tôi cung cấp một phương thức để lấy cấu hình remote, giúp bạn chủ động hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## Lấy Remote Config của paywall và hiển thị \{#get-paywall-remote-config-and-present-it\} Trong v4, flow chứa danh sách `remoteConfigs` — mỗi remote config tương ứng với một ngôn ngữ đã được cấu hình. Chọn mục phù hợp với ngôn ngữ của người dùng và trích xuất các giá trị cần thiết. Xem [Bản địa hóa và mã ngôn ngữ](flutter-localizations-and-locale-codes) để chọn đúng ngôn ngữ. ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // one entry per configured localization; fall back to the first one final config = flow.remoteConfigs.firstWhereOrNull((c) => c.locale == 'en') ?? flow.remoteConfig; final String? headerText = config?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và kết hợp chúng thành một trang có giao diện đẹp. Hãy đảm bảo thiết kế tương thích với nhiều loại màn hình điện thoại và hướng xoay khác nhau, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy nhớ ghi lại sự kiện xem paywall như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](flutter-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](flutter-use-fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi chúng tôi tự động thu thập dữ liệu về các giao dịch mua, việc ghi lại lượt xem paywall cần có sự can thiệp của bạn, vì chỉ bạn mới biết khi nào khách hàng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowFlow(flow: flow)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowFlow(flow: flow)` nếu bạn đang hiển thị các flow hoặc paywall được tạo trong [builder](adapty-paywall-builder). ::: ```dart showLineNumbers try { await Adapty().logShowFlow(flow: flow); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:----------------------------------------------------------------------| | **flow** | bắt buộc | Một đối tượng `AdaptyFlow`. | </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng có thể thấy nó. Vì Remote Config cho phép linh hoạt theo nhu cầu của bạn, bạn hoàn toàn quyết định những gì được đưa vào và giao diện paywall trông như thế nào. Chúng tôi cung cấp một phương thức để lấy cấu hình Remote Config, giúp bạn chủ động hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## Lấy Remote Config của paywall và hiển thị \{#get-paywall-remote-config-and-present-it\} Để lấy Remote Config của một paywall, hãy truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID"); final String? headerText = paywall.remoteConfig?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và kết hợp chúng thành một trang có giao diện hấp dẫn. Hãy đảm bảo thiết kế phù hợp với nhiều loại màn hình điện thoại và các hướng xoay khác nhau, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy đảm bảo ghi lại sự kiện xem paywall như mô tả bên dưới, cho phép Adapty analytics thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](flutter-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](flutter-use-fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu suất của các paywall. Trong khi chúng tôi tự động thu thập dữ liệu về giao dịch mua hàng, việc ghi lại lượt xem paywall cần có sự tham gia của bạn vì chỉ bạn mới biết khi nào người dùng nhìn thấy một paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị các paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```dart showLineNumbers try { final result = await Adapty().logShowPaywall(paywall: paywall); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:----------------------------------------------------------------------| | **paywall** | required | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). | </SDKv3> --- # File: flutter-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng di động với Flutter SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng di động là bước thiết yếu để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ hiển thị paywall thôi là đủ để hỗ trợ mua hàng nếu bạn dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall của mình. Nếu bạn không sử dụng Paywall Builder, bạn phải dùng một phương thức riêng gọi là `.makePurchase()` để hoàn tất giao dịch mua và mở khóa nội dung mong muốn. Phương thức này là cổng để người dùng tương tác với các paywall và thực hiện giao dịch của họ. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm mà người dùng muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm thanh toán. :::warning Lưu ý rằng ưu đãi giới thiệu sẽ chỉ được áp dụng tự động nếu bạn sử dụng các paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [kiểm tra điều kiện nhận ưu đãi giới thiệu của người dùng trên iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Bỏ qua bước này có thể khiến ứng dụng của bạn bị từ chối khi phát hành. Hơn nữa, người dùng đủ điều kiện nhận ưu đãi giới thiệu có thể bị tính giá đầy đủ. ::: Hãy đảm bảo bạn đã [thực hiện cấu hình ban đầu](quickstart) mà không bỏ qua bất kỳ bước nào. Nếu không, chúng tôi không thể xác thực các giao dịch mua. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang dùng [Paywall Builder](adapty-paywall-builder)?** Các giao dịch mua được xử lý tự động — bạn có thể bỏ qua bước này. **Cần hướng dẫn từng bước?** Xem [hướng dẫn quickstart](flutter-implement-paywalls-manually) để biết hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```dart showLineNumbers try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): if (profile.accessLevels['premium']?.isActive ?? false) { // Grant access to the paid features } break; case AdaptyPurchaseResultPending(): break; case AdaptyPurchaseResultUserCancelled(): break; default: break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | bắt buộc | Một đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) được lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>Nếu yêu cầu thành công, phản hồi sẽ chứa đối tượng này. Đối tượng [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) cung cấp thông tin đầy đủ về mức độ truy cập, các gói đăng ký và sản phẩm mua một lần của người dùng trong ứng dụng.</p><p>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.</p> | :::warning **Lưu ý:** Nếu bạn đang dùng StoreKit của Apple phiên bản thấp hơn v2.0 và Adapty SDK phiên bản thấp hơn v2.9.0, bạn cần cung cấp [Apple App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple deprecate. ::: ## Thay đổi gói đăng ký khi mua hàng \{#change-subscription-when-making-a-purchase\} Khi người dùng chọn một gói đăng ký mới thay vì gia hạn gói hiện tại, cách thức hoạt động sẽ phụ thuộc vào cửa hàng: - Với App Store, gói đăng ký sẽ tự động được cập nhật trong nhóm gói đăng ký. Nếu người dùng mua một gói đăng ký từ nhóm này trong khi đã có gói đăng ký từ nhóm khác, cả hai gói sẽ cùng hoạt động đồng thời. - Với Google Play, gói đăng ký không tự động được cập nhật. Bạn cần xử lý việc chuyển đổi trong code của ứng dụng như mô tả bên dưới. Để thay thế gói đăng ký bằng một gói khác trên Android, hãy gọi phương thức `.makePurchase()` với tham số bổ sung: ```dart showLineNumbers try { final subscriptionUpdateParams = AdaptyAndroidSubscriptionUpdateParameters( 'OLD_PRODUCT_ID', AdaptyAndroidSubscriptionUpdateReplacementMode.immediateWithTimeProration, ); final result = await Adapty().makePurchase( product: product, parameters: AdaptyPurchaseParameters( subscriptionUpdateParams: subscriptionUpdateParams, ), ); // successful cross-grade } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Tham số yêu cầu bổ sung: | Parameter | Presence | Description | | :--------------------------- | :------- |:--------------------------------------------------------------------------------------------------------| | **parameters** | required | một đối tượng `AdaptyPurchaseParameters` với trường `subscriptionUpdateParams` được đặt thành một đối tượng [`AdaptyAndroidSubscriptionUpdateParameters`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyAndroidSubscriptionUpdateParameters-class.html). | Bạn có thể đọc thêm về gói đăng ký và các chế độ thay thế trong tài liệu dành cho nhà phát triển của Google: - [Về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Đề xuất từ Google về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Chế độ thay thế [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Lưu ý: phương thức này chỉ áp dụng cho việc nâng cấp gói đăng ký. Hạ cấp không được hỗ trợ. - Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Việc thay đổi gói đăng ký thực sự chỉ xảy ra khi chu kỳ thanh toán hiện tại kết thúc. ## Đổi mã ưu đãi trong iOS \{#redeem-offer-codes-in-ios\} <Details> <summary>Về offer code</summary> Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} :::warning Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. ::: Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác. </Details> Để hiển thị màn hình nhập mã trong ứng dụng của bạn: ```dart showLineNumbers try { await Adapty().presentCodeRedemptionSheet(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` :::danger Theo quan sát của chúng tôi, màn hình đổi mã ưu đãi trong một số ứng dụng có thể hoạt động không ổn định. Chúng tôi khuyến nghị chuyển hướng người dùng trực tiếp đến App Store. Để thực hiện điều này, bạn cần mở URL theo định dạng sau: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ### Quản lý gói trả trước (Android) \{#manage-prepaid-plans-android\} Nếu người dùng ứng dụng của bạn có thể mua [gói trả trước](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (ví dụ: mua gói đăng ký không tự gia hạn trong vài tháng), bạn có thể bật [giao dịch chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleEnablePendingPrepaidPlans(true), ); ``` --- # File: flutter-restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng di động với Flutter SDK" description: "Tìm hiểu cách khôi phục giao dịch mua trong Adapty để đảm bảo trải nghiệm người dùng liền mạch." --- Khôi phục giao dịch mua trên cả iOS và Android là tính năng cho phép người dùng lấy lại quyền truy cập vào nội dung đã mua trước đó, chẳng hạn như các gói đăng ký hoặc in-app purchase, mà không bị tính phí thêm. Tính năng này đặc biệt hữu ích cho những người dùng đã gỡ cài đặt và cài lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập nội dung đã mua trước đó mà không phải thanh toán lại. :::note Trong các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), giao dịch mua được khôi phục tự động mà không cần thêm code từ bạn. Nếu đó là trường hợp của bạn — bạn có thể bỏ qua bước này. ::: Để khôi phục giao dịch mua khi bạn không sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`: ```dart showLineNumbers try { final profile = await Adapty().restorePurchases(); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful access restore } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số phản hồi: | Tham số | Mô tả | |---------|-----------| | **Profile** | <p>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.</p><p>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.</p> | :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: --- # File: implement-observer-mode-flutter --- --- title: "Triển khai Observer mode trong Flutter SDK" description: "Triển khai observer mode trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong Flutter SDK." --- Nếu bạn đã có hệ thống xử lý in-app purchase riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp phân tích nâng cao và tích hợp liền mạch với các hệ thống attribution và phân tích. Nếu cách này phù hợp với bạn, bạn chỉ cần: 1. Bật tính năng này khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. Làm theo hướng dẫn cài đặt cho [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk). 2. [Báo cáo giao dịch](report-transactions-observer-mode-flutter) từ hệ thống mua hàng hiện tại của bạn lên Adapty. ## Thiết lập chế độ Observer \{#observer-mode-setup\} Bật chế độ Observer nếu bạn tự xử lý các giao dịch mua và trạng thái gói đăng ký, và chỉ dùng Adapty để gửi sự kiện gói đăng ký và phân tích. :::important Khi chạy ở chế độ Observer, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều này. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withObserverMode(true) // Enable observer mode ..withLogLevel(AdaptyLogLevel.verbose), ); ``` Tham số: | Tham số | Mô tả | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean kiểm soát [Chế độ Observer](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thêm một số bước cấu hình trong Observer mode. Ngoài các bước đã thực hiện ở trên, bạn cần làm thêm: 1. Hiển thị paywall như thông thường đối với [Remote Config paywall](present-remote-config-paywalls-flutter). 3. [Liên kết paywall](report-transactions-observer-mode-flutter) với các giao dịch mua hàng. :::tip Trong SDK v4, bạn cũng có thể hiển thị các flow và paywall được Adapty render trong Observer mode: đăng ký một `AdaptyUIObserverModeResolver` để thực hiện giao dịch mua hoặc khôi phục bằng code của chính bạn khi người dùng nhấn vào nút tương ứng. Xem [Hiển thị flow trong Observer mode](flutter-present-flows-in-observer-mode). ::: --- # File: report-transactions-observer-mode-flutter --- --- title: "Báo cáo giao dịch ở chế độ Observer Mode trong Flutter SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trong Flutter SDK." --- <Tabs groupId="sdk-version" queryString> <TabItem value="current" label="Adapty SDK v3.4+ (hiện tại)" default> Trong Observer mode, Adapty SDK không thể tự theo dõi các giao dịch mua được thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng. Điều quan trọng là phải thiết lập điều này **trước khi** phát hành ứng dụng để tránh sai sót trong phân tích. Sử dụng `reportTransaction` để báo cáo từng giao dịch một cách rõ ràng để Adapty nhận biết. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận ra giao dịch đó, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy đính kèm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```dart showLineNumbers try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | required | <ul><li> Đối với iOS: Mã định danh của giao dịch.</li><li> Đối với Android: Mã định danh dạng chuỗi `purchase.getOrderId` của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong billing library.</li></ul> | | variationId | optional | Mã định danh dạng chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). | </TabItem> <TabItem value="old" label="Adapty SDK 3.3.x (legacy)" default> Trong Observer mode, Adapty SDK không thể tự theo dõi các giao dịch mua được thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng hoặc khôi phục chúng. Điều quan trọng là phải thiết lập điều này **trước khi** phát hành ứng dụng để tránh lỗi trong analytics. Sử dụng `reportTransaction` trên cả hai nền tảng để báo cáo từng giao dịch một cách rõ ràng, và sử dụng `restorePurchases` trên Android như một bước bổ sung để đảm bảo Adapty nhận ra giao dịch đó. :::warning **Đừng bỏ qua bước báo cáo giao dịch và khôi phục mua hàng!** Nếu bạn không gọi các phương thức này, Adapty sẽ không nhận ra giao dịch, giao dịch sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy truyền `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```dart showLineNumbers // every time when calling transaction.finish() if (Platform.isAndroid) { try { await Adapty().restorePurchases(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | <ul><li> Với iOS, StoreKit 1: một đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).</li><li> Với iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).</li><li> Với Android: Chuỗi định danh (purchase.getOrderId của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) thuộc billing library.</li></ul> | | variationId | tùy chọn | Chuỗi định danh của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). | </TabItem> <TabItem value="old2" label="Adapty SDK up to 3.2.x (legacy)" default> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> **Báo cáo giao dịch** - Các phiên bản đến 3.1.x tự động lắng nghe các giao dịch trên App Store, do đó không cần báo cáo thủ công. - Phiên bản 3.2 không hỗ trợ Observer Mode. </TabItem> <TabItem value="kotlin" label="Android and Android-based cross-platforms" default> **Báo cáo giao dịch** Dùng `restorePurchases` để báo cáo giao dịch cho Adapty trong Observer Mode, như được giải thích trên trang [Khôi phục giao dịch mua trong code mobile](flutter-restore-purchase). :::warning **Đừng bỏ qua bước báo cáo giao dịch!** Nếu bạn không gọi `restorePurchases`, Adapty sẽ không nhận ra giao dịch đó, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: </TabItem> </Tabs> **Liên kết paywall với giao dịch** SDK của Adapty không thể xác định nguồn gốc của các giao dịch mua, vì bạn là người xử lý chúng. Do đó, nếu bạn dự định sử dụng paywall và/hoặc A/B test trong chế độ Observer, bạn cần liên kết giao dịch đến từ cửa hàng ứng dụng với paywall tương ứng trong code ứng dụng di động của mình. Điều này cần được thực hiện đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến sai sót trong phân tích. ```dart final transactionId = transaction.transactionIdentifier final variationId = paywall.variationId try { await Adapty().setVariationId('transactionId', variationId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </TabItem> </Tabs> --- # File: flutter-troubleshoot-purchases --- --- title: "Xử lý sự cố mua hàng trong Flutter SDK" description: "Xử lý sự cố mua hàng trong Flutter SDK" --- Hướng dẫn này giúp bạn giải quyết các vấn đề thường gặp khi triển khai mua hàng thủ công trong Flutter SDK. ## makePurchase được gọi thành công nhưng hồ sơ người dùng không được cập nhật \{#makepurchase-is-called-successfully-but-the-profile-is-not-being-updated\} **Vấn đề**: Phương thức `makePurchase` hoàn tất thành công, nhưng hồ sơ người dùng và trạng thái gói đăng ký không được cập nhật trong Adapty. **Nguyên nhân**: Điều này thường cho thấy quá trình thiết lập Google Play Store chưa hoàn chỉnh hoặc có vấn đề về cấu hình. **Giải pháp**: Đảm bảo bạn đã hoàn thành tất cả [các bước thiết lập Google Play](initial-android). ## makePurchase được gọi hai lần \{#makepurchase-is-invoked-twice\} **Vấn đề**: Phương thức `makePurchase` đang được gọi nhiều lần cho cùng một giao dịch mua. **Nguyên nhân**: Điều này thường xảy ra khi flow mua hàng được kích hoạt nhiều lần do vấn đề quản lý trạng thái giao diện hoặc người dùng thao tác quá nhanh. **Giải pháp**: Đảm bảo bạn đã hoàn thành tất cả [các bước thiết lập Google Play](initial-android). ## AdaptyError.cantMakePayments ở chế độ observer \{#adaptyerrorcantmakepayments-in-observer-mode\} **Vấn đề**: Bạn nhận được `AdaptyError.cantMakePayments` khi sử dụng `makePurchase` ở chế độ observer. **Nguyên nhân**: Ở chế độ observer, bạn nên tự xử lý giao dịch mua ở phía mình, không sử dụng phương thức `makePurchase` của Adapty. **Giải pháp**: Nếu bạn dùng `makePurchase` cho giao dịch mua, hãy tắt chế độ observer. Bạn cần chọn một trong hai: dùng `makePurchase` hoặc tự xử lý giao dịch mua ở phía mình trong chế độ observer. Xem [Triển khai chế độ Observer](implement-observer-mode-flutter) để biết thêm chi tiết. ## Lỗi Adapty: (code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null) \{#adapty-error-code-103-message-play-market-request-failed-on-purchases-updated-responsecode3-debugmessagebilling-unavailable-detail-null\} **Vấn đề**: Bạn nhận được lỗi billing không khả dụng từ Google Play Store. **Nguyên nhân**: Lỗi này không liên quan đến Adapty. Đây là lỗi từ Google Play Billing Library cho biết tính năng thanh toán không khả dụng trên thiết bị. **Giải pháp**: Lỗi này không liên quan đến Adapty. Bạn có thể tìm hiểu thêm trong tài liệu Play Store: [Handle BillingResult response codes](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers. ## Không tìm thấy makePurchasesCompletionHandlers \{#not-found-makepurchasescompletionhandlers\} **Vấn đề**: Bạn gặp sự cố với `makePurchasesCompletionHandlers` không được tìm thấy. **Nguyên nhân**: Vấn đề này thường liên quan đến sự cố khi kiểm thử trong môi trường sandbox. **Giải pháp**: Tạo một người dùng sandbox mới và thử lại. Cách này thường giải quyết được các vấn đề với purchase completion handler trong sandbox. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các sự cố liên quan đến mua hàng khác chưa được đề cập ở trên. **Giải pháp**: Migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](flutter-sdk-migration-guides) nếu cần. Nhiều vấn đề đã được giải quyết trong các phiên bản SDK mới hơn. --- # File: flutter-identifying-users --- --- title: "Xác định người dùng trong Flutter SDK" description: "Xác định người dùng trong Adapty để cải thiện trải nghiệm đăng ký được cá nhân hóa." --- Adapty tạo một ID hồ sơ nội bộ cho mỗi người dùng. Tuy nhiên, nếu bạn có hệ thống xác thực riêng, bạn nên đặt Customer User ID của mình. Bạn có thể tìm người dùng theo Customer User ID của họ trong phần [Profiles](profiles-crm) và sử dụng nó trong [server-side API](getting-started-with-server-side-api), thứ sẽ được gửi đến tất cả các tích hợp. ### Đặt Customer User ID khi cấu hình \{#setting-customer-user-id-on-configuration\} Nếu bạn đã có user ID khi cấu hình, hãy truyền nó qua tham số `customerUserId` vào phương thức `.activate()`: ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) ); } catch (e) { // handle the error } ``` :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Đặt Customer User ID sau khi cấu hình \{#setting-customer-user-id-after-configuration\} Nếu bạn chưa có user ID khi cấu hình SDK, bạn có thể đặt nó bất cứ lúc nào sau đó bằng phương thức `.identify()`. Trường hợp phổ biến nhất để sử dụng phương thức này là sau khi đăng ký hoặc đăng nhập, khi người dùng chuyển từ người dùng ẩn danh sang người dùng đã xác thực. ```dart showLineNumbers try { await Adapty().identify(customerUserId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số yêu cầu: - **Customer User ID** (bắt buộc): chuỗi định danh người dùng. :::warning Gửi lại dữ liệu người dùng quan trọng Trong một số trường hợp, chẳng hạn khi người dùng đăng nhập lại vào tài khoản của họ, máy chủ Adapty đã có thông tin về người dùng đó. Trong những tình huống này, Adapty SDK sẽ tự động chuyển sang làm việc với người dùng mới. Nếu bạn đã truyền dữ liệu nào đó cho người dùng ẩn danh, chẳng hạn như thuộc tính tùy chỉnh hoặc attribution từ các mạng bên thứ ba, bạn cần gửi lại dữ liệu đó cho người dùng đã được xác định. Ngoài ra, bạn cần gọi lại tất cả các paywall và sản phẩm sau khi xác định người dùng, vì dữ liệu của người dùng mới có thể khác. ::: ### Đăng xuất và đăng nhập \{#logging-out-and-logging-in\} Bạn có thể đăng xuất người dùng bất cứ lúc nào bằng cách gọi phương thức `.logout()`: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` Sau đó bạn có thể đăng nhập người dùng bằng phương thức `.identify()`. ## Gán `appAccountToken` (iOS) \{#assign-appaccounttoken-ios\} [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) là một **UUID** cho phép bạn liên kết các giao dịch App Store với danh tính người dùng nội bộ của mình. StoreKit liên kết token này với mọi giao dịch, giúp backend của bạn khớp dữ liệu App Store với người dùng của bạn. Hãy sử dụng một UUID ổn định được tạo cho từng người dùng và tái sử dụng nó cho cùng một tài khoản trên nhiều thiết bị. Điều này đảm bảo rằng các giao dịch mua và thông báo từ App Store được liên kết đúng cách. Bạn có thể đặt token theo hai cách – trong quá trình kích hoạt SDK hoặc khi xác định người dùng. :::important Bạn phải luôn truyền `appAccountToken` cùng với `customerUserId`. Nếu bạn chỉ truyền token, nó sẽ không được đưa vào giao dịch. ::: ```dart showLineNumbers // During configuration: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN") ); } catch (e) { // handle the error } // Or when identifying users try { await Adapty().identify(customerUserId, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Đặt obfuscated account ID (Android) \{#set-obfuscated-account-ids-android\} Google Play yêu cầu obfuscated account ID cho một số trường hợp sử dụng nhằm tăng cường quyền riêng tư và bảo mật người dùng. Các ID này giúp Google Play xác định các giao dịch mua trong khi vẫn giữ thông tin người dùng ẩn danh, điều này đặc biệt quan trọng để phòng chống gian lận và phân tích. Bạn có thể cần đặt các ID này nếu ứng dụng của bạn xử lý dữ liệu người dùng nhạy cảm hoặc nếu bạn cần tuân thủ các quy định về quyền riêng tư cụ thể. Các obfuscated ID cho phép Google Play theo dõi các giao dịch mua mà không để lộ định danh người dùng thực tế. ```dart showLineNumbers // During configuration: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID") ); } catch (e) { // handle the error } // Or when identifying users try { await Adapty().identify(customerUserId, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: flutter-setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong Flutter SDK" description: "Tìm hiểu cách thiết lập thuộc tính người dùng trong Adapty để phân khúc đối tượng tốt hơn." --- Bạn có thể thiết lập các thuộc tính tùy chọn như email, số điện thoại, v.v. cho người dùng ứng dụng. Sau đó, bạn có thể dùng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM. ### Cài đặt thuộc tính người dùng \{#setting-user-attributes\} Để cài đặt thuộc tính người dùng, gọi phương thức `.updateProfile()`: ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setEmail("email@email.com") ..setPhoneNumber("+18888888888") ..setFirstName('John') ..setLastName('Appleseed') ..setGender(AdaptyProfileGender.other) ..setBirthday(DateTime(1970, 1, 3)); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Xin lưu ý rằng các thuộc tính bạn đã đặt trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các key được phép \{#the-allowed-keys-list\} Các key `<Key>` được phép của `AdaptyProfileParameters.Builder` và các giá trị `<Value>` tương ứng được liệt kê bên dưới: | Key | Value | |---|-----| | <p>email</p><p>phoneNumber</p><p>firstName</p><p>lastName</p> | String | | gender | Enum, các giá trị cho phép: `female`, `male`, `other` | | birthday | Date | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể tự thiết lập các thuộc tính tùy chỉnh cho người dùng. Những thuộc tính này thường liên quan đến cách người dùng sử dụng ứng dụng của bạn. Ví dụ, với ứng dụng thể dục, đó có thể là số buổi tập mỗi tuần; với ứng dụng học ngoại ngữ, đó có thể là trình độ kiến thức của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi nhắm mục tiêu, đồng thời cũng có thể dùng trong analytics để xác định những chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```dart showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..setCustomStringAttribute('value1', 'key1') ..setCustomDoubleAttribute(1.0, 'key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Để xóa key hiện có, sử dụng phương thức `.withRemoved(customAttributeForKey:)`: ```dart showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..removeCustomAttribute('key1') ..removeCustomAttribute('key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Đôi khi bạn cần biết những thuộc tính tùy chỉnh nào đã được cài đặt trước đó. Để làm điều này, hãy sử dụng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không còn cập nhật, vì các thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau vào bất kỳ lúc nào, do đó các thuộc tính trên server có thể đã thay đổi sau lần đồng bộ cuối cùng. ::: ### Giới hạn \{#limits\} - Tối đa 30 thuộc tính tùy chỉnh mỗi người dùng - Tên key tối đa 30 ký tự. Tên key có thể bao gồm các ký tự chữ và số, cùng với các ký tự sau: `_` `-` `.` - Giá trị có thể là chuỗi hoặc số thực (float) với tối đa 50 ký tự. --- # File: flutter-listen-subscription-changes --- --- title: "Kiểm tra trạng thái gói đăng ký trong Flutter SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký người dùng trong Adapty để cải thiện việc giữ chân khách hàng trong ứng dụng Flutter của bạn." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải chèn thủ công các product ID vào code. Thay vào đó, bạn có thể dễ dàng xác nhận trạng thái gói đăng ký của người dùng bằng cách kiểm tra [mức độ truy cập](access-level) đang hoạt động. <details> <summary>Trước khi bắt đầu kiểm tra trạng thái gói đăng ký (Nhấn để mở rộng)</summary> - 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) </details> ## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là các thuộc tính của đối tượng [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html). Chúng tôi khuyến nghị bạn lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](flutter-identifying-users#setting-customer-user-id-on-configuration), rồi cập nhật lại mỗi khi có thay đổi. Bằng cách này, bạn có thể sử dụng đối tượng hồ sơ mà không cần liên tục gửi yêu cầu lấy lại. Để nhận thông báo về các cập nhật hồ sơ người dùng, hãy lắng nghe các thay đổi hồ sơ như được mô tả trong phần [Lắng nghe cập nhật hồ sơ, bao gồm mức độ truy cập](flutter-listen-subscription-changes) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ máy chủ \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ máy chủ, sử dụng phương thức `.getProfile()`: ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số phản hồi: | Tham số | Mô tả | | --------- | ------------------------------------------------------------ | | Profile | <p>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.</p><p></p><p>Phương thức `.getProfile` luôn cố gắng truy vấn API nên cho kết quả cập nhật nhất. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ máy chủ, dữ liệu từ bộ nhớ cache sẽ được trả về. Cũng cần lưu ý rằng Adapty SDK thường xuyên cập nhật cache của `AdaptyProfile` để giữ cho thông tin này luôn mới nhất có thể.</p> | Phương thức `.getProfile()` cung cấp cho bạn hồ sơ người dùng, từ đó bạn có thể kiểm tra trạng thái mức độ truy cập. Mỗi ứng dụng có thể có nhiều mức độ truy cập. Ví dụ, nếu bạn có ứng dụng tin tức và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết trường hợp, bạn chỉ cần một mức độ truy cập — khi đó bạn có thể dùng mức độ truy cập mặc định "premium". Dưới đây là ví dụ kiểm tra mức độ truy cập mặc định "premium": ```dart showLineNumbers try { final profile = await Adapty().getProfile(); if (profile?.accessLevels['premium']?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Lắng nghe cập nhật trạng thái đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận thông báo từ Adapty, bạn cần thực hiện thêm một số cấu hình: ```dart showLineNumbers Adapty().didUpdateProfileStream.listen((profile) { // handle any changes to subscription state }); ``` Adapty cũng kích hoạt một sự kiện khi ứng dụng khởi động. Trong trường hợp này, trạng thái gói đăng ký được lưu trong cache sẽ được truyền vào. ### Bộ nhớ đệm trạng thái gói đăng ký \{#subscription-status-cache\} Bộ nhớ đệm được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi máy chủ không khả dụng, dữ liệu đã được lưu trong bộ nhớ đệm vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ người dùng. Tuy nhiên, cần lưu ý rằng không thể truy vấn dữ liệu trực tiếp từ bộ nhớ cache. SDK sẽ định kỳ truy vấn máy chủ mỗi phút một lần để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ người dùng. Nếu có bất kỳ thay đổi nào, chẳng hạn như giao dịch mới hoặc các cập nhật khác, chúng sẽ được gửi đến dữ liệu trong bộ nhớ cache để đảm bảo đồng bộ với máy chủ. --- # File: flutter-deal-with-att --- --- title: "Xử lý ATT trong Flutter SDK" description: "Bắt đầu với Adapty trên Flutter để đơn giản hóa việc thiết lập và quản lý gói đăng ký." --- Nếu ứng dụng của bạn sử dụng framework AppTrackingTransparency và hiển thị yêu cầu ủy quyền theo dõi ứng dụng cho người dùng, bạn cần gửi [trạng thái ủy quyền](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) đó đến Adapty. ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setAppTrackingTransparencyStatus(AdaptyIOSAppTrackingTransparencyStatus.authorized); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::warning Chúng tôi khuyến nghị bạn gửi giá trị này càng sớm càng tốt khi nó thay đổi. Chỉ như vậy dữ liệu mới được gửi kịp thời đến các tích hợp bạn đã cấu hình. ::: --- # File: kids-mode-flutter --- --- title: "Chế độ Trẻ em trong Flutter SDK" description: "Dễ dàng bật Chế độ Trẻ em để tuân thủ chính sách của Apple và Google. Không thu thập IDFA, GAID hay dữ liệu quảng cáo trong Flutter SDK." --- Nếu ứng dụng Flutter của bạn dành cho trẻ em, bạn phải tuân thủ chính sách của [Apple](https://developer.apple.com/kids/) và [Google](https://support.google.com/googleplay/android-developer/answer/9893335). Nếu đang sử dụng Adapty SDK, bạn chỉ cần thực hiện vài bước đơn giản để cấu hình SDK đáp ứng các chính sách này và vượt qua quá trình xét duyệt của cửa hàng ứng dụng. ## Yêu cầu là gì? \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyên bạn nên thận trọng khi sử dụng customer user ID. User ID có định dạng `<FirstName.LastName>` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc sử dụng email. Đối với Kids Mode, phương pháp tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: hashed ID hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ quy định. ## Bật Chế độ Trẻ em \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\} Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để thực hiện, vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** trong phần **Collect users' IP address**. ### Cập nhật trong mã nguồn ứng dụng di động của bạn \{#updates-in-your-mobile-app-code\} Để tuân thủ chính sách, hãy tắt tính năng thu thập IDFA (cho iOS), GAID/AAID (cho Android) và địa chỉ IP của người dùng. **Android: Cập nhật cấu hình SDK của bạn** ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') // highlight-start ..withGoogleAdvertisingIdCollectionDisabled(true) // set to `true` ..withIpAddressCollectionDisabled(true), // set to `true` // highlight-end ); } catch (e) { // handle the error } ``` **iOS: Bật Chế độ trẻ em trong SDK v4** :::important Trong SDK v4, native iOS SDK được cài đặt qua Swift Package Manager, và Chế độ trẻ em được bật thông qua trait `KidsMode` của Swift package — trait này sẽ loại bỏ toàn bộ code liên quan đến IDFA, AdSupport và AppTrackingTransparency trong quá trình biên dịch. Yêu cầu **Xcode 26** trở lên. ::: Trong SDK v4, hãy dùng package `adapty_flutter_kids` thay cho `adapty_flutter` trong `pubspec.yaml`. Đây là biến thể Chế độ trẻ em của plugin với cùng public API và cùng phiên bản — điểm khác biệt duy nhất là native iOS SDK của nó được build với trait `KidsMode`: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter_kids: 4.0.0 ``` Mã Dart của bạn vẫn giữ nguyên — chỉ cần cập nhật import sang tên package mới: ```dart showLineNumbers title="Dart" ``` **iOS: Bật Kids Mode bằng CocoaPods (SDK v3)** 1. Cập nhật Podfile của bạn: - Nếu bạn **chưa** có mục `post_install`, hãy thêm toàn bộ đoạn code bên dưới. - Nếu bạn **đã** có mục `post_install`, hãy gộp các dòng được highlight vào đó. ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. Áp dụng các thay đổi bằng cách chạy ```sh showLineNumbers title="Shell" pod install ``` --- # File: flutter-get-onboardings --- --- title: "Lấy onboarding trong Flutter SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho Flutter." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: khác với onboardings chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Flutter của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement và cấu hình view của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Flutter SDK](sdk-installation-flutter) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Lấy dữ liệu onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng trình tạo no-code của chúng tôi, nó được lưu trữ dưới dạng một container chứa cấu hình mà ứng dụng cần lấy về và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung hiển thị, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập từ form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần phải tự triển khai việc theo dõi lượt xem riêng. Để có hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để hình ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy onboarding, hãy dùng phương thức `getOnboarding`: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboarding(placementId: "YOUR_PLACEMENT_ID"); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` Sau đó, gọi phương thức `createOnboardingView` để lấy view bạn sẽ hiển thị. :::warning Kết quả của phương thức `createOnboardingView` chỉ có thể được sử dụng một lần. Nếu bạn cần sử dụng lại, hãy gọi lại phương thức `createOnboardingView`. Việc gọi hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers try { final onboardingView = await Adapty().createOnboardingView(onboarding: onboarding); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh bản địa hóa của onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai thẻ con phân cách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha Brazil.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyên dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp vấn đề với kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối internet có chập chờn đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập phòng trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản onboarding mới nhất trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet bị hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | ## Các tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyOnboarding-class.html) chứa: mã định danh và cấu hình onboarding, Remote Config, cùng một số thuộc tính khác. | ## Tăng tốc tải onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và onboarding, và người dùng có kết nối internet yếu, việc tải onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống đó, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getOnboardingForDefaultAudience`, phương thức này sẽ lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như đã mô tả chi tiết trong phần [Fetch Onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc dùng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, đòi hỏi thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không đúng. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "Tất cả người dùng", loại bỏ khả năng nhắm mục tiêu theo quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ tải nhanh hơn quan trọng hơn các hạn chế này trong trường hợp của bạn, hãy sử dụng `getOnboardingForDefaultAudience` như hướng dẫn bên dưới. Nếu không, hãy dùng `getOnboarding` như đã mô tả [ở trên](#fetch-onboarding). ::: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboardingForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |-----------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của bản địa hóa onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai subtag được phân tách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha của Brazil.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình hay gặp tình trạng mạng không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ có bập bõm đến đâu. Cache được cập nhật thường xuyên nên an toàn khi sử dụng trong suốt phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài lại hoặc khi dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và các onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập phòng khi CDN không thể truy cập được. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản onboarding mới nhất trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | --- # File: flutter-present-onboardings --- --- title: "Hiển thị onboarding trong Flutter SDK" description: "Tìm hiểu cách hiển thị onboarding hiệu quả để tăng tỷ lệ chuyển đổi." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: không giống như onboarding chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại hiệu ứng animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh một onboarding bằng builder, bạn không cần lo về việc render nó trong code Flutter để hiển thị cho người dùng. Onboarding đó đã chứa đầy đủ cả nội dung lẫn cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Flutter SDK](sdk-installation-flutter) 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Adapty Flutter SDK cung cấp hai cách để hiển thị onboarding: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị một onboarding dưới dạng màn hình độc lập, sử dụng phương thức `onboardingView.present()` trên `onboardingView` được tạo bởi phương thức `createOnboardingView`. Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị onboarding lại, hãy gọi `createOnboardingView` thêm một lần nữa để tạo một instance `onboardingView` mới. :::warning Việc tái sử dụng cùng một `onboardingView` mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers title="Flutter" try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Đóng onboarding \{#dismiss-the-onboarding\} Khi bạn cần đóng onboarding theo cách lập trình, hãy sử dụng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await onboardingView.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\} Cấu hình cách onboarding được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await onboardingView.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng một onboarding vào cây widget hiện có của bạn, hãy sử dụng widget `AdaptyUIOnboardingPlatformView` trực tiếp trong cây widget Flutter của bạn. ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, // The onboarding object you fetched onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` :::note Để platform view trên Android hoạt động, đảm bảo `MainActivity` của bạn extends `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: ## Màn hình tải trong quá trình onboarding \{#loader-during-onboarding\} Khi hiển thị onboarding, bạn có thể thấy một màn hình tải ngắn giữa màn hình splash và onboarding trong khi view bên dưới đang được khởi tạo. Bạn có thể xử lý điều này theo nhiều cách khác nhau tùy theo nhu cầu. #### Kiểm soát màn hình splash bằng onDidFinishLoading \{#control-splash-screen-using-ondidfinishloading\} :::note Cách này chỉ khả dụng khi nhúng onboarding dưới dạng widget. Không áp dụng cho việc trình bày dưới dạng màn hình độc lập. ::: Cách được khuyến nghị cho đa nền tảng là giữ màn hình splash hoặc lớp phủ tùy chỉnh hiển thị cho đến khi onboarding tải xong, rồi ẩn nó đi theo cách thủ công. Khi dùng embedded widget, đặt widget của bạn phủ lên trên và ẩn lớp phủ đó khi `onDidFinishLoading` được kích hoạt: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Hide your custom splash screen or overlay here }, // ... other callbacks ) ``` ### Tùy chỉnh loader mặc định \{#customize-native-loader\} :::important Cách này phụ thuộc vào từng nền tảng và yêu cầu duy trì code UI native. Không được khuyến nghị trừ khi bạn đã duy trì các native layer riêng biệt trong ứng dụng của mình. ::: Nếu bạn cần tùy chỉnh loader mặc định, bạn có thể thay thế nó bằng các layout theo từng nền tảng. Cách này yêu cầu triển khai riêng cho Android và iOS: - **iOS**: Thêm `AdaptyOnboardingPlaceholderView.xib` vào dự án Xcode của bạn - **Android**: Tạo `adapty_onboarding_placeholder_view.xml` trong `res/layout` và định nghĩa một placeholder ở đó ## Tùy chỉnh cách mở liên kết trong onboarding \{#customize-how-links-open-in-onboardings\} :::important Tính năng tùy chỉnh cách mở liên kết trong onboarding được hỗ trợ từ Adapty SDK v3.15.1 trở lên. ::: Theo mặc định, các liên kết trong onboarding sẽ mở trong trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị trang web ngay trong ứng dụng, không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết trong trình duyệt ngoài, có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `AdaptyWebPresentation.externalBrowser`: <Tabs> <TabItem value="standalone" label="Màn hình độc lập" default> ```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 } ``` </TabItem> <TabItem value="embedded" label="Widget nhúng"> ```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) { }, ) ``` </TabItem> </Tabs> ## Tắt safe area paddings (Android) \{#disable-safe-area-paddings-android\} Theo mặc định, trên các thiết bị Android, màn hình onboarding sẽ tự động áp dụng safe area paddings để tránh các thành phần giao diện hệ thống như thanh trạng thái và thanh điều hướng. Tuy nhiên, nếu bạn muốn tắt hành vi này và kiểm soát hoàn toàn bố cục, bạn có thể thực hiện bằng cách thêm một boolean resource vào ứng dụng: 1. Truy cập vào `android/app/src/main/res/values`. Nếu chưa có file `bools.xml`, hãy tạo mới. 2. Thêm resource sau: ```xml <resources> <bool name="adapty_onboarding_enable_safe_area_paddings">false</bool> </resources> ``` Lưu ý rằng các thay đổi này áp dụng toàn cục cho tất cả các onboarding trong ứng dụng của bạn. --- # File: flutter-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong Flutter SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong Flutter bằng Adapty." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong các phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: khác với onboardings chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-present-paywalls) để bắt đầu. ::: Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể phản hồi. Cách xử lý các sự kiện này phụ thuộc vào phương thức hiển thị bạn đang dùng: - **Hiển thị toàn màn hình**: Yêu cầu thiết lập một observer sự kiện toàn cục để xử lý các sự kiện cho tất cả các onboarding view - **Widget nhúng**: Xử lý sự kiện thông qua các tham số callback trực tiếp trong widget Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Flutter SDK](sdk-installation-flutter) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Sự kiện hiển thị toàn màn hình \{#full-screen-presentation-events\} ### Thiết lập event observer \{#set-up-event-observer\} Để xử lý sự kiện cho các onboarding toàn màn hình, hãy triển khai `AdaptyUIOnboardingsEventsObserver` và thiết lập nó trước khi hiển thị: ```dart showLineNumbers title="Flutter" AdaptyUI().setOnboardingsEventsObserver(this); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Xử lý sự kiện \{#handle-events\} Triển khai các phương thức sau trong observer của bạn: ```dart showLineNumbers title="Flutter" void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { // Onboarding finished loading } void onboardingViewDidFailWithError( AdaptyUIOnboardingView view, AdaptyError error, ) { // Handle loading errors } void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle close action view.dismiss(); } void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle custom actions } void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle user input updates } void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { // Track analytics events } ``` ## Sự kiện của widget nhúng \{#embedded-widget-events\} Khi sử dụng `AdaptyUIOnboardingPlatformView`, bạn có thể xử lý các sự kiện thông qua các tham số callback nội tuyến trực tiếp trong widget. Lưu ý rằng các sự kiện sẽ được gửi đến cả callback của widget lẫn observer toàn cục (nếu đã được thiết lập), nhưng observer toàn cục là tùy chọn: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Onboarding finished loading }, onDidFailWithError: (error) { // Handle loading errors }, onCloseAction: (meta, actionId) { // Handle close action }, onPaywallAction: (meta, actionId) { _openPaywall(actionId); }, onCustomAction: (meta, actionId) { // Handle custom actions }, onStateUpdatedAction: (meta, elementId, params) { // Handle user input updates }, onAnalyticsEvent: (meta, event) { // Track analytics events }, ) ``` ## Các loại sự kiện \{#event-types\} Các phần sau mô tả các loại sự kiện khác nhau mà bạn có thể xử lý, bất kể phương thức hiển thị nào bạn đang sử dụng. ### Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Trong builder, bạn có thể thêm hành động **custom** vào một nút và gán cho nó một ID. <img src="/assets/shared/img/ios-events-1.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một custom action. Ví dụ: nếu người dùng nhấn vào một nút tùy chỉnh, chẳng hạn như **Login** hoặc **Allow notifications**, delegate method `onboardingController` sẽ được kích hoạt với case `.custom(id:)` và tham số `actionId` chính là **Action ID** từ builder. Bạn có thể tự tạo ID theo ý muốn, chẳng hạn như "allowNotifications". ```dart // Full-screen presentation void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { switch (actionId) { case 'login': _login(); break; case 'allow_notifications': _allowNotifications(); break; } } // Embedded widget onCustomAction: (meta, actionId) { _handleCustomAction(actionId); } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ``` </Details> ### Hoàn tất tải onboarding \{#finishing-loading-onboarding\} Khi một onboarding hoàn tất tải, sự kiện này sẽ được kích hoạt: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { print('Onboarding loaded: ${meta.onboardingId}'); } // Embedded widget onDidFinishLoading: (meta) { print('Onboarding loaded: ${meta.onboardingId}'); } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ``` </Details> ### Đóng onboarding \{#closing-onboarding\} Onboarding được coi là đã đóng khi người dùng nhấn vào một nút có hành động **Close** được gán. <img src="/assets/shared/img/ios-events-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important Lưu ý rằng bạn cần tự xử lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ, bạn cần dừng hiển thị chính onboarding đó. ::: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { await view.dismiss(); } // Embedded widget onCloseAction: (meta, actionId) { Navigator.of(context).pop(); } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ``` </Details> ### Mở một paywall \{#opening-a-paywall\} :::tip Xử lý sự kiện này để mở một paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi nó được đóng lại, có một cách đơn giản hơn — xử lý hành động đóng và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall: Lưu ý rằng, trên iOS, chỉ có thể hiển thị một màn hình (paywall hoặc onboarding) tại một thời điểm. Nếu bạn hiển thị một paywall chồng lên một onboarding, bạn không thể điều khiển onboarding ở nền theo cách lập trình. Việc cố gắng đóng onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn đóng màn hình onboarding trước khi hiển thị paywall. ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } Future<void> _openPaywall(String actionId) async { // Implement your paywall opening logic here } // Embedded widget onPaywallAction: (meta, actionId) { _openPaywall(actionId); } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ``` </Details> ### Theo dõi điều hướng \{#tracking-navigation\} Bạn nhận được một sự kiện analytics khi các sự kiện liên quan đến điều hướng xảy ra trong flow onboarding: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { trackEvent(event.type, meta.onboardingId); } // Embedded widget onAnalyticsEvent: (meta, event) { trackEvent(event.type, meta.onboardingId); } ``` Đối tượng `event` có thể là một trong các kiểu sau: | Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình được hoàn thành. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn thành) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `secondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy [gán ID `final` cho màn hình cuối cùng](design-onboarding). | | `unknown` | Cho bất kỳ loại sự kiện không được nhận dạng nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (siêu dữ liệu bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của flow onboarding | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```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 } } ``` </Details> --- # File: flutter-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong Flutter SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng Flutter của bạn với Adapty SDK." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: không giống như onboardings chạy trong WebView, flows render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời câu hỏi trong quiz hoặc nhập dữ liệu vào trường input, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Ví dụ: ```dart // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Process data } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Process data } ``` Xem định dạng action [tại đây](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyUIOnboardingPlatformView/onStateUpdatedAction.html). <Details> <summary>Hình dạng thuộc tính cho từng kiểu params (nhấn để mở rộng)</summary> ```dart void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // elementId là một String: elementId; // 'preference_selector' // meta — AdaptyUIOnboardingMeta: meta.onboardingId; // 'onboarding_123' meta.screenClientId; // 'preferences_screen' meta.screenIndex; // 1 meta.screensTotal; // 3 // params là một trong các lớp con của AdaptyOnboardingsStateUpdatedParams: switch (params) { case AdaptyOnboardingsSelectParams(:final id, :final value, :final label): // một tùy chọn được chọn id; // 'option_1' value; // 'premium' label; // 'Premium Plan' break; case AdaptyOnboardingsMultiSelectParams(:final params): // danh sách các tùy chọn được chọn, mỗi tùy chọn là AdaptyOnboardingsSelectParams params; // [(id: 'interest_1', value: 'sports', label: 'Sports'), (id: 'interest_2', value: 'music', label: 'Music')] break; case AdaptyOnboardingsInputParams(:final input): switch (input) { case AdaptyOnboardingsTextInput(:final value): value; // 'John Doe' break; case AdaptyOnboardingsEmailInput(:final value): value; // 'user@example.com' break; case AdaptyOnboardingsNumberInput(:final value): value; // 25.0 (a double) break; } break; case AdaptyOnboardingsDatePickerParams(:final day, :final month, :final year): day; // 15 month; // 6 year; // 1990 break; } } ``` </Details> ## Các trường hợp sử dụng \{#use-cases\} ### Làm phong phú hồ sơ người dùng với dữ liệu \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ hai lần về cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](flutter-setting-user-attributes) với dữ liệu đầu vào khi xử lý action. Ví dụ: bạn yêu cầu người dùng nhập tên của họ vào trường văn bản có ID `name`, và bạn muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn cũng yêu cầu họ nhập email vào trường `email`. Trong code của ứng dụng, nó có thể trông như thế này: ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Sử dụng quiz trong onboarding, bạn có thể tùy chỉnh paywall hiển thị cho người dùng sau khi họ hoàn thành onboarding. Ví dụ, bạn có thể hỏi người dùng về kinh nghiệm tập thể thao của họ và hiển thị các CTA và sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) trong trình tạo onboarding và gán ID có ý nghĩa cho các lựa chọn của nó. 2. Xử lý các phản hồi quiz dựa trên ID của chúng và [thiết lập thuộc tính tùy chỉnh](flutter-setting-user-attributes) cho người dùng. ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho từng phân khúc bạn đã tạo. 5. [Hiển thị paywall](flutter-paywalls) cho placement trong code của ứng dụng. Nếu onboarding của bạn có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho hành động của nút này](flutter-handling-onboarding-events#opening-a-paywall). --- # File: flutter-sdk-call-order --- --- title: "Thứ tự gọi trong Flutter SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution, và lỗi #2002 không liên tục bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự." --- `Adapty().activate()` phải hoàn thành trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi nó hoàn tất, SDK không có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` sẽ thất bại với lỗi [`#2002 notActivated`](error-handling-on-flutter-react-native-unity#custom-network-codes). Nếu ứng dụng của bạn xác thực người dùng và bạn thu thập customer user ID sau khi khởi chạy, hãy gọi `Adapty().identify()` tại thời điểm đó. Đừng gọi các phương thức hành động người dùng cho đến khi `identify` hoàn tất. Các lệnh gọi chạy song song với nó có thể thất bại với [`#3006 profileWasChanged`](error-handling-on-flutter-react-native-unity#custom-network-codes), hoặc sẽ tác động lên hồ sơ người dùng ẩn danh được tạo lúc kích hoạt. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu lượt cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. SDK của MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo quy tắc tương tự. Hãy khởi tạo chúng trước và chờ callback UID của chúng trước khi gọi `Adapty().activate`. Nếu không, MMP ID sẽ gắn vào hồ sơ người dùng ẩn danh tạm thời và không phải lúc nào cũng được chuyển sang hồ sơ đã xác định. Để biết thêm chi tiết về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai điều: thời điểm bạn biết được customer user ID, và việc bạn có sử dụng MMP hoặc analytics SDK hay không. - **Bước 2 và 5**: Bắt buộc với mọi ứng dụng. Kích hoạt SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ cần thiết nếu bạn tích hợp MMP hoặc analytics SDK (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ cần thiết nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi động. Nếu bạn đã có customer user ID ngay khi ứng dụng khởi động, hãy truyền trực tiếp vào `activate()` (bước 2a). Cách này không bao giờ tạo hồ sơ người dùng ẩn danh, nên bước 4 là không cần thiết. | Bước | Lệnh gọi | Thời điểm | Ghi chú | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo MMP hoặc analytics SDK của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi động ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty().activate(configuration: ...)` với `withCustomerUserId` được thiết lập trong cấu hình | Khởi động ứng dụng, sau bước 1, nếu bạn đã có customer user ID | Khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty().activate(configuration: ...)` không có `withCustomerUserId` | Khởi động ứng dụng, sau bước 1, nếu bạn chưa có customer user ID (hoặc không thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. | | 3 | `Adapty().setIntegrationIdentifier(key: ..., value: ...)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi nào do người dùng thực hiện | Bắt buộc để các ID của MMP gắn đúng vào hồ sơ người dùng. | | 4 | `await Adapty().identify(customerUserId)` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên luồng 2b có xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong quá trình `identify` sẽ trả về `#3006 profileWasChanged`. | | 5 | `getPaywall` (`getFlow` trong SDK v4), `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này sẽ dẫn đến mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về cho sai đối tượng. ::: ## Cài đặt web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng qua web checkout (Stripe, Paddle) và sau đó cài đặt ứng dụng native, lần `activate()` đầu tiên trên thiết bị sẽ tạo một hồ sơ ẩn danh mới. Hồ sơ này không được liên kết với hồ sơ web. Nếu bạn có thể xác định customer user ID trước khi khởi chạy ứng dụng (từ luồng xác thực hoặc install referrer), hãy truyền trực tiếp vào `activate()`. Nếu không, giao dịch mua trên web sẽ không hiển thị trên thiết bị cho đến khi bạn gọi `identify("YOUR_USER_ID")` và sau đó gọi `restorePurchases`. Về metadata cần gửi kèm mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: flutter-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong Flutter SDK" description: "Tải paywall Adapty một cách đáng tin cậy: thời điểm, bộ nhớ đệm và các pattern dự phòng cho Flutter." --- Một lần tải paywall đáng tin cậy trên Flutter cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng người dùng, và xử lý dự phòng khi mạng chậm. Các quy tắc dưới đây đề cập đến thời điểm, bộ nhớ đệm và các pattern dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định rằng `Adapty().activate()` và `Adapty().identify()` đã được giải quyết xong. Xem [Thứ tự gọi trong Flutter SDK](flutter-sdk-call-order). ::: Các hướng dẫn dưới đây sử dụng tên phương thức v3. Trong SDK v4, `getPaywall` được đổi tên thành `getFlow` và kiểu fetch policy thành `AdaptyFlowFetchPolicy` — mọi quy tắc đều áp dụng không thay đổi. ## Quy tắc và lưu ý \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Fetch placement bạn sắp hiển thị. | Prefetch tất cả placement đồng thời khi khởi động. | Bulk prefetch chặn main thread và gây màn hình đen trong thời gian burst. | | Fetch `getPaywall` sau khi attribution đã có thời gian xử lý — ví dụ, 1–2 giây sau `activate` hoặc sau khi `didUpdateProfileStream` kích hoạt. | Gọi `getPaywall` trong `main()` trước `runApp`. | Attribution chưa được xử lý xong. Paywall sẽ được resolve theo đối tượng mặc định và âm thầm bỏ qua các phân khúc cũng như ASA personalization. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` vô thời hạn. | Không có timeout, người dùng có kết nối kém sẽ thấy màn hình trắng cho đến khi mạng phản hồi — hoặc họ sẽ đóng ứng dụng. | Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-flutter) để tham khảo thông số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn placement phù hợp. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối liên tục kém (khu vực nông thôn, phương tiện công cộng, vùng bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy: AdaptyPaywallFetchPolicy.returnCacheDataElseLoad` cho mọi lần tải, ngoại trừ lần đầu tiên. - Cấu hình [paywall dự phòng](fallback-paywalls) cho mọi placement trong Adapty Dashboard. - Đặt `loadTimeout` từ 3–5 giây và chấp nhận paywall dự phòng khi timeout xảy ra. - Không chặn việc hiển thị paywall bởi `getProfile()`. Gọi `getPaywall` độc lập để một profile phản hồi chậm không làm tắc nghẽn giao diện. --- # File: flutter-show-aa-targeted-paywall --- --- title: "Hiển thị paywall theo mục tiêu AA khi khởi chạy lần đầu trong Flutter SDK" description: "Chờ một chút để nhận attribution từ Apple Ads trước khi hiển thị paywall khi khởi chạy lần đầu trong Flutter, tự động chuyển về đối tượng mặc định khi hết thời gian chờ. Sử dụng AdaptyProfile.appliedAttributionSources." --- Attribution từ Apple Ads (AA) được nhận bất đồng bộ sau khi gọi `Adapty().activate()`. Ở lần khởi chạy đầu tiên, dữ liệu này thường chưa có, vì vậy nếu bạn gọi `getPaywall` ngay lập tức, Adapty sẽ xử lý yêu cầu dựa trên đối tượng mặc định và những người dùng Apple Ads sẽ không thấy paywall được nhắm mục tiêu theo AA. Thay vì hiển thị một paywall rồi thay thế nó, hãy chờ một chút để nhận attribution từ AA trước khi hiển thị bất cứ thứ gì: hiển thị paywall đúng đối tượng nếu attribution đến trong thời gian chờ ngắn, hoặc hiển thị paywall theo đối tượng mặc định nếu không. `AdaptyProfile.appliedAttributionSources` cho bạn biết khi nào attribution từ AA đã được áp dụng. ## Trước khi bắt đầu \{#before-you-start\} Bạn cần: - Adapty Flutter SDK **3.17.0** trở lên. - Apple Ads đã được cấu hình cho ứng dụng trong Adapty. Xem [Apple Ads](apple-search-ads). ## Cách hoạt động \{#how-it-works\} Sau khi gọi `Adapty().activate()`, SDK sẽ yêu cầu attribution từ Apple Ads ở chế độ nền và chuyển kết quả đó về backend của Adapty. Khi AA trở thành nguồn attribution đang hoạt động cho hồ sơ người dùng, SDK sẽ gửi một `AdaptyProfile` đã được cập nhật đến listener `didUpdateProfileStream` của bạn, với `AdaptyAttributionSource.appleAds` trong danh sách `appliedAttributionSources`. Ở lần chạy đầu tiên, bạn sẽ cần xử lý hai trường hợp: 1. **Attribution đến trong thời gian chờ của bạn.** Gọi `getPaywall` — Adapty xử lý yêu cầu dựa trên đối tượng Apple Ads và trả về paywall được nhắm mục tiêu. 2. **Thời gian chờ hết trước.** Hiển thị paywall cho đối tượng mặc định thay thế, để người dùng không có attribution Apple Ads không phải chờ đợi. `getPaywallForDefaultAudience` trả về paywall đó mà không cần chờ phân khúc. `appliedAttributionSources` có thể rỗng. Điều đó có nghĩa là: - Attribution Apple Ads chưa được xử lý cho hồ sơ người dùng này, hoặc - không có attribution nào đến cả. Dù thế nào, `getPaywallForDefaultAudience` vẫn an toàn để gọi — nó trả về paywall cho đối tượng mặc định bất kể trạng thái hồ sơ người dùng. :::important Việc chờ chỉ áp dụng cho lần khởi chạy đầu tiên. Sau khi attribution Apple Ads đã được ghi lại, nó sẽ được lưu trữ vĩnh viễn trên hồ sơ người dùng. Ở mọi lần khởi chạy tiếp theo, hồ sơ người dùng đã được cache sẵn `AdaptyAttributionSource.appleAds` trong `appliedAttributionSources`, nên đường dẫn attribution được xử lý ngay lập tức và `getPaywall` trả về paywall được phân khúc theo Apple Ads mà không có bất kỳ độ trễ nào. ::: ## Triển khai \{#implementation\} Khi khởi chạy lần đầu, hãy chờ `AdaptyAttributionSource.appleAds` và áp dụng thời gian chờ tối đa — nếu attribution từ Apple Ads không bao giờ đến, những người dùng đó vẫn cần được xem paywall. 1. **Kích hoạt SDK.** Xem [Cài đặt & cấu hình Flutter SDK](sdk-installation-flutter). 2. **Đăng ký nhận cập nhật hồ sơ người dùng** với `Adapty().didUpdateProfileStream.listen(…)`. Nếu bạn chưa thiết lập listener, xem [Lắng nghe cập nhật gói đăng ký](flutter-check-subscription-status#listen-to-subscription-updates). 3. **Theo dõi `AdaptyAttributionSource.appleAds` trong `appliedAttributionSources`.** Khi nó xuất hiện, tải paywall bằng `getPaywall` — Adapty sẽ trả về biến thể được phân khúc theo AA: ```dart final subscription = Adapty().didUpdateProfileStream.listen((profile) async { if (!profile.appliedAttributionSources.contains(AdaptyAttributionSource.appleAds)) return; final paywall = await Adapty().getPaywall(placementId: placementId); // present the segmented paywall, then cancel the subscription and the timer }); ``` `didUpdateProfileStream` là một broadcast stream và không replay, vì vậy hãy kiểm tra hồ sơ người dùng hiện tại một lần bằng `getProfile()`. Khi khởi động lại ứng dụng, attribution đã được lưu trữ sẽ không phát lại sự kiện. 4. **Bắt đầu đồng hồ đếm ngược 3–5 giây song song với gói đăng ký.** Nếu đồng hồ kích hoạt trước khi `AdaptyAttributionSource.appleAds` xuất hiện, hãy tải paywall cho đối tượng mặc định bằng `getPaywallForDefaultAudience`. Hiển thị paywall nào được trả về trước và hủy luồng còn lại để tránh tải paywall hai lần. Hãy cấu hình [paywall dự phòng](flutter-use-fallback-paywalls) cho placement đó để người dùng không bị kẹt nếu yêu cầu mạng thất bại. ## Ví dụ đầy đủ \{#complete-example\} Cách triển khai dưới đây chạy đua giữa attribution và một khoảng timeout, tải trước paywall cho đối tượng mặc định song song, và trả về paywall phù hợp. Phía gọi hàm chỉ cần `await` một hàm duy nhất — không cần listener hay cờ trạng thái nào ở call site: - Nếu attribution về kịp trong `timeout`, hàm trả về paywall theo phân khúc thông qua `getPaywall`. - Nếu `timeout` hết trước, hàm trả về paywall đối tượng mặc định đã tải trước thông qua `getPaywallForDefaultAudience`. ```dart title="apple_ads_paywall.dart" /// Returns the Apple Ads-segmented paywall if attribution is applied within /// [timeout], otherwise the default-audience paywall. Call after Adapty().activate(). Future<AdaptyPaywall> 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<AdaptyPaywall>(); late final StreamSubscription<AdaptyProfile> subscription; late final Timer timer; void resolve(Future<AdaptyPaywall> paywall) { if (completer.isCompleted) return; timer.cancel(); subscription.cancel(); completer.complete(paywall); } void onProfile(AdaptyProfile profile) { if (profile.appliedAttributionSources.contains(AdaptyAttributionSource.appleAds)) { resolve(Adapty().getPaywall(placementId: placementId)); } } // Attribution path: react to profile updates as attribution is applied. subscription = Adapty().didUpdateProfileStream.listen(onProfile); // The stream is a broadcast stream and doesn't replay, so check the current // profile too — on relaunches attribution is already stored and won't re-emit. Adapty().getProfile().then(onProfile).ignore(); // Timeout path: fall back to the prefetched default-audience paywall. timer = Timer(timeout, () => resolve(defaultPaywall)); return completer.future; } ``` Gọi từ màn hình splash của bạn, sau đó hiển thị paywall khi promise được giải quyết: ```dart try { final paywall = await getPaywallOrDefault( placementId: 'YOUR_PLACEMENT_ID', timeout: const Duration(seconds: 5), ); // present the paywall } on AdaptyError catch (adaptyError) { // handle the error or show a fallback paywall } catch (e) { // handle the error } ``` Điều chỉnh `timeout` theo thời gian bạn muốn người dùng chờ trước khi paywall xuất hiện. Hầu hết người dùng không có attribution từ Apple Ads, nên họ sẽ chờ hết toàn bộ thời gian timeout — 3 đến 5 giây là mức cân bằng hợp lý. Attribution nếu có thường đến trong vài giây sau khi khởi động ứng dụng. Nếu ứng dụng của bạn đã lắng nghe `didUpdateProfileStream` cho các mục đích khác (ví dụ: [kiểm tra trạng thái gói đăng ký](flutter-check-subscription-status#listen-to-subscription-updates)), bạn không cần thay đổi gì. `didUpdateProfileStream` là một broadcast stream, nên nó hỗ trợ nhiều listener độc lập mà không ảnh hưởng đến nhau. --- # File: flutter-test --- --- title: "Test & release in Flutter SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Flutter của bạn với Adapty." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng Flutter của mình, bước tiếp theo là kiểm tra xem mọi thứ đã được thiết lập đúng chưa và các giao dịch mua có hoạt động như mong đợi trên cả hai nền tảng iOS và Android không. Quá trình này bao gồm việc kiểm tra cả tích hợp SDK lẫn luồng mua hàng thực tế với môi trường sandbox của Apple và môi trường thử nghiệm của Google Play. ## Kiểm thử ứng dụng \{#test-your-app\} Để kiểm thử in-app purchase một cách toàn diện, hãy xem các hướng dẫn kiểm thử theo nền tảng: [Hướng dẫn kiểm thử iOS](test-purchases-in-sandbox) và [Hướng dẫn kiểm thử Android](testing-on-android). ## Chuẩn bị phát hành \{#prepare-for-release\} Trước khi nộp ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối cửa hàng và thông báo từ máy chủ đã được cấu hình - Giao dịch mua hoàn tất và được báo cáo về Adapty - Mức độ truy cập được mở khóa và khôi phục đúng cách - Các yêu cầu về quyền riêng tư và xét duyệt đã được đáp ứng --- # File: InvalidProductIdentifiers-flutter --- --- title: "Sửa lỗi Code-1000 noProductIDsFound trong Flutter SDK" description: "Khắc phục lỗi mã sản phẩm không hợp lệ khi quản lý gói đăng ký trong Adapty." --- Lỗi mã 1000, `noProductIDsFound`, cho biết không có sản phẩm nào bạn yêu cầu trên paywall có thể mua được trong App Store, mặc dù chúng đã được đăng ký ở đó. Đôi khi lỗi này đi kèm với cảnh báo `InvalidProductIdentifiers`. Nếu cảnh báo xuất hiện mà không có lỗi, bạn có thể bỏ qua an toàn. Nếu bạn gặp lỗi `noProductIDsFound`, hãy làm theo các bước sau để khắc phục: ## Bước 1. Kiểm tra bundle ID \{#step-2-check-bundle-id\} 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <Zoom> <img src="/docs/img/afd5012-bundle_id_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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**. <Zoom> <img src="/docs/img/2d64163-bundle_id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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**. <img src="/assets/shared/img/ready-to-submit.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/product-id-copy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## 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ự. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/product-availability.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## 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**. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống phần **Subscription Pricing** và mở rộng mục **Current Pricing for New Subscribers**. <img src="/assets/shared/img/check-prices.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Đảm bảo tất cả các mức giá cần thiết đã được liệt kê. <img src="/assets/shared/img/product-pricing.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## 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**. <img src="/assets/shared/img/business.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Chọn tên công ty của bạn. <img src="/assets/shared/img/business-name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/appstore-connect-status.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Làm theo các bước trên, bạn sẽ có thể khắc phục cảnh báo `InvalidProductIdentifiers` và đưa sản phẩm của mình lên cửa hàng. ## Bước 6. Tạo lại sản phẩm nếu bị kẹt \{#step-6-recreate-the-product-if-its-stuck\} Các bước 1–5 có thể đều vượt qua — trạng thái `Approved`, Bundle ID khớp, API key hợp lệ — nhưng SDK vẫn trả về `1000 noProductIDsFound`. Trong trường hợp đó, sản phẩm có thể bị kẹt trong registry của Apple. Registry sản phẩm của Apple đôi khi rơi vào trạng thái mà sản phẩm tồn tại trong giao diện App Store Connect nhưng không xuất hiện trên đường dẫn tra cứu StoreKit. Hãy xóa sản phẩm trong App Store Connect và tạo lại với cùng ID sản phẩm. Chờ tối đa 24 giờ sau khi tạo lại để các thay đổi được lan truyền. --- # File: cantMakePayments-flutter --- --- title: "Sửa lỗi Code-1003 cantMakePayment trong Flutter SDK" description: "Giải quyết lỗi thanh toán khi quản lý gói đăng ký trong Adapty." --- Lỗi 1003, `cantMakePayments`, cho biết thiết bị không thể thực hiện in-app purchase. Nếu bạn gặp lỗi `cantMakePayments`, thường là do một trong các nguyên nhân sau: - Giới hạn thiết bị: Lỗi này không liên quan đến Adapty. Xem cách khắc phục bên dưới. - Cấu hình Observer mode: Không thể dùng đồng thời phương thức `makePurchase` và Observer mode. Xem phần bên dưới. ## Sự cố: Giới hạn thiết bị \{#issue-device-restrictions\} | Sự cố | Giải pháp | |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------| | Giới hạn Screen Time | Tắt giới hạn In-App Purchase trong [Screen Time](https://support.apple.com/en-us/102470) | | Tài khoản bị tạm khóa | Liên hệ Apple Support để giải quyết vấn đề tài khoản | | Giới hạn khu vực | Sử dụng tài khoản App Store từ vùng được hỗ trợ | ## Sự cố: Dùng đồng thời Observer mode và makePurchase \{#issue-using-both-observer-mode-and-makepurchase\} Nếu bạn đang dùng `makePurchases` để xử lý giao dịch mua, bạn không cần dùng Observer mode. [Observer mode](observer-vs-full-mode) chỉ cần thiết khi bạn tự triển khai logic mua hàng. Vì vậy, nếu bạn đang dùng `makePurchase`, bạn có thể xóa phần kích hoạt Observer mode khỏi code khởi tạo SDK một cách an toàn. --- # File: migration-to-flutter-sdk-v4 --- --- title: "Migrate Adapty Flutter SDK sang phiên bản 4.0" description: "Migrate lên Adapty Flutter SDK v4.0 bằng cách thay thế các API paywall bằng API flow, tương thích với cả Flow Builder và Paywall Builder." --- Adapty Flutter SDK 4.0 giới thiệu flow và đổi tên các API paywall tương ứng. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi cấu hình nào trên Adapty Dashboard. ## Tham khảo nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `Adapty().getPaywall(placementId: id)` | `Adapty().getFlow(placementId: id)` | | `Adapty().getPaywallForDefaultAudience(placementId: id)` | `Adapty().getFlowForDefaultAudience(placementId: id)` | | `Adapty().getPaywallProducts(paywall: paywall)` | `Adapty().getPaywallProducts(flow: flow)` | | `Adapty().logShowPaywall(paywall: paywall)` | `Adapty().logShowFlow(flow: flow)` | | `AdaptyPaywall` (kiểu) | `AdaptyFlow` | | `AdaptyPaywallFetchPolicy` (kiểu) | `AdaptyFlowFetchPolicy` | | `AdaptyUI().createPaywallView(paywall: paywall)` | `AdaptyUI().createFlowView(flow: flow)` | | `AdaptyUIPaywallView` (kiểu) | `AdaptyUIFlowView` | | `AdaptyUIPaywallPlatformView` (widget) | `AdaptyUIFlowPlatformView` | | `AdaptyUI().presentPaywallView(view)` / `dismissPaywallView(view)` | `AdaptyUI().presentFlowView(view)` / `dismissFlowView(view)` | | `AdaptyUIPaywallsEventsObserver` | `AdaptyUIFlowsEventsObserver` | | `AdaptyUI().setPaywallsEventsObserver(observer)` | `AdaptyUI().setFlowsEventsObserver(observer)` | | callback `paywallViewDid*` | callback `flowViewDid*` | | `paywallViewDidFailRendering` | `flowViewDidReceiveError` | `AdaptyPaywallProduct` vẫn giữ nguyên tên — các sản phẩm vẫn thuộc về một flow, và `getPaywallProducts` giờ nhận vào một `AdaptyFlow`. Bạn không còn cần truyền `locale` khi lấy một flow nữa. Các API mua hàng và hồ sơ người dùng (`makePurchase`, `restorePurchases`, `getProfile`, `identify`, v.v.) không thay đổi, tương tự với các phương thức giao diện `present`, `dismiss` và `showDialog`. Một số hành vi mặc định đã thay đổi — xem [Thay đổi hành vi mặc định](#default-behavior-changes). ## Phiên bản tối thiểu \{#minimum-versions\} Adapty Flutter SDK 4.0 nâng cao yêu cầu tối thiểu: - **iOS 15.0** — deployment target iOS tối thiểu, tăng từ iOS 13.0. - **Xcode 26** trở lên — iOS SDK native sử dụng Swift tools 6.2. - **Flutter 3.32.0** (Dart 3.8.0) trở lên. ## Cài đặt \{#installation\} ### Cập nhật package \{#update-the-package\} Package bạn cài đặt phụ thuộc vào việc ứng dụng có sử dụng Kids Mode hay không. Với hầu hết các ứng dụng, hãy cập nhật `adapty_flutter` lên v4.0 trong `pubspec.yaml`: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: 4.0.0 ``` Nếu ứng dụng sử dụng Kids Mode, hãy chỉ định `adapty_flutter_kids` thay thế: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter_kids: 4.0.0 ``` Gói **độc lập** này loại bỏ mã IDFA và theo dõi quảng cáo để tuân thủ các yêu cầu của App Store. Cập nhật đường dẫn import Dart thành `package:adapty_flutter_kids/adapty_flutter.dart`. Ngoài ra, quá trình migration hoàn toàn giống với gói thông thường. Kids Mode cũng yêu cầu bạn tắt tính năng thu thập địa chỉ IP trong Adapty Dashboard — xem [Kids Mode](kids-mode-flutter) để biết hướng dẫn cài đặt đầy đủ. ### iOS: SDK iOS native giờ được phân phối qua Swift Package Manager \{#ios-native-sdks-now-come-through-swift-package-manager\} [Repo spec của CocoaPods sẽ chuyển sang chế độ chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/), vì vậy bắt đầu từ v4, SDK iOS native **không còn được phân phối qua CocoaPods** nữa — plugin sẽ kéo nó qua **Swift Package Manager** mà thôi. Nếu bạn đang dùng Flutter 3.32–3.43, hãy bật hỗ trợ Swift Package Manager một lần: ```bash flutter config --enable-swift-package-manager ``` Flutter 3.44 trở lên đã bật Swift Package Manager theo mặc định, nên bạn không cần làm gì thêm. ## Lấy flows \{#fetching-flows\} ### getPaywall → getFlow Kiểu trả về thay đổi từ `AdaptyPaywall` sang `AdaptyFlow`, và bạn không cần truyền `locale` nữa — khi render một flow, localization sẽ được tự động xử lý; với các paywall tùy chỉnh, tất cả locale đã cấu hình sẽ được trả về trong `flow.remoteConfigs`: ```diff showLineNumbers - final paywall = await Adapty().getPaywall(placementId: 'YOUR_PLACEMENT_ID', locale: 'en'); + final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); ``` `getPaywallForDefaultAudience` cũng được đổi tên theo cách tương tự: ```diff showLineNumbers - final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID', locale: 'en'); + final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); ``` Kiểu fetch policy được đổi tên từ `AdaptyPaywallFetchPolicy` thành `AdaptyFlowFetchPolicy`; các tùy chọn của nó (`reloadRevalidatingCacheData`, `returnCacheDataElseLoad`, `returnCacheDataIfNotExpiredElseLoad`) không thay đổi. ### getPaywallProducts(paywall) → getPaywallProducts(flow) `getPaywallProducts` giữ nguyên tên nhưng giờ nhận một `AdaptyFlow` qua tham số `flow`: ```diff showLineNumbers - final products = await Adapty().getPaywallProducts(paywall: paywall); + final products = await Adapty().getPaywallProducts(flow: flow); ``` ## Mô hình dữ liệu \{#data-model\} `getFlow` trả về `AdaptyFlow` thay vì `AdaptyPaywall`, và cấu trúc đối tượng đã thay đổi: | Thành viên `AdaptyPaywall` v3 | Thành viên `AdaptyFlow` v4 | Hành động | |---|---|---| | `remoteConfig` (đơn, nullable) | `remoteConfigs` (danh sách) | Một flow chứa một remote config cho mỗi ngôn ngữ được cấu hình. Getter `remoteConfig` vẫn tồn tại và trả về mục đầu tiên; để chọn một ngôn ngữ cụ thể, hãy tìm trong `remoteConfigs` theo `locale` của nó. | | `productIdentifiers` | `productIdentifiers` | Được giữ lại, nhưng hiện được thu thập từ tất cả các biến thể paywall trong flow. Các identifier theo từng biến thể nằm ở `flow.paywalls[i].productIdentifiers`. | | `hasViewConfiguration` | `hasViewConfiguration` | Không thay đổi. | | `placementId` (deprecated) | đã xóa | Dùng `flow.placement.id`. | | `revision` (deprecated) | đã xóa | Dùng `flow.placement.revision`. | | `vendorProductIds` (deprecated) | đã xóa | Dùng `productIdentifiers`. | | _(mới)_ | `paywalls` (danh sách `AdaptyFlowPaywall`) | Mỗi mục là một biến thể paywall trong flow, với `name`, `variationId` và `productIdentifiers` riêng. | `AdaptyPaywallViewConfiguration` is no longer exposed — the view configuration is now opaque. Remove any references to this type. ## Phương thức paywall web \{#web-paywall-methods\} `openWebPaywall` và `createWebPaywallUrl` giữ nguyên tên, nhưng tham số `paywall` giờ nhận `AdaptyFlowPaywall` (một biến thể flow) thay vì `AdaptyPaywall`. Bạn vẫn có thể truyền `AdaptyPaywallProduct` như trước. ```diff showLineNumbers final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); - await Adapty().openWebPaywall(paywall: paywall); + if (flow.paywalls.isNotEmpty) { + await Adapty().openWebPaywall(paywall: flow.paywalls[0]); + } ``` ## Theo dõi lượt xem flow \{#tracking-flow-views\} ### logShowPaywall → logShowFlow `logShowPaywall` được đổi tên thành `logShowFlow` và nhận vào một `AdaptyFlow`. Sự kiện vẫn được ghi lại đối với cùng một biến thể, vì vậy các chỉ số funnel và A/B test hiện có vẫn hoạt động bình thường mà không cần thay đổi gì trên dashboard. ```diff showLineNumbers - await Adapty().logShowPaywall(paywall: paywall); + await Adapty().logShowFlow(flow: flow); ``` Giống như ở v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được dựng bởi [Flow Builder](adapty-flow-builder) hay [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi các lượt xem đó. ## Hiển thị flows \{#displaying-flows\} ### createPaywallView → createFlowView Đổi tên phương thức và truyền `AdaptyFlow` qua tham số `flow`. Các tham số khác (`loadTimeout`, `preloadProducts`, `customTags`, `customTimers`, `customAssets`, `productPurchaseParams`) không thay đổi, cũng như các phương thức của view là `present`, `dismiss`, và `showDialog`: ```diff showLineNumbers - final view = await AdaptyUI().createPaywallView(paywall: paywall); + final view = await AdaptyUI().createFlowView(flow: flow); await view.present(); ``` ### AdaptyUIPaywallView → AdaptyUIFlowView Kiểu view được đổi tên. Thuộc tính `paywallVariationId` đã bị xóa — hãy dùng `variationId` thay thế: ```diff showLineNumbers - void flowViewDidAppear(AdaptyUIPaywallView view) { + void flowViewDidAppear(AdaptyUIFlowView view) { ``` ### AdaptyUIPaywallPlatformView → AdaptyUIFlowPlatformView Nếu bạn nhúng view dưới dạng widget trong widget tree, hãy đổi tên nó và truyền tham số `flow`. Các callback sự kiện (`onDidAppear`, `onDidFinishPurchase`, v.v.) vẫn giữ nguyên tên: ```diff showLineNumbers - AdaptyUIPaywallPlatformView( - paywall: paywall, + AdaptyUIFlowPlatformView( + flow: flow, onDidFinishPurchase: (view, product, purchaseResult) { /* … */ }, ) ``` :::note Một flow view được tạo bằng `createFlowView` chỉ dùng được một lần: sau khi bạn gọi `dismiss()`, view đó sẽ được giải phóng khỏi bộ nhớ và không thể hiển thị lại — hãy gọi `createFlowView` một lần nữa nếu muốn hiển thị flow lại. ::: ## Xử lý sự kiện \{#handling-events\} Tên lớp observer được đổi từ `AdaptyUIPaywallsEventsObserver` thành `AdaptyUIFlowsEventsObserver`, phương thức đăng ký từ `setPaywallsEventsObserver` thành `setFlowsEventsObserver`, và tất cả các callback `paywallViewDid*` thành `flowViewDid*`: ```diff showLineNumbers - class MyObserver extends AdaptyUIPaywallsEventsObserver { + class MyObserver extends AdaptyUIFlowsEventsObserver { @override - void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { + void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { // … } } - AdaptyUI().setPaywallsEventsObserver(this); + AdaptyUI().setFlowsEventsObserver(this); ``` Ba callback sau đây là **bắt buộc** — observer của bạn sẽ không biên dịch được nếu thiếu chúng: - **`flowViewDidFinishPurchase`**: Trước đây là tùy chọn trong v3, mặc định sẽ dismiss view sau khi mua hàng. Giờ bạn tự quyết định: tiếp tục flow hay gọi `view.dismiss()`. - **`flowViewDidFinishRestore`**: Bắt buộc, giống như trong v3. - **`flowViewDidReceiveError`**: Thay thế `paywallViewDidFailRendering` và giờ cũng nhận các lỗi view khác. Hai thay đổi nhỏ hơn: - `setFlowsEventsObserver` (và `setOnboardingsEventsObserver`) giờ chấp nhận `null` để gỡ bỏ observer đã đặt trước đó, giúp SDK không còn giữ tham chiếu đến nó nữa. - Callback tùy chọn mới `flowViewDidReceiveAnalyticEvent` được dành riêng cho các sự kiện analytic tùy chỉnh từ một flow. Hiện tại các flow chưa phát ra sự kiện này đến code của bạn, nên bạn không cần implement nó. v4 cũng bổ sung các tính năng bạn có thể chọn dùng: - `AdaptyUI().setObserverModeResolver(...)` với một `AdaptyUIObserverModeResolver` — xử lý các giao dịch mua và khôi phục được khởi tạo từ flow khi SDK chạy ở [chế độ Observer](implement-observer-mode-flutter). Trước đây tính năng này chỉ có trong SDK iOS và Android gốc. Xem [Hiển thị flow trong chế độ Observer](flutter-present-flows-in-observer-mode). - `AdaptyUI().setSystemRequestsHandler(...)` với một `AdaptyUISystemRequestsHandler` — dành riêng cho các yêu cầu hệ thống từ flow (lời nhắc cấp quyền của hệ điều hành và yêu cầu đánh giá App Store). Hiện tại flow chưa kích hoạt các yêu cầu này, nên bạn không cần đăng ký handler. ## Các API đã bị xóa \{#removed-apis\} Các ký hiệu này đã bị deprecated trong 3.x và bị xóa trong v4: ### setFallbackPaywalls → setFallback ```diff showLineNumbers - await Adapty().setFallbackPaywalls(assetId); + await Adapty().setFallback(assetId); ``` ### withIdfaCollectionDisabled → withAppleIdfaCollectionDisabled ```diff showLineNumbers configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') - ..withIdfaCollectionDisabled(true), + ..withAppleIdfaCollectionDisabled(true), ``` ### Các thành phần đã bị xóa khác \{#other-removed-members\} - **`AdaptyPurchaseResultSuccess.jwsTransaction`**: Sử dụng `appleJwsTransaction`. - **`AdaptyUIFlowView.paywallVariationId`**: Sử dụng `variationId`. - **`AdaptyUIObserver` và `AdaptyUI().setObserver(...)`**: Sử dụng `AdaptyUIFlowsEventsObserver` và `setFlowsEventsObserver(...)`. ## Thay đổi hành vi mặc định \{#default-behavior-changes\} Những thay đổi này không gây ra lỗi biên dịch, vì vậy hãy kiểm tra chúng ở runtime: - **Mua hàng thành công**: Trong v3, `paywallViewDidFinishPurchase` mặc định sẽ đóng view. Trong v4, `flowViewDidFinishPurchase` là bắt buộc và không có hành vi mặc định — bạn cần tự đóng view nếu muốn. - **Nút back hệ thống Android**: Nút này không còn đóng flow theo mặc định nữa. Hành động sẽ được gửi đến `flowViewDidPerformAction` dưới dạng `AndroidSystemBackAction` — hãy xử lý ở đó nếu muốn nút back đóng flow. - **Mở URL**: `flowViewDidPerformAction` mặc định hiện xử lý `OpenUrlAction` bằng cách mở URL theo cách native (tôn trọng cài đặt trình duyệt in-app hoặc bên ngoài từ dashboard), ngoài việc đóng view khi nhận `CloseAction`. Ghi đè callback này nếu bạn muốn tự xử lý URL. - **Lỗi view**: `flowViewDidReceiveError` là bắt buộc, và việc đóng view tùy thuộc vào cách bạn triển khai. Nếu tích hợp v3 của bạn phụ thuộc vào việc view tự đóng khi có lỗi render, hãy gọi `view.dismiss()` trong callback này. - **Vòng đời view**: Đóng một flow hoặc onboarding view sẽ giải phóng nó khỏi bộ nhớ. View đã đóng không thể hiển thị lại — hãy tạo một view mới thay thế. ## Onboarding API đã bị deprecated \{#onboarding-api-deprecation\} Onboarding API cũ đã bị deprecated trong v4.0, thay thế bằng [Flow Builder](adapty-flow-builder). API này vẫn hoạt động bình thường, và IDE của bạn sẽ đánh dấu các ký hiệu deprecated thông qua annotation `@Deprecated` — không có cảnh báo nào ở runtime. Các ký hiệu này sẽ bị xóa trong bản phát hành tương lai, vì vậy hãy lên kế hoạch migrate các onboarding của bạn sang Flow Builder. Các ký hiệu không còn được hỗ trợ: `getOnboarding`, `getOnboardingForDefaultAudience`, `createOnboardingView`, `presentOnboardingView`, `dismissOnboardingView`, `setOnboardingsEventsObserver`, `AdaptyOnboarding`, `AdaptyUIOnboardingView`, `AdaptyUIOnboardingPlatformView`, `AdaptyUIOnboardingsEventsObserver`, và các model state, input, analytics của onboarding. --- # File: flutter-migration-guide-310 --- --- title: "Hướng dẫn migration lên Flutter Adapty SDK 3.10.0" description: "" --- Adapty SDK 3.10.0 là một bản phát hành lớn mang lại một số cải tiến, tuy nhiên có thể yêu cầu bạn thực hiện một số bước migration: 1. Cập nhật phương thức `makePurchase` để sử dụng `AdaptyPurchaseParameters` thay vì các tham số riêng lẻ. 2. Thay thế `vendorProductIds` bằng `productIdentifiers` trong model `AdaptyPaywall`. ## Cập nhật phương thức makePurchase \{#update-makepurchase-method\} Phương thức `makePurchase` hiện sử dụng `AdaptyPurchaseParameters` thay vì các đối số `subscriptionUpdateParams` và `isOfferPersonalized` riêng lẻ. Điều này giúp đảm bảo kiểu dữ liệu an toàn hơn và cho phép mở rộng các tham số mua hàng trong tương lai. ```diff showLineNumbers - final purchaseResult = await adapty.makePurchase( - product: product, - subscriptionUpdateParams: subscriptionUpdateParams, - isOfferPersonalized: true, - ); + final parameters = AdaptyPurchaseParametersBuilder() + ..setSubscriptionUpdateParams(subscriptionUpdateParams) + ..setIsOfferPersonalized(true) + ..setObfuscatedAccountId('your-account-id') + ..setObfuscatedProfileId('your-profile-id'); + final purchaseResult = await adapty.makePurchase( + product: product, + parameters: parameters.build(), + ); ``` Nếu không cần thêm tham số nào, bạn có thể sử dụng đơn giản như sau: ```dart showLineNumbers final purchaseResult = await adapty.makePurchase( product: product, ); ``` ## Cập nhật cách sử dụng model AdaptyPaywall \{#update-adaptywall-model-usage\} Thuộc tính `vendorProductIds` đã bị deprecated và được thay thế bằng `productIdentifiers`. Thuộc tính mới trả về các đối tượng `AdaptyProductIdentifier` thay vì chuỗi đơn giản, cung cấp thông tin sản phẩm có cấu trúc hơn. ```diff showLineNumbers - paywall.vendorProductIds.map((vendorId) => - ListTextTile(title: vendorId) - ).toList() + paywall.productIdentifiers.map((productId) => + ListTextTile(title: productId.vendorProductId) + ).toList() ``` Đối tượng `AdaptyProductIdentifier` cung cấp quyền truy cập vào vendor product ID thông qua thuộc tính `vendorProductId`, giữ nguyên chức năng như cũ trong khi cung cấp cấu trúc tốt hơn cho các cải tiến trong tương lai. ## Tương thích ngược \{#backward-compatibility\} Cả hai thay đổi đều duy trì tương thích ngược: - Các tham số cũ trong `makePurchase` đã bị deprecated nhưng vẫn hoạt động bình thường - Thuộc tính `vendorProductIds` đã bị deprecated nhưng vẫn có thể truy cập được - Code hiện tại sẽ tiếp tục hoạt động, mặc dù bạn sẽ thấy các cảnh báo deprecation Chúng tôi khuyến nghị cập nhật code của bạn để sử dụng các API mới nhằm đảm bảo khả năng tương thích trong tương lai và tận dụng tính an toàn kiểu dữ liệu và khả năng mở rộng được cải thiện. --- # File: flutter-migration-guide-38 --- --- title: "Migrate Adapty Flutter SDK to v3.8" description: "Migrate sang Adapty Flutter SDK v3.8 để cải thiện hiệu suất và có thêm tính năng monetization mới." --- Adapty SDK 3.8.0 là một bản phát hành lớn mang lại một số cải tiến, tuy nhiên có thể yêu cầu bạn thực hiện một số bước migration. 1. Cập nhật tên class observer và tên phương thức. 2. Cập nhật tên phương thức paywall dự phòng. 3. Cập nhật tên class view trong các phương thức xử lý sự kiện. ## Cập nhật tên class observer và tên phương thức \{#update-observer-class-and-method-names\} Tên class observer và phương thức đăng ký của nó đã được đổi tên: ```diff showLineNumbers - class MyObserver extends AdaptyUIObserver { + class MyObserver extends AdaptyUIPaywallsEventsObserver { @override void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { // Handle action } } // Register observer - AdaptyUI().setObserver(this); + AdaptyUI().setPaywallsEventsObserver(this); ``` ## Cập nhật tên phương thức paywall dự phòng \{#update-fallback-paywalls-method-name\} Phương thức đặt paywall dự phòng đã được đơn giản hóa: ```diff showLineNumbers try { - await Adapty.setFallbackPaywalls(assetId); + await Adapty.setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## Cập nhật tên class view trong các phương thức xử lý sự kiện \{#update-view-class-name-in-event-handling-methods\} Tất cả các phương thức xử lý sự kiện hiện sử dụng class `AdaptyUIPaywallView` mới thay cho `AdaptyUIView`: ```diff showLineNumbers - void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) + void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) - void paywallViewDidSelectProduct(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidSelectProduct(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidStartPurchase(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidFinishPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyProfile profile) + void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyProfile profile) - void paywallViewDidFailPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyError error) + void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) - void paywallViewDidFinishRestore(AdaptyUIView view, AdaptyProfile profile) + void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) - void paywallViewDidFailRestore(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) - void paywallViewDidFailLoadingProducts(AdaptyUIView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) + void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) - void paywallViewDidFailRendering(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) ``` --- # File: migration-to-flutter-sdk-34 --- --- title: "Migrate Adapty Flutter SDK to v3.4" description: "Migrate to Adapty Flutter SDK v3.4 for better performance and new monetization features." --- Adapty SDK 3.4.0 là một bản phát hành lớn với các cải tiến yêu cầu bạn thực hiện các bước migration. ## Cập nhật các file paywall dự phòng \{#update-fallback-paywall-files\} Cập nhật các file paywall dự phòng để đảm bảo tương thích với phiên bản SDK mới: 1. [Tải xuống các file paywall dự phòng đã cập nhật](fallback-paywalls) từ Adapty Dashboard. 2. [Thay thế các paywall dự phòng hiện có trong ứng dụng di động của bạn](flutter-use-fallback-paywalls) bằng các file mới. ## Cập nhật cách triển khai Observer Mode \{#update-implementation-of-observer-mode\} Nếu bạn đang sử dụng Observer Mode, hãy đảm bảo cập nhật cách triển khai của nó. Trước đây, các phương thức khác nhau được dùng để báo cáo giao dịch cho Adapty. Trong phiên bản mới, phương thức `reportTransaction` nên được sử dụng thống nhất trên cả Android và iOS. Phương thức này báo cáo rõ ràng từng giao dịch cho Adapty, đảm bảo nó được nhận diện. Nếu có sử dụng paywall, hãy truyền variation ID để liên kết giao dịch với paywall đó. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận diện được giao dịch, giao dịch đó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: ```diff showLineNumbers - // every time when calling transaction.finish() - if (Platform.isAndroid) { - try { - await Adapty().restorePurchases(); - } on AdaptyError catch (adaptyError) { - // handle the error - } catch (e) { - } - } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` --- # File: migration-to-flutter330 --- --- title: "Migrate Adapty Flutter SDK sang v3.3" description: "Migrate sang Adapty Flutter SDK v3.3 để cải thiện hiệu suất và các tính năng monetization mới." --- Adapty SDK 3.3.0 là một bản phát hành lớn mang lại một số cải tiến, tuy nhiên có thể yêu cầu bạn thực hiện một số bước migration. 1. Cập nhật phương thức cung cấp paywall dự phòng. 2. Xóa phương thức `getProductsIntroductoryOfferEligibility`. 3. Cập nhật cấu hình tích hợp cho Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh. 4. Cập nhật cài đặt Observer mode. ## Cập nhật phương thức cung cấp paywall dự phòng \{#update-method-for-providing-fallback-paywalls\} Trước đây, phương thức yêu cầu paywall dự phòng dưới dạng chuỗi JSON (`jsonString`), nhưng bây giờ nó nhận đường dẫn đến file dự phòng cục bộ (`assetId`) thay thế. ```diff showLineNumbers import 'dart:async' show Future; import 'dart:io' show Platform; -import 'package:flutter/services.dart' show rootBundle; -final filePath = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; -final jsonString = await rootBundle.loadString(filePath); +final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { - await adapty.setFallbackPaywalls(jsonString); + await adapty.setFallbackPaywalls(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Để xem ví dụ code đầy đủ, hãy xem trang [Sử dụng paywall dự phòng](flutter-use-fallback-paywalls). ## Xóa phương thức `getProductsIntroductoryOfferEligibility` \{#remove-getproductsintroductoryoffereligibility-method\} Trước Adapty iOS SDK 3.3.0, đối tượng sản phẩm luôn bao gồm các ưu đãi, bất kể người dùng có đủ điều kiện hay không. Bạn phải kiểm tra điều kiện thủ công trước khi sử dụng ưu đãi. Bây giờ, đối tượng sản phẩm chỉ bao gồm ưu đãi nếu người dùng đủ điều kiện. Điều này có nghĩa là bạn không cần kiểm tra điều kiện nữa — nếu có ưu đãi, người dùng đã đủ điều kiện. ## Cập nhật cấu hình SDK tích hợp bên thứ ba \{#update-third-party-integration-sdk-configuration\} Để đảm bảo các tích hợp hoạt động đúng với Adapty Flutter SDK 3.3.0 trở lên, hãy cập nhật cấu hình SDK của bạn cho các tích hợp sau theo mô tả trong các mục bên dưới. ### Adjust Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Adjust](adjust#connect-your-app-to-adjust). ```diff showLineNumbers import 'package:adjust_sdk/adjust.dart'; import 'package:adjust_sdk/adjust_config.dart'; try { final adid = await Adjust.getAdid(); if (adid == null) { // handle the error } + await Adapty().setIntegrationIdentifier( + key: "adjust_device_id", + value: adid, + ); final attributionData = await Adjust.getAttribution(); var attribution = Map<String, String>(); 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(<YOUR_OPTIONS>); 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: <BRANCH_IDENTITY_ID>, + ); - 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:<ul><li>error: Chỉ ghi lại các lỗi.</li><li>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ú ý.</li><li>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.</li><li>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.</li></ul> | | **withObserverMode** | tùy chọn | <p>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.</p><p>Giá trị mặc định là `false`.</p><p></p><p>🚧 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.</p> | | **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 | <p>Đặt thành `true` để tắt tính năng thu thập và chia sẻ IDFA.</p><p>Chia sẻ địa chỉ IP của người dùng.</p><p>Giá trị mặc định là `false`.</p><p>Để 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).</p> | | **withIpAddressCollectionDisabled** | tùy chọn | <p>Đặ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.</p><p>Giá trị mặc định là `false`.</p> | ### 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: <AdaptyUIObserver Implementation>, ); } catch (e) { // handle the error } ``` Lưu ý rằng cấu hình AdaptyUI là tùy chọn, bạn có thể kích hoạt module AdaptyUI mà không cần config. Tuy nhiên, nếu bạn sử dụng config, tất cả các tham số trong đó đều là bắt buộc. Các tham số: | Tham số | Bắt buộc | Mô tả | | :------------------------------ | :------- | :----------------------------------------------------------- | | **memoryStorageTotalCostLimit** | bắt buộc | Giới hạn tổng dung lượng lưu trữ tính bằng byte. | | **memoryStorageCountLimit** | bắt buộc | Giới hạn số lượng item trong bộ nhớ lưu trữ. | | **diskStorageSizeLimit** | bắt buộc | Giới hạn kích thước file trên ổ đĩa tính bằng byte. 0 nghĩa là không giới hạn. | --- # End of Documentation _Generated on: 2026-07-24T13:01:43.439Z_ _Successfully processed: 44/44 files_ # IOS - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.442Z Total files: 44 --- # File: sdk-installation-ios --- --- title: "Cài đặt & cấu hình iOS SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên iOS cho các ứng dụng dựa trên gói đăng ký." --- Adapty SDK bao gồm hai module chính để tích hợp liền mạch vào ứng dụng di động của bạn: - **Core Adapty**: SDK thiết yếu này bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module tùy chọn này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), công cụ no-code thân thiện với người dùng để dễ dàng tạo paywall đa nền tả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-iOS/tree/master/Examples) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: Để xem hướng dẫn triển khai đầy đủ, bạn cũng có thể xem các video sau: <Tabs groupId="current-os" queryString> <TabItem value="swiftui" label="iOS (SwiftUI)" default> <div style={{ textAlign: 'center' }}> <iframe width="560" height="315" src="https://www.youtube.com/embed/cSChHc8k2zA?si=KhNFhqXccIzYwTcm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> </div> </TabItem> <TabItem value="uikit" label="iOS (UIKit)" default> <div style={{ textAlign: 'center' }}> <iframe width="560" height="315" src="https://www.youtube.com/embed/WEUnlaAjSI0?si=sjXKVVb56tEHDKzJ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> </div> </TabItem> </Tabs> ## Yêu cầu \{#requirements\} Adapty iOS SDK yêu cầu iOS 15.0 trở lên. :::important Adapty SDK 3.15.7+ là bắt buộc khi build với Xcode 26.4 trở lên. ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-iOS.svg?style=flat&logo=apple)](https://github.com/adaptyteam/AdaptySDK-iOS/releases) Adapty SDK được cài đặt qua Swift Package Manager. Trong Xcode, vào **File** -> **Add Package Dependency...**. Lưu ý rằng các bước thêm package dependency có thể khác nhau tùy phiên bản Xcode, vì vậy hãy tham khảo tài liệu của Xcode nếu cần. 1. Nhập URL repository: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Chọn phiên bản (khuyến nghị dùng phiên bản ổn định mới nhất) rồi nhấn **Add Package**. 3. Trong cửa sổ **Choose Package Products**, chọn các module bạn cần: - **Adapty** (module cốt lõi) - **AdaptyUI** (tùy chọn - chỉ cần thiết nếu bạn dùng Paywall Builder) :::note Lưu ý: - Để bật [Kids Mode](kids-mode) trong SDK 3.x, hãy chọn **Adapty_KidsMode** thay vì **Adapty**. Từ SDK 4.0 trở đi, chọn các module thông thường — Kids Mode được bật thông qua package trait `KidsMode`. - Đừng chọn bất kỳ package nào khác trong danh sách — bạn sẽ không cần đến chúng. ::: 4. Nhấn **Add Package** để hoàn tất cài đặt. 5. **Kiểm tra cài đặt:** Trong project navigator, bạn sẽ thấy "Adapty" (và "AdaptyUI" nếu đã chọn) xuất hiện dưới **Package Dependencies**. :::important Adapty iOS SDK 4.0 là phiên bản pre-release. Swift Package Manager không tự động phân giải các phiên bản beta thông qua quy tắc **Up to Next Major Version** (`from:`), vì vậy bạn phải ghim đúng phiên bản cụ thể. Trong Xcode, đặt **Dependency Rule** thành **Exact Version** và nhập `4.0.0-beta.2`. Trong `Package.swift`, sử dụng `.exact("4.0.0-beta.2")`. Xem [Migrate Adapty iOS SDK sang v4](migration-to-ios-sdk-v4). ::: ## Kích hoạt module Adapty trong Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Kích hoạt Adapty SDK trong code của ứng dụng. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: Để lấy **Public SDK Key**: 1. Truy cập Adapty Dashboard và điều hướng đến [**App settings → General**](https://app.adapty.io/settings/general). 2. Trong phần **Api keys**, sao chép **Public SDK Key** (KHÔNG phải Secret Key). 3. Thay thế `"YOUR_PUBLIC_SDK_KEY"` trong code. Hoặc lấy theo cách lập trình, sử dụng [Adapty CLI](developer-cli): ``` npm install -g adapty adapty auth login adapty apps list ``` Hoặc, trực tiếp: ``` npx adapty auth login adapty apps list ``` - Đảm bảo bạn sử dụng **Public SDK key** để khởi tạo Adapty, **Secret key** chỉ nên dùng cho [server-side API](getting-started-with-server-side-api). - **SDK keys** là duy nhất cho mỗi ứng dụng, vì vậy nếu bạn có nhiều ứng dụng, hãy đảm bảo chọn đúng key. <Tabs groupId="current-os" queryString> <TabItem value="swiftui" label="SwiftUI"> ```swift showLineNumbers @main struct YourApp: App { init() { // Configure Adapty SDK let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard Adapty.logLevel = .verbose // recommended for development and the first production release let config = configurationBuilder.build() // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } var body: some Scene { WindowGroup { // Your content view } } } } ``` </TabItem> <TabItem value="swift" label="UIKit" default> ```swift showLineNumbers // In your AppDelegate class: // If you only use an AppDelegate, place the following code in the // application(_:didFinishLaunchingWithOptions:) method. // If you use a SceneDelegate, place the following code in the // scene(_:willConnectTo:options:) method. Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(logLevel: .verbose) // recommended for development and the first production release let config = configurationBuilder.build() try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` </TabItem> </Tabs> :::important Chờ `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 iOS SDK](ios-sdk-call-order) để biết toàn bộ trình tự. ::: Tiếp theo, 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 về Paywall Builder](ios-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, xem [hướng dẫn nhanh cho paywall tùy chỉnh](ios-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn dự định sử dụng [Paywall Builder](adapty-paywall-builder) và đã [cài đặt module AdaptyUI](sdk-installation-ios#install-adapty-sdk), bạn cũng cần kích hoạt AdaptyUI. :::important Trong code của bạn, bạn phải kích hoạt module Adapty core trước khi kích hoạt AdaptyUI. ::: <Tabs groupId="current-os" queryString> <TabItem value="swiftui" label="SwiftUI"> ```swift showLineNumbers title="Swift" @main struct YourApp: App { init() { // ...ConfigurationBuilder steps // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: config) try await AdaptyUI.activate() } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } // main body... } } ``` </TabItem> <TabItem value="uikit" label="UIKit" default> ```swift showLineNumbers title="UIKit" // If you only use an AppDelegate, place the following code in the // application(_:didFinishLaunchingWithOptions:) method. // If you use a SceneDelegate, place the following code in the // scene(_:willConnectTo:options:) method. Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(logLevel: .verbose) // recommended for development let config = configurationBuilder.build() try await Adapty.activate(with: config) try await AdaptyUI.activate() } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` </TabItem> </Tabs> :::tip Tùy chọn, khi kích hoạt AdaptyUI, bạn có thể [ghi đè cài đặt lưu cache mặc định cho paywall](#set-up-media-cache-configuration-for-adaptyui). ::: ## Cài đặt tùy chọn \{#optional-setup\} ### Ghi log \{#logging\} #### Thiết lập hệ thống ghi log \{#set-up-the-logging-system\} Adapty ghi lại các lỗi và thông tin quan trọng khác để giúp bạn hiểu chuyện gì đang xảy ra. Các cấp độ log có sẵn như sau: | Level | Description | | ---------- | ------------------------------------------------------------ | | `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à nhiều thông báo thông tin khác | | `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, truy vấn API, v.v. | ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(logLevel: .verbose) // recommended for development ``` #### Chuyển hướng thông báo từ hệ thống logging \{#redirect-the-logging-system-messages\} Nếu bạn cần gửi thông báo log của Adapty đến hệ thống của mình hoặc lưu chúng vào file, hãy sử dụng phương thức `setLogHandler` và triển khai logic logging tùy chỉnh bên trong đó. Handler này nhận các bản ghi log chứa nội dung thông báo và mức độ nghiêm trọng. ```swift showLineNumbers title="Swift" Adapty.setLogHandler { record in writeToLocalFile("Adapty \(record.level): \(record.message)") } ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi lên, nhưng bạn có thể triển khai 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 từng quốc gia. #### Tắt thu thập và chia sẻ IDFA \{#disable-idfa-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `idfaCollectionDisabled` thành `true` để tắt việc thu thập và chia sẻ IDFA. Sử dụng tham số này để tuân thủ Nguyên tắc Đánh giá App Store hoặc tránh kích hoạt lời nhắc App Tracking Transparency khi IDFA không cần thiết cho ứng dụng của bạn. Giá trị mặc định là `false`. Để biết thêm chi tiết về việc thu thập IDFA, hãy tham khảo phần [Tích hợp Analytics](analytics-integration#disable-collection-of-advertising-identifiers). ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(idfaCollectionDisabled: true) ``` #### Tắt tính năng thu thập và chia sẻ địa chỉ IP \{#disable-ip-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt việc thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Dùng tham số này để tăng cường quyền riêng tư cho người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm bớt 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ần thiết cho ứng dụng của bạn. ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(ipAddressCollectionDisabled: true) ``` #### Cấu hình cache media cho paywall trong AdaptyUI \{#media-cache-configuration-for-paywalls-in-adaptyui\} Lưu ý rằng cấu hình AdaptyUI là tùy chọn. Bạn có thể kích hoạt module AdaptyUI mà không cần config. Tuy nhiên, nếu bạn sử dụng config, tất cả các tham số đều bắt buộc. ```swift showLineNumbers title="Swift" // Configure AdaptyUI let adaptyUIConfiguration = AdaptyUI.Configuration( mediaCacheConfiguration: .init( memoryStorageTotalCostLimit: 100 * 1024 * 1024, memoryStorageCountLimit: .max, diskStorageSizeLimit: 100 * 1024 * 1024 ) ) // Activate AdaptyUI AdaptyUI.activate(configuration: adaptyUIConfiguration) ``` Tham số: | Parameter | Presence | Description | | :-------------------------- | :------- | :----------------------------------------------------------- | | memoryStorageTotalCostLimit | required | Tổng giới hạn chi phí của bộ nhớ lưu trữ tính bằng byte. | | memoryStorageCountLimit | required | Giới hạn số lượng mục của bộ nhớ lưu trữ. | | diskStorageSizeLimit | required | Giới hạn kích thước tệp trên đĩa của bộ nhớ lưu trữ tính bằng byte. 0 nghĩa là không giới hạn. | ### Hành vi hoàn tất giao dịch \{#transaction-finishing-behavior\} :::info Tính năng này khả dụng từ SDK phiên bản 3.12.0 trở lên. ::: Theo mặc định, Adapty tự động hoàn tất giao dịch sau khi xác thực thành công. Tuy nhiên, nếu bạn cần xác thực giao dịch nâng cao (chẳng hạn như xác thực receipt phía server, phát hiện gian lận, hoặc logic nghiệp vụ tùy chỉnh), bạn có thể cấu hình SDK để sử dụng chế độ hoàn tất giao dịch thủ công. ```swift showLineNumbers title="Swift" let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(transactionsFinishBehavior: .manual) // .auto là mặc định ``` Xem thêm chi tiết về cách hoàn tất giao dịch trong [hướng dẫn](ios-transaction-management). ### Xóa dữ liệu khi khôi phục từ backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ bản backup iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng đã cache, chi tiết sản phẩm và paywall. Sau đó SDK sẽ khởi tạo lại với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ cache cục bộ của SDK bị xóa. Lịch sử giao dịch với Apple và dữ liệu người dùng trên máy chủ Adapty vẫn không thay đổi. ::: ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(clearDataOnBackup: true) // default – false ``` ## Khắc phục sự cố \{#troubleshooting\} #### Lỗi concurrency Swift 6 với Tuist \{#swift-6-concurrency-error-with-tuist\} Khi build với [Tuist](https://tuist.dev/), bạn có thể gặp lỗi biên dịch strict concurrency của Swift 6. Biểu hiện điển hình là lỗi không khớp thuộc tính `@Sendable` trong `AdaptyUIBuilderLogic` hoặc các lỗi Sendability tương tự giữa các module. Điều này xảy ra vì Tuist tạo ra các dự án Xcode từ các gói SPM nhưng không giữ nguyên cài đặt `swift-tools-version: 6.0`. Kết quả là, một số target của Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`) được biên dịch theo quy tắc Swift 5 trong khi các target khác dùng Swift 6, dẫn đến sự không khớp `@Sendable` giữa các module. **Cách khắc phục**: Nâng cấp lên Adapty SDK **3.15.5** trở lên, phiên bản này giải quyết vấn đề bất kể sự pha trộn giữa các phiên bản ngôn ngữ Swift. **Giải pháp tạm thời**: Nếu bạn chưa thể nâng cấp, hãy đặt Swift 6 một cách tường minh cho cả ba target của Adapty trong cấu hình Tuist của bạn: ```swift showLineNumbers targetSettings: [ "Adapty": .init().swiftVersion("6"), "AdaptyUI": .init().swiftVersion("6"), "AdaptyUIBuilder": .init().swiftVersion("6"), ] ``` --- # File: ios-quickstart-paywalls --- --- title: "Kích hoạt mua hàng với Flow Builder trong iOS SDK" description: "Hướng dẫn nhanh để kích hoạt in-app purchase với Adapty Flow Builder." --- Để kích hoạt in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – mọi thứ người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flows**](adapty-flow-builder) – chuỗi màn hình giới thiệu sản phẩm đến người dùng, được xây dựng trong Flow Builder không cần code. SDK truy xuất chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện bằng code, hãy dùng paywall — xem [Triển khai paywall thủ công](ios-quickstart-manual). - [**Placements**](placements) – nơi và thời điểm hiển thị flow trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trên dashboard, sau đó yêu cầu chúng theo placement ID trong code. Cách này giúp bạn dễ dàng chạy A/B test và hiển thị các flow khác nhau cho từng nhóm người dùng. Adapty cung cấp cho bạn ba cách để kích hoạt mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Cách triển khai | Độ phức tạp | Khi nào nên dùng | |---|---|---| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng mua hàng trong trình tạo no-code](quickstart-paywalls). Adapty tự động hiển thị và xử lý toàn bộ flow mua hàng, xác thực biên lai, và quản lý gói đăng ký. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự triển khai giao diện paywall trong code của mình, nhưng vẫn lấy đối tượng flow từ Adapty để duy trì sự linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](ios-quickstart-manual). | | Chế độ observer | 🔴 Khó | Bạn đã có hạ tầng xử lý mua hàng riêng và muốn tiếp tục dùng nó. Lưu ý rằng chế độ observer có một số hạn chế trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, hãy xem [Triển khai paywall thủ công](ios-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code ứng dụng, bạn chỉ cần: 1. **Lấy flow**: Lấy flow từ Adapty. 2. **Hiển thị flow và Adapty sẽ xử lý thanh toán cho bạn**: Hiển thị view trong ứng dụng. 3. **Xử lý các hành động của nút**: Liên kết các thao tác của người dùng với phản hồi tương ứng trong ứng dụng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. [Kết nối ứng dụng với App Store](initial_ios) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo flow và thêm sản phẩm vào đó](create-paywall). 4. [Tạo placement và thêm flow vào đó](create-placement). 5. [Cài đặt và kích hoạt SDK](sdk-installation-ios) trong code của ứng dụng. Hướng dẫn này sử dụng Adapty iOS SDK v4 APIs. ## 1. Lấy flow \{#get-the-flow\} Các flow của bạn được liên kết với các placement được cấu hình trong dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy một flow đã tạo trong Adapty Flow Builder, bạn cần: 1. Lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow` và kiểm tra xem nó có cấu hình view hay không. 2. Lấy cấu hình view bằng phương thức `getFlowConfiguration`. Cấu hình này chứa các thành phần UI và styling cần thiết để hiển thị flow. ```swift func loadFlow() async { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") guard flow.hasViewConfiguration else { print("Flow doesn't have a view configuration") return } flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow) } ``` ## 2. Hiển thị flow \{#2-display-the-flow\} Bây giờ, khi bạn đã có cấu hình flow, chỉ cần thêm vài dòng code để hiển thị flow của mình. <Tabs groupId="current-os" queryString> <TabItem value="swiftui" label="SwiftUI" default> Trong SwiftUI, khi hiển thị flow, bạn cũng cần xử lý các sự kiện. `didFinishPurchase`, `didFailPurchase`, `didFinishRestore`, `didFailRestore` và `didReceiveError` là bắt buộc. Khi kiểm thử, bạn có thể sao chép đoạn code bên dưới để ghi log các sự kiện này. :::tip Flow không tự động đóng sau khi mua hàng thành công. Trong `didFinishPurchase`, hãy đặt binding hiển thị của bạn thành `false` để đóng flow, hoặc không làm gì để flow tiếp tục chuyển sang các màn hình tiếp theo. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didFinishPurchase: { product, purchaseResult in print("Purchase finished successfully") flowPresented = false // or do nothing to let the flow continue }, didFailPurchase: { product, error in print("Purchase failed: \(error)") }, didFinishRestore: { profile in print("Restore finished successfully") }, didFailRestore: { error in print("Restore failed: \(error)") }, didReceiveError: { error in flowPresented = false print("Flow error: \(error)") } ) ``` </TabItem> <TabItem value="uikit" label="UIKit" default> ```swift func presentFlow(with config: AdaptyUI.FlowConfiguration) { let flowController = try AdaptyUI.flowController( with: config, delegate: self ) present(flowController, animated: true) } ``` Implement `AdaptyFlowControllerDelegate` để xử lý các sự kiện. Tối thiểu, hãy implement bốn phương thức không có cài đặt mặc định. Lưu ý rằng controller sẽ không tự dismiss sau khi mua thành công — hãy dismiss nó trong `didFinishPurchase`, hoặc không làm gì để flow tiếp tục sang các màn hình tiếp theo: ```swift extension YourViewController: AdaptyFlowControllerDelegate { func flowController(_ controller: AdaptyFlowController, didFinishPurchase product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) { if !purchaseResult.isPurchaseCancelled { controller.dismiss(animated: true) // or do nothing to let the flow continue } } func flowController(_ controller: AdaptyFlowController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError) { print("Purchase failed: \(error)") } func flowController(_ controller: AdaptyFlowController, didFinishRestoreWith profile: AdaptyProfile) { print("Restore finished successfully") } func flowController(_ controller: AdaptyFlowController, didFailRestoreWith error: AdaptyError) { print("Restore failed: \(error)") } } ``` </TabItem> </Tabs> :::info Để biết thêm chi tiết về cách hiển thị một flow, hãy xem [hướng dẫn](ios-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút \{#handle-button-actions\} Khi người dùng nhấn các nút, iOS SDK tự động xử lý việc mua hàng, khôi phục, đóng flow và mở liên kết. Tuy nhiên, một số nút có ID tùy chỉnh hoặc được định sẵn và cần xử lý hành động trong code của bạn. Hoặc, bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ, dưới đây là cách xử lý nút đóng. Trong UIKit, SDK tự động dismiss controller khi `.close` được kích hoạt — chỉ ghi đè nếu bạn muốn hành vi tùy chỉnh. Trong SwiftUI, bạn phải tự đặt binding `isPresented` của mình thành `false`. :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [hành động](handle-paywall-actions) và [sự kiện](ios-handling-events) của nút. ::: <Tabs groupId="current-os" queryString> <TabItem value="swiftui" label="SwiftUI" default> ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false // dismiss the flow when the user taps close default: break } }, didFinishPurchase: { product, purchaseResult in flowPresented = false }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) ``` </TabItem> <TabItem value="uikit" label="UIKit" default> ```swift extension YourViewController: AdaptyFlowControllerDelegate { func flowController(_ controller: AdaptyFlowController, didPerform action: AdaptyUI.Action) { switch action { case .close: controller.dismiss(animated: true) // default behavior — override only if needed default: break } } } ``` </TabItem> </Tabs> ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Flow của bạn đã sẵn sàng để hiển thị trong app. [Kiểm thử mua hàng trong chế độ sandbox](test-purchases-in-sandbox) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](ios-check-subscription-status) để đảm bảo bạn hiển thị flow hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Dưới đây là cách tích hợp tất cả các bước trong hướng dẫn này vào ứng dụng của bạn. <Tabs groupId="current-os" queryString> <TabItem value="swiftui" label="SwiftUI" default> ```swift struct ContentView: View { @State private var flowPresented = false @State private var flowConfiguration: AdaptyUI.FlowConfiguration? @State private var isLoading = false @State private var hasInitialized = false var body: some View { VStack { if isLoading { ProgressView("Loading...") } else { Text("Your App Content") } } .task { guard !hasInitialized else { return } await initializeFlow() hasInitialized = true } .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false default: break } }, didFinishPurchase: { product, purchaseResult in print("Purchase finished successfully") flowPresented = false // or do nothing to let the flow continue }, didFailPurchase: { product, error in print("Purchase failed: \(error)") }, didFinishRestore: { profile in print("Restore finished successfully") }, didFailRestore: { error in print("Restore failed: \(error)") }, didReceiveError: { error in print("Flow error: \(error)") flowPresented = false } ) } private func initializeFlow() async { isLoading = true defer { isLoading = false } await loadFlow() flowPresented = true } private func loadFlow() async { do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") guard flow.hasViewConfiguration else { print("Flow doesn't have a view configuration") return } flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow) } catch { print("Failed to load: \(error)") } } } ``` </TabItem> <TabItem value="uikit" label="UIKit" default> ```swift class ViewController: UIViewController { private var flowConfiguration: AdaptyUI.FlowConfiguration? override func viewDidLoad() { super.viewDidLoad() Task { await initializeFlow() } } private func initializeFlow() async { do { flowConfiguration = try await loadFlow() if let flowConfiguration { await MainActor.run { presentFlow(with: flowConfiguration) } } } catch { print("Error initializing: \(error)") } } private func loadFlow() async throws -> AdaptyUI.FlowConfiguration? { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") guard flow.hasViewConfiguration else { print("Flow doesn't have a view configuration") return nil } return try await AdaptyUI.getFlowConfiguration(forFlow: flow) } private func presentFlow(with config: AdaptyUI.FlowConfiguration) { guard let flowController = try? AdaptyUI.flowController( with: config, delegate: self ) else { return } present(flowController, animated: true) } } extension ViewController: AdaptyFlowControllerDelegate { func flowController(_ controller: AdaptyFlowController, didFinishPurchase product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) { if !purchaseResult.isPurchaseCancelled { controller.dismiss(animated: true) // or do nothing to let the flow continue } } func flowController(_ controller: AdaptyFlowController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError) { print("Purchase failed for \(product.vendorProductId): \(error)") guard error.adaptyErrorCode != .paymentCancelled else { return } let message = switch error.adaptyErrorCode { case .paymentNotAllowed: "Purchases are not allowed on this device." default: "Purchase failed. Please try again." } let alert = UIAlertController(title: "Purchase Error", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) present(alert, animated: true) } func flowController(_ controller: AdaptyFlowController, didFinishRestoreWith profile: AdaptyProfile) { print("Restore finished successfully") controller.dismiss(animated: true) } func flowController(_ controller: AdaptyFlowController, didFailRestoreWith error: AdaptyError) { print("Restore failed: \(error)") } func flowController(_ controller: AdaptyFlowController, didReceiveError error: AdaptyUIError) { print("Flow error: \(error)") controller.dismiss(animated: true) } } ``` </TabItem> </Tabs> --- # File: ios-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong iOS SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng iOS của bạn với Adapty." --- Để quyết định người dùng có thể truy cập nội dung trả phí hay cần hiển thị paywall, bạn cần kiểm tra [mức độ truy cập](access-level) của họ trong hồ sơ người dùng. Bài viết này hướng dẫn bạn cách truy cập trạng thái hồ sơ người dùng để quyết định nên hiển thị paywall hay cấp quyền truy cập tính năng trả phí cho người dùng. ## Lấy trạng thái gói đăng ký \{#get-subscription-status\} Khi quyết định hiển thị paywall hay nội dung trả phí cho người dùng, bạn kiểm tra [mức độ truy cập](access-level) trong hồ sơ người dùng của họ. Có hai cách: - Gọi `getProfile` nếu bạn cần dữ liệu hồ sơ mới nhất ngay lập tức (ví dụ khi khởi động ứng dụng) hoặc muốn buộc cập nhật. - Thiết lập **cập nhật hồ sơ tự động** để duy trì bản sao cục bộ được tự động làm mới mỗi khi trạng thái gói đăng ký thay đổi. :::important Theo mặc định, mức độ truy cập `premium` đã tồn tại trong Adapty. Nếu bạn không cần thiết lập nhiều hơn một mức độ truy cập, bạn có thể dùng `premium` luôn. ::: ### Lấy hồ sơ người dùng \{#get-profile\} Cách đơn giản nhất để lấy trạng thái gói đăng ký là dùng phương thức `getProfile` để truy cập hồ sơ người dùng: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` </TabItem> </Tabs> ### Lắng nghe cập nhật gói đăng ký \{#listen-to-subscription-updates\} Nếu bạn muốn tự động nhận cập nhật hồ sơ trong ứng dụng: 1. Conform vào protocol `AdaptyDelegate` trong kiểu dữ liệu bạn chọn và implement phương thức `didLoadLatestProfile` — Adapty sẽ tự động gọi phương thức này mỗi khi trạng thái gói đăng ký của người dùng thay đổi. Trong ví dụ dưới đây, chúng ta dùng kiểu `SubscriptionManager` để hỗ trợ xử lý các luồng gói đăng ký và hồ sơ người dùng. Kiểu này có thể được inject như một dependency hoặc thiết lập như singleton trong ứng dụng UIKit, hoặc thêm vào môi trường SwiftUI từ struct chính của ứng dụng. 2. Lưu dữ liệu hồ sơ được cập nhật khi phương thức này được gọi, để bạn có thể sử dụng xuyên suốt ứng dụng mà không cần thực hiện thêm request mạng. ```swift class SubscriptionManager: AdaptyDelegate { nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { let hasAccess = profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false // Update UI, unlock content, etc. } } // Set delegate after Adapty activation Adapty.delegate = subscriptionManager ``` :::note Adapty tự động gọi `didLoadLatestProfile` khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký đã cache ngay cả khi thiết bị ngoại tuyến. ::: ## Kết nối hồ sơ người dùng với logic paywall \{#connect-profile-with-paywall-logic\} Khi bạn cần đưa ra quyết định ngay lập tức về việc hiển thị paywall hay cấp quyền truy cập tính năng trả phí, bạn có thể kiểm tra hồ sơ người dùng trực tiếp. Cách này hữu ích trong các tình huống như khởi động ứng dụng, khi vào các khu vực premium, hoặc trước khi hiển thị nội dung cụ thể. <Tabs> <TabItem value="swiftui" label="SwiftUI" default> ```swift private func checkAccessLevel() async -> Bool { do { let profile = try await Adapty.getProfile() return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false } catch { print("Error checking access level: \(error)") return false } } // In your initialization logic: let hasAccess = await checkAccessLevel() if !hasAccess { paywallPresented = true // Show paywall if no access } ``` </TabItem> <TabItem value="uikit" label="UIKit"> ```swift private func checkAccessLevel() async throws -> Bool { let profile = try await Adapty.getProfile() return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false } // In your initialization logic: let hasAccess = try await checkAccessLevel() if !hasAccess { presentPaywall(with: paywallConfiguration) } ``` </TabItem> </Tabs> ## Bước tiếp theo \{#next-steps\} Bây giờ bạn đã biết cách theo dõi trạng thái gói đăng ký, hãy [tìm hiểu cách làm việc với hồ sơ người dùng](ios-quickstart-identify) để đảm bảo nó phù hợp với hệ thống xác thực hiện có và quyền chia sẻ quyền truy cập trả phí của bạn. Nếu bạn không có hệ thống xác thực riêng, điều đó hoàn toàn không sao — Adapty sẽ quản lý người dùng cho bạn, nhưng bạn vẫn có thể đọc [hướng dẫn](ios-quickstart-identify) để tìm hiểu cách Adapty hoạt động với người dùng ẩn danh. --- # File: ios-quickstart-identify --- --- title: "Xác định người dùng trong iOS SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký trong ứng dụng." --- :::important Hướng dẫn này dành cho bạn nếu bạn có hệ thống xác thực riêng. Ở đây, bạn sẽ học cách làm việc với hồ sơ người dùng trong Adapty để đảm bảo nó phù hợp với hệ thống xác thực hiện có của bạn. ::: Cách bạn quản lý các giao dịch mua của người dùng phụ thuộc vào mô hình xác thực của ứng dụng: - Nếu ứng dụng của bạn không sử dụng xác thực backend và không lưu trữ dữ liệu người dùng, xem [phần về người dùng ẩn danh](#anonymous-users). - Nếu ứng dụng của bạn có (hoặc sẽ có) xác thực backend, xem [phần về người dùng đã xác định](#identified-users). **Các khái niệm chính**: - **Hồ sơ người dùng** là các thực thể cần thiết để SDK hoạt động. Adapty tự động tạo chúng. - Chúng có thể là ẩn danh **(không có customer user ID)** hoặc đã xác định **(có customer user ID)**. - Bạn cung cấp **customer user ID** để liên kết chéo hồ sơ người dùng trong Adapty với hệ thống xác thực nội bộ của bạn. Dưới đây là sự khác biệt giữa người dùng ẩn danh và người dùng đã xác định: | | Người dùng ẩn danh | Người dùng đã xác định | |-------------------------|--------------------------------------------------------------|--------------------------------------------------------------------------------------| | **Quản lý mua hàng** | Khôi phục giao dịch mua ở cấp độ cửa hàng | Duy trì lịch sử mua hàng trên nhiều thiết bị thông qua customer user ID của họ | | **Quản lý hồ sơ** | Hồ sơ mới mỗi lần cài đặt lại | Cùng một hồ sơ xuyên suốt các phiên và thiết bị | | **Lưu trữ dữ liệu** | Dữ liệu của người dùng ẩn danh gắn với lần cài đặt ứng dụng | Dữ liệu của người dùng đã xác định được lưu trữ xuyên suốt các lần cài đặt ứng dụng | ## Người dùng ẩn danh \{#anonymous-users\} Nếu bạn không có xác thực backend, **bạn không cần xử lý xác thực trong code ứng dụng**: 1. Khi SDK được kích hoạt lần đầu tiên khởi chạy ứng dụng, Adapty **tạo một hồ sơ người dùng mới cho người dùng**. 2. Khi người dùng mua bất cứ thứ gì trong ứng dụng, giao dịch mua này được **liên kết với hồ sơ Adapty và tài khoản cửa hàng của họ**. 3. Khi người dùng **cài đặt lại** ứng dụng hoặc cài đặt từ **thiết bị mới**, Adapty **tạo một hồ sơ ẩn danh mới khi kích hoạt**. 4. Nếu người dùng đã thực hiện giao dịch mua trước đó trong ứng dụng của bạn, theo mặc định, các giao dịch mua của họ sẽ được tự động đồng bộ từ App Store khi SDK kích hoạt. :::note Khôi phục từ backup hoạt động khác với cài đặt lại. Theo mặc định, khi người dùng khôi phục từ backup, SDK giữ nguyên dữ liệu đã lưu trong bộ nhớ cache và không tạo hồ sơ mới. Bạn có thể cấu hình hành vi này bằng cài đặt `clearDataOnBackup`. [Tìm hiểu thêm](sdk-installation-ios#clear-data-on-backup-restore). ::: Vì vậy, với người dùng ẩn danh, hồ sơ mới sẽ được tạo mỗi lần cài đặt, nhưng đó không phải là vấn đề vì trong phân tích Adapty, bạn có thể [cấu hình những gì sẽ được coi là lần cài đặt mới](general#4-installs-definition-for-analytics). Đối với người dùng ẩn danh, bạn cần đếm lần cài đặt theo **ID thiết bị**. Trong trường hợp này, mỗi lần cài đặt ứng dụng trên một thiết bị được tính là một lần cài đặt, kể cả cài đặt lại. ## Người dùng đã xác định \{#identified-users\} Bạn có hai tùy chọn để xác định người dùng trong ứng dụng: - [**Trong quá trình đăng nhập/đăng ký:**](#during-loginsignup) Nếu người dùng đăng nhập sau khi ứng dụng khởi động, gọi `identify()` với customer user ID khi họ xác thực. - [**Trong quá trình kích hoạt SDK:**](#during-the-sdk-activation) Nếu bạn đã có customer user ID được lưu trữ khi ứng dụng khởi động, hãy gửi nó khi gọi `activate()`. :::important Theo mặc định, khi Adapty nhận được giao dịch mua từ một Customer User ID hiện đang được liên kết với Customer User ID khác, mức độ truy cập sẽ được chia sẻ, vì vậy cả hai hồ sơ đều có quyền truy cập trả phí. Bạn có thể cấu hình cài đặt này để chuyển quyền truy cập trả phí từ hồ sơ này sang hồ sơ khác hoặc tắt hoàn toàn việc chia sẻ. Xem [bài viết](general#6-sharing-paid-access-between-user-accounts) để biết thêm chi tiết. ::: <img src="/assets/shared/img/identify-diagram.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Trong quá trình đăng nhập/đăng ký \{#during-loginsignup\} Nếu bạn xác định người dùng sau khi ứng dụng khởi động (ví dụ: sau khi họ đăng nhập vào ứng dụng hoặc đăng ký), hãy dùng phương thức `identify` để đặt customer user ID của họ. - Nếu bạn **chưa sử dụng customer user ID này trước đây**, Adapty sẽ tự động liên kết nó với hồ sơ hiện tại. - Nếu bạn **đã sử dụng customer user ID này để xác định người dùng trước đây**, Adapty sẽ chuyển sang làm việc với hồ sơ được liên kết với customer user ID này. :::important Customer user ID phải là duy nhất cho mỗi người dùng. Nếu bạn hardcode giá trị tham số, tất cả người dùng sẽ được coi là một. ::: Luôn `await` `identify` trước khi gọi các phương thức SDK khác. Các lệnh gọi đồng thời sẽ tạo ra lỗi `#3006 profileWasChanged` hoặc sẽ hoạt động trên hồ sơ ẩn danh. Xem [Thứ tự gọi trong iOS SDK](ios-sdk-call-order). <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` </TabItem> </Tabs> ### Trong quá trình kích hoạt SDK \{#during-the-sdk-activation\} Nếu bạn đã biết customer user ID khi kích hoạt SDK, bạn có thể gửi nó trong phương thức `activate` thay vì gọi `identify` riêng. Nếu bạn biết customer user ID nhưng chỉ đặt nó sau khi kích hoạt, điều đó có nghĩa là khi kích hoạt, Adapty sẽ tạo một hồ sơ ẩn danh mới và chỉ chuyển sang hồ sơ hiện có sau khi bạn gọi `identify`. Bạn có thể truyền customer user ID hiện có (đã dùng trước đó) hoặc một customer user ID mới. Nếu bạn truyền một customer user ID mới, hồ sơ ẩn danh được tạo khi kích hoạt sẽ tự động được liên kết với customer user ID đó. :::note Theo mặc định, việc tạo hồ sơ ẩn danh không ảnh hưởng đến dashboard phân tích, vì lần cài đặt được đếm dựa trên ID thiết bị. ID thiết bị đại diện cho một lần cài đặt ứng dụng từ cửa hàng trên một thiết bị và chỉ được tạo lại sau khi ứng dụng được cài đặt lại. Nó không phụ thuộc vào việc đây là lần cài đặt đầu tiên hay lần cài đặt lặp lại, hoặc liệu có sử dụng customer user ID hiện có hay không. Việc tạo hồ sơ (khi kích hoạt SDK hoặc đăng xuất), đăng nhập, hoặc nâng cấp ứng dụng mà không cài đặt lại không tạo ra các sự kiện cài đặt bổ sung. Nếu bạn muốn đếm lần cài đặt dựa trên người dùng duy nhất thay vì thiết bị, hãy vào **App settings** và cấu hình [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers // Place in the app main struct for SwiftUI or in AppDelegate for UIKit let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers // Place in the app main struct for SwiftUI or in AppDelegate for UIKit let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` </TabItem> </Tabs> ### Đă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. ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { try await Adapty.logout() } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.logout { error in if error == nil { // successful logout } } ``` </TabItem> </Tabs> :::info Để đăng nhập lại người dùng vào ứng dụng, hãy sử dụng phương thức `identify`. ::: ### Cho phép mua hàng mà không cần đăng nhập \{#allow-purchases-without-login\} Nếu người dùng của bạn có thể thực hiện giao dịch mua cả trước và sau khi đăng nhập vào ứng dụng, bạn cần đảm bảo rằng họ vẫn giữ được quyền truy cập sau khi đăng nhập: 1. Khi người dùng chưa đăng nhập thực hiện giao dịch mua, Adapty liên kết nó với ID hồ sơ ẩn danh của họ. 2. Khi người dùng đăng nhập vào tài khoản của họ, Adapty chuyển sang làm việc với hồ sơ đã xác định của họ. - Nếu đây là customer user ID mới (ví dụ: giao dịch mua đã được thực hiện trước khi đăng ký), Adapty sẽ gán customer user ID cho hồ sơ hiện tại, do đó toàn bộ lịch sử mua hàng được duy trì. - Nếu đây là customer user ID hiện có (customer user ID đã được liên kết với một hồ sơ), bạn cần lấy mức độ truy cập thực tế sau khi chuyển đổi hồ sơ. Bạn có thể gọi [`getProfile`](ios-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe các cập nhật hồ sơ](ios-check-subscription-status) để dữ liệu tự động đồng bộ. ## Các bước tiếp theo \{#next-steps\} Chúc mừng! Bạn đã triển khai logic thanh toán trong ứng dụng! Chúc bạn thành công với việc kiếm tiền từ ứng dụng! Để khai thác thêm từ Adapty, bạn có thể khám phá các chủ đề sau: - [**Kiểm thử**](test-purchases-in-sandbox): Đảm bảo mọi thứ hoạt động như mong đợi - [**Onboardings**](ios-onboardings): Thu hút người dùng với onboarding và thúc đẩy tỷ lệ giữ chân - [**Tích hợp**](configuration): Tích hợp với các dịch vụ attribution marketing và phân tích chỉ trong một dòng code - [**Đặt thuộc tính hồ sơ tùy chỉnh**](setting-user-attributes): Thêm thuộc tính tùy chỉnh vào hồ sơ người dùng và tạo phân khúc, để bạn có thể chạy A/B test hoặc hiển thị các paywall khác nhau cho các người dùng khác nhau --- # File: adapty-sdk-integration-skill --- --- title: "Tích hợp Adapty vào ứng dụng iOS của bạn với kỹ năng tích hợp SDK" description: "Sử dụng kỹ năng adapty-sdk-integration để tích hợp Adapty SDK vào ứng dụng iOS của bạn từ đầu đến cuối với công cụ lập trình AI của bạn." --- :::important Kỹ năng này đang ở giai đoạn beta. Nếu nó bị treo hoặc hoạt động không như mong đợi, hãy làm theo [hướng dẫn tích hợp từng bước](adapty-cursor) — hướng dẫn này sẽ dẫn công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn form phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt xong, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor --- --- title: "Tích hợp Adapty vào ứng dụng iOS với sự hỗ trợ của AI" description: "Hướng dẫn từng bước tích hợp Adapty vào ứng dụng iOS sử dụng Cursor, Context7, ChatGPT, Claude hoặc các công cụ AI khác." --- Hướng dẫn này sẽ đưa bạn qua từng bước tích hợp Adapty vào ứng dụng iOS với sự hỗ trợ của công cụ lập trình AI — bạn cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ code SDK nào. Bạn có thể thực hiện điều này với kỹ năng LLM tương tác, hoặc thủ công thông qua Dashboard. ### Cách tiếp cận với Skill (khuyến nghị) \{#skill-approach-recommended\} Kỹ năng Adapty CLI cho phép LLM của bạn thiết lập ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng của mình](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm kỹ năng, chạy `/adapty-cli` trong agent của bạn. Nó sẽ hướng dẫn bạn từng bước — bao gồm cả khi nào cần mở Dashboard để kết nối cửa hàng. ### Cách tiếp cận thủ công qua Dashboard \{#dashboard-approach\} Nếu bạn muốn cấu hình mọi thứ thủ công, đây là những gì bạn cần trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard thay cho bạn — bạn cần tự cung cấp chúng. 1. **Kết nối app store của bạn**: Trong Adapty Dashboard, vào **App settings → General**. Đây là yêu cầu bắt buộc để mua hàng hoạt động. [Kết nối App Store](integrate-payments) 2. **Sao chép Public SDK key của bạn**: Trong Adapty Dashboard, vào **App settings → General**, sau đó tìm phần **API keys**. Trong code, đây là chuỗi bạn truyền vào `Adapty.activate("YOUR_PUBLIC_SDK_KEY")`. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu trực tiếp đến sản phẩm trong code — Adapty phân phối chúng thông qua flow hoặc paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo một flow hoặc paywall và một placement**: Trong Adapty Dashboard, tạo một flow (hoặc paywall nếu bạn tự xây dựng giao diện), sau đó gán nó cho một placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty.getFlow("YOUR_PLACEMENT_ID")`. [Tạo flow](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình cho từng sản phẩm trên trang **Products**. Trong code, chuỗi được kiểm tra trong `profile.accessLevels["premium"]`. Mức độ truy cập `premium` mặc định phù hợp với hầu hết ứng dụng. Nếu người dùng trả phí được truy cập vào các tính năng khác nhau tùy theo sản phẩm (ví dụ, gói `basic` so với gói `pro`), hãy [tạo các mức độ truy cập bổ sung](assigning-access-level-to-a-product) trước khi bắt đầu lập trình. :::tip Khi bạn có đủ năm mục này, bạn đã sẵn sàng viết code. Hãy nói với LLM của bạn: "Public SDK key của tôi là X, placement ID của tôi là Y" để nó có thể tạo code khởi tạo và lấy paywall chính xác. ::: ### Thiết lập khi sẵn sàng \{#set-up-when-ready\} Những mục này không bắt buộc để bắt đầu lập trình, nhưng bạn sẽ cần chúng khi tích hợp trưởng thành hơn: - **A/B test**: Cấu hình trên trang **Placements**. Không cần thay đổi code. [A/B test](ab-tests) - **Thêm flow và placement**: Thêm nhiều lệnh gọi `getFlow` với các placement ID khác nhau. - **Tích hợp analytics**: Cấu hình trên trang **Integrations**. Thiết lập khác nhau tùy theo tích hợp. Xem [tích hợp analytics](analytics-integration) và [tích hợp attribution](attribution-integration). ## Cung cấp tài liệu Adapty cho LLM của bạn \{#feed-adapty-docs-to-your-llm\} ### Sử dụng Context7 (khuyến nghị) \{#use-context7-recommended\} [Context7](https://context7.com) là một máy chủ MCP cung cấp cho LLM của bạn quyền truy cập trực tiếp vào tài liệu Adapty cập nhật. LLM của bạn tự động lấy đúng tài liệu dựa trên những gì bạn hỏi — không cần dán URL thủ công. Context7 hoạt động với **Cursor**, **Claude Code**, **Windsurf** và các công cụ tương thích MCP khác. Để thiết lập, chạy: ``` npx ctx7 setup ``` Lệnh này phát hiện trình soạn thảo của bạn và cấu hình máy chủ Context7. Để thiết lập thủ công, xem [kho lưu trữ GitHub của Context7](https://github.com/upstash/context7). Sau khi cấu hình, hãy tham chiếu thư viện Adapty trong các prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the iOS SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn quan trọng. Hãy làm theo [hướng dẫn triển khai](#implementation-walkthrough) bên dưới từng bước để đảm bảo mọi thứ hoạt động đúng. ::: ### Sử dụng tài liệu dạng văn bản thuần \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng văn bản thuần Markdown. Thêm `.md` vào cuối URL của nó, hoặc nhấp vào **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor.md](https://adapty.io/docs/vi/adapty-cursor.md). Mỗi giai đoạn trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới bao gồm một khối "Gửi đến LLM của bạn" với các link `.md` để dán. Để có thêm tài liệu cùng một lúc, xem [tệp chỉ mục và tập hợp con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này đi qua việc tích hợp Adapty theo thứ tự triển khai. Mỗi giai đoạn bao gồm các tài liệu cần gửi cho LLM, những gì bạn sẽ thấy khi hoàn thành và các vấn đề thường gặp. ### Lên kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết code, hãy yêu cầu LLM phân tích dự án của bạn và tạo kế hoạch triển khai. Nếu công cụ AI của bạn hỗ trợ chế độ lập kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy sử dụng nó để LLM có thể đọc cả cấu trúc dự án lẫn tài liệu Adapty trước khi viết code. Hãy cho LLM biết bạn sử dụng cách tiếp cận nào cho việc mua hàng — điều này ảnh hưởng đến các hướng dẫn nó nên tuân theo: - [**Adapty Flow Builder**](adapty-flow-builder): Bạn tạo flow trong trình xây dựng no-code của Adapty, và SDK hiển thị chúng tự động. - [**Paywall tự tạo thủ công**](ios-quickstart-manual): Bạn tự xây dựng giao diện paywall trong code nhưng vẫn dùng Adapty để lấy sản phẩm và xử lý mua hàng. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên cơ sở hạ tầng mua hàng hiện có và chỉ dùng Adapty cho analytics và tích hợp. Chưa biết chọn cái nào? Đọc [bảng so sánh trong quickstart](ios-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Cài đặt gói Adapty SDK qua Swift Package Manager trong Xcode và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — không có gì hoạt động được nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-ios) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-ios.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Ứng dụng build và chạy được. Console Xcode hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra xem bạn đã thay placeholder bằng key thực từ App settings chưa. ::: ### Hiển thị flow hoặc paywall và xử lý mua hàng \{#show-flows-or-paywalls-and-handle-purchases\} Lấy một flow hoặc paywall theo placement ID, hiển thị nó và xử lý các sự kiện mua hàng. Các hướng dẫn bạn cần phụ thuộc vào cách bạn xử lý mua hàng. Hãy kiểm tra từng giao dịch trong sandbox khi bạn tiến hành — đừng đợi đến cuối. Xem [Kiểm tra mua hàng trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. <Tabs groupId="paywall-approach"> <TabItem value="builder" label="Flow Builder" default> **Hướng dẫn:** - [Kích hoạt mua hàng với Flow Builder (quickstart)](ios-quickstart-paywalls) - [Lấy flow và cấu hình của chúng](get-pb-paywalls) - [Hiển thị flow](ios-present-paywalls) - [Xử lý sự kiện flow](ios-handling-events) - [Phản hồi hành động nút](handle-paywall-actions) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-quickstart-paywalls.md - https://adapty.io/docs/vi/get-pb-paywalls.md - https://adapty.io/docs/vi/ios-present-paywalls.md - https://adapty.io/docs/vi/ios-handling-events.md - https://adapty.io/docs/vi/handle-paywall-actions.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Flow hiển thị với các sản phẩm bạn đã cấu hình. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Flow trống hoặc lỗi `getFlow` → xác minh placement ID khớp chính xác với dashboard và placement đã được gán đối tượng. ::: </TabItem> <TabItem value="manual" label="Manual paywalls"> **Hướng dẫn:** - [Kích hoạt mua hàng trong paywall tùy chỉnh của bạn (quickstart)](ios-quickstart-manual) - [Lấy paywall và sản phẩm](fetch-paywalls-and-products) - [Hiển thị paywall được thiết kế bằng remote config](present-remote-config-paywalls) - [Thực hiện mua hàng](making-purchases) - [Khôi phục mua hàng](restore-purchase) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products.md - https://adapty.io/docs/vi/present-remote-config-paywalls.md - https://adapty.io/docs/vi/making-purchases.md - https://adapty.io/docs/vi/restore-purchase.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Paywall tùy chỉnh của bạn hiển thị các sản phẩm được lấy từ Adapty. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Mảng sản phẩm rỗng → xác minh paywall đã được gán sản phẩm trong dashboard và placement đã có đối tượng. ::: </TabItem> <TabItem value="observer" label="Observer mode"> **Hướng dẫn:** - [Tổng quan về Observer mode](observer-vs-full-mode) - [Triển khai Observer mode](implement-observer-mode) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/observer-vs-full-mode.md - https://adapty.io/docs/vi/implement-observer-mode.md - https://adapty.io/docs/vi/report-transactions-observer-mode.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Sau khi mua hàng sandbox bằng flow mua hàng hiện có của bạn, giao dịch xuất hiện trong **Event Feed** trên Adapty dashboard. - **Lưu ý:** Không có sự kiện → xác minh bạn đang báo cáo giao dịch cho Adapty và App Store Server Notifications đã được cấu hình. ::: </TabItem> </Tabs> ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua hàng, kiểm tra hồ sơ người dùng để xem mức độ truy cập đang hoạt động nhằm giới hạn nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](ios-check-subscription-status) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-check-subscription-status.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Sau khi mua hàng sandbox, `profile.accessLevels["premium"]?.isActive` trả về `true`. - **Lưu ý:** `accessLevels` rỗng sau khi mua → kiểm tra xem sản phẩm đã được gán mức độ truy cập trong dashboard chưa. ::: ### Xác định người dùng \{#identify-users\} Liên kết tài khoản người dùng trong ứng dụng của bạn với hồ sơ Adapty để giao dịch mua hàng được duy trì trên nhiều thiết bị. :::important Bỏ qua bước này nếu ứng dụng của bạn không có xác thực. ::: **Hướng dẫn:** [Xác định người dùng](ios-quickstart-identify) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-quickstart-identify.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Sau khi gọi `Adapty.identify("your-user-id")`, phần **Profiles** trên dashboard hiển thị ID người dùng tùy chỉnh của bạn. - **Lưu ý:** Gọi `identify` sau khi kích hoạt nhưng trước khi lấy paywall để tránh attribution hồ sơ ẩn danh. ::: ### Chuẩn bị phát hành \{#prepare-for-release\} Khi tích hợp của bạn hoạt động trong sandbox, hãy xem qua danh sách kiểm tra phát hành để đảm bảo mọi thứ sẵn sàng cho production. **Hướng dẫn:** [Danh sách kiểm tra phát hành](release-checklist) Gửi đến LLM của bạn: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/vi/release-checklist.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Tất cả các mục trong danh sách đã được xác nhận: kết nối cửa hàng, thông báo máy chủ, flow mua hàng, kiểm tra mức độ truy cập và yêu cầu về quyền riêng tư. - **Lưu ý:** Thiếu App Store Server Notifications → cấu hình trong **App settings → iOS SDK** hoặc sự kiện sẽ không xuất hiện trong dashboard. ::: ## Tệp chỉ mục tài liệu văn bản thuần \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM bối cảnh rộng hơn ngoài các trang riêng lẻ, chúng tôi lưu trữ các tệp chỉ mục liệt kê hoặc kết hợp toàn bộ tài liệu Adapty: - [`llms.txt`](https://adapty.io/docs/vi/llms.txt): Liệt kê tất cả các trang với các link `.md`. Đây là [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho các trang web có thể truy cập được bởi LLM. Lưu ý rằng đối với một số AI agent (ví dụ: ChatGPT), bạn sẽ cần tải xuống `llms.txt` và tải nó lên chat dưới dạng tệp. - [`llms-full.txt`](https://adapty.io/docs/vi/llms-full.txt): Toàn bộ tài liệu Adapty được kết hợp thành một tệp duy nhất. Rất lớn — chỉ sử dụng khi bạn cần toàn bộ nội dung. - Tập hợp con dành riêng cho iOS: [`ios-llms.txt`](https://adapty.io/docs/vi/ios-llms.txt) và [`ios-llms-full.txt`](https://adapty.io/docs/vi/ios-llms-full.txt): Tiết kiệm token hơn so với toàn bộ tài liệu. --- # File: get-pb-paywalls --- --- title: "Lấy flows & paywalls - iOS" description: "Tải flows và paywalls từ Adapty trong ứng dụng iOS của bạn." --- <SDKv4> <MethodPromo method="getFlow" /> Sau khi [bạn đã thiết kế flow hoặc paywall trong Paywall Builder](adapty-paywall-builder), bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên là lấy flow hoặc paywall được liên kết với placement và cấu hình view tương ứng như mô tả bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu</summary> 1. [Tạo sản phẩm](create-product) của bạn trong Adapty Dashboard. 2. [Tạo flow/paywall và đưa sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và đưa flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-ios) vào ứng dụng di động của bạn. </details> ## Tải flow/paywall \{#fetch-flowpaywall\} Nếu bạn đã thiết kế một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng mobile để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới trình bày nó trong ứng dụng mobile của bạn. Tải flow hoặc paywall và [cấu hình view](get-pb-paywalls#fetch-the-view-configuration) của nó càng sớm càng tốt — lý tưởng là trước khi hiển thị. Ngay khi bạn tải cấu hình view, SDK bắt đầu tải và cache hình ảnh trong nền. Tải càng sớm, các hình ảnh có càng nhiều thời gian để hoàn tất. Đến khi bạn hiển thị flow hoặc paywall, cấu hình và hình ảnh của nó đã có thể được cache sẵn sàng. Để lấy flow hoặc paywall, sử dụng phương thức `getFlow`: <Tabs> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") // the requested flow/paywall } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow/paywall case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyên dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp tình trạng kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã lưu trong cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ trải nghiệm tốc độ tải nhanh hơn dù kết nối có kém đến đâu. Cache được cập nhật định kỳ, vì vậy việc sử dụng nó trong phiên làm việc để tránh các yêu cầu mạng là hoàn toàn an toàn.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật định kỳ như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản paywall mới nhất trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet yếu.</p> | | **loadTimeout** | mặc định: 5 giây | <p>Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã lưu trong cache hoặc fallback cục bộ sẽ được trả về.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác này có thể bao gồm nhiều yêu cầu khác nhau ở bên dưới.</p> | Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, Remote Config, và cờ `hasViewConfiguration` cho biết flow có bao gồm cấu hình giao diện hay không. Để lấy các sản phẩm thực tế phục vụ việc tải trước, giao diện tùy chỉnh, hoặc kiểm tra theo chương trình, hãy gọi `getPaywallProducts(flow:)`. | ## Lấy cấu hình giao diện \{#fetch-the-view-configuration\} Sau khi lấy flow hoặc paywall, hãy kiểm tra xem nó có bao gồm cấu hình giao diện hay không thông qua `flow.hasViewConfiguration`. Cờ này cho biết cách placement được thiết kế trong Adapty Dashboard: - **`true`** — placement được thiết kế trong **Flow Builder** (một flow) hoặc **Paywall Builder** (một paywall). Adapty sẽ tự động hiển thị giao diện cho bạn. Tiếp tục với các bước bên dưới để lấy cấu hình giao diện và [hiển thị flow hoặc paywall](ios-present-paywalls). - **`false`** — placement là một paywall tùy chỉnh không có giao diện Builder. Sử dụng phương thức `getFlowConfiguration` để tải cấu hình view. ```swift showLineNumbers guard flow.hasViewConfiguration else { // handle as remote config paywall return } let flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow) ``` Các tham số: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------------- | :---------- | | **forFlow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow`. | | **locale** | <p>tùy chọn</p><p>mặc định: `nil`</p> | Mã định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Nhập dưới dạng mã ngôn ngữ với một hoặc hai subtag phân cách bằng `-` (ví dụ: `en`, `pt-br`). Xem [Bản địa hóa và mã locale](localizations-and-locale-codes). | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều request khác nhau bên dưới. | | **products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động fetch các sản phẩm cần thiết. | | **systemRequestsHandler** | tùy chọn | Một đối tượng tuân theo `AdaptySystemRequestsHandler` để xử lý các yêu cầu quyền hệ thống và đánh giá được kích hoạt bởi các hành động trong flow. Chỉ cần thiết nếu flow của bạn có các hành động như vậy. | | **assetsResolver** | tùy chọn | Một dictionary `[String: AdaptyCustomAsset]` dùng để ghi đè hình ảnh và video trong flow/paywall. Xem [Tùy chỉnh assets](#customize-assets). | | **timerResolver** | tùy chọn | Một đối tượng tuân theo `AdaptyTimerResolver` cung cấp ngày kết thúc cho các bộ đếm thời gian do nhà phát triển định nghĩa. Xem [Thiết lập bộ đếm thời gian do nhà phát triển định nghĩa](#set-up-developer-defined-timers). | Sau khi tải xong, [hiển thị flow/paywall](ios-present-paywalls). ## Lấy flow hoặc paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, flow và paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng của bạn có kết nối internet yếu, việc tải flow hoặc paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow hoặc paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getFlowForDefaultAudience`, phương thức này lấy flow hoặc paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow hoặc paywall bằng phương thức `getFlow`, như đã trình bày trong phần [Lấy thông tin Paywall](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng targeting cá nhân hóa (bao gồm dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow hoặc paywall nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã được mô tả [ở trên](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). ::: ```swift showLineNumbers Adapty.getFlowForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow case let .failure(error): // handle the error } } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối mạng có yếu đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall/flow của bạn, hãy triển khai custom assets. Hình ảnh hero và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác nhau cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh xem trước trước khi phát video. - Cung cấp độ phân giải pixel của video để trình phát có thể dành sẵn không gian bố cục (tỷ lệ khung hình = `width / height`) trước khi video tải. Truyền `nil` để bỏ qua phần này. Dưới đây là ví dụ về cách cung cấp các asset tùy chỉnh thông qua một dictionary đơn giản: ```swift showLineNumbers let customAssets: [String: AdaptyCustomAsset] = [ // Show a local image using a custom ID "custom_image": .image( .uiImage(value: UIImage(named: "image_name")!) ), // Show a local preview image while a remote main image is loading "hero_image": .image( .remote( url: URL(string: "https://example.com/image.jpg")!, preview: UIImage(named: "preview_image") ) ), // Show a local video with a preview image and a known resolution "hero_video": .video( .file( url: Bundle.main.url(forResource: "custom_video", withExtension: "mp4")!, preview: .uiImage(value: UIImage(named: "video_preview")!), resolution: CGSize(width: 1080, height: 1920) ) ), ] let flowConfig = try await AdaptyUI.getFlowConfiguration( forFlow: flow, assetsResolver: customAssets ) ``` :::note Nếu không tìm thấy asset, paywall/flow sẽ hiển thị theo giao diện mặc định. ::: ## Thiết lập timer do lập trình viên định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng timer tùy chỉnh trong ứng dụng, hãy tạo một object tuân theo protocol `AdaptyTimerResolver`. Object này định nghĩa cách hiển thị từng timer tùy chỉnh. Nếu muốn, bạn có thể dùng trực tiếp dictionary `[String: Date]` vì nó đã tương thích với protocol này. Dưới đây là ví dụ: ```swift showLineNumbers @MainActor struct AdaptyTimerResolverImpl: AdaptyTimerResolver { func timerEndAtDate(for timerId: String) -> Date { switch timerId { case "CUSTOM_TIMER_6H": Date(timeIntervalSinceNow: 3600.0 * 6.0) // 6 hours case "CUSTOM_TIMER_NY": Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 1)) ?? Date(timeIntervalSinceNow: 3600.0) default: Date(timeIntervalSinceNow: 3600.0) // 1 hour } } } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là các **Timer ID** của các timer do developer tự định nghĩa mà bạn đã thiết lập trong Adapty Dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động từng timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như Năm Mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng 6 giờ bắt đầu từ khi người dùng mở paywall. </SDKv4> <SDKv3> Sau khi [bạn đã thiết kế phần hiển thị cho paywall của mình](adapty-paywall-builder) bằng Paywall Builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động. Bước đầu tiên là lấy paywall liên kết với placement và cấu hình giao diện của nó như mô tả bên dưới. Lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall theo cách thủ công, hãy tham khảo [Lấy paywall và sản phẩm cho paywall Remote Config](fetch-paywalls-and-products). :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, bao gồm toàn bộ thiết lập, từ hiển thị paywall, thực hiện mua hàng đến các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu hiển thị paywall trong ứng dụng di động của bạn</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-ios) vào ứng dụng di động của bạn. </details> ## Tải paywall được thiết kế bằng Paywall Builder \{#fetch-paywall-designed-with-paywall-builder\} Nếu bạn đã [thiết kế paywall bằng Paywall Builder](adapty-paywall-builder), bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới trình bày nó trong ứng dụng. Để đảm bảo hiệu suất tối ưu, hãy lấy paywall và [cấu hình hiển thị](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, nhờ đó ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `getPaywall`: <Tabs> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản dịch paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này được kỳ vọng là mã ngôn ngữ gồm một hoặc hai thẻ con được phân cách bằng ký tự gạch ngang (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>Xem [Bản dịch và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn bất kể kết nối mạng có chập chờn đến đâu. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối mạng yếu.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ trễ hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác này có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------- | | Paywall | Đối tượng [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) chứa danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình hiển thị của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bật toggle **Show on device** trong Paywall Builder. Nếu tùy chọn này chưa được bật, cấu hình hiển thị sẽ không thể lấy được. ::: Sau khi lấy paywall, hãy kiểm tra xem nó có chứa cấu hình hiển thị hay không — đây là dấu hiệu cho thấy paywall được tạo bằng Paywall Builder. Điều này sẽ giúp bạn biết cách hiển thị paywall. Nếu cấu hình hiển thị có mặt, hãy xử lý nó như một paywall Paywall Builder; nếu không, [xử lý nó như một paywall Remote Config](present-remote-config-paywalls). Sử dụng phương thức `getPaywallConfiguration` để tải cấu hình view. ```swift showLineNumbers guard paywall.hasViewConfiguration else { // use your custom logic return } do { let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, products: products ) // use loaded configuration } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------------- | :---------- | | **paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn timeout cho phương thức này. Nếu hết thời gian chờ, dữ liệu được cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều request khác nhau bên dưới. | | **products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền vào `nil`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](localizations-and-locale-codes). ::: Sau khi tải xong, hãy [hiển thị paywall](ios-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, hoặc người dùng đang dùng kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống đó, bạn có thể muốn hiển thị paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ lấy paywall của placement đã chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là phương pháp được khuyến nghị vẫn là lấy paywall qua phương thức `getPaywall`, như đã trình bày chi tiết trong phần [Lấy thông tin Paywall](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (phiên bản hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không hiển thị được. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa mục tiêu (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để có được tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy dùng `getPaywall` đã được mô tả [ở trên](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). ::: ```swift showLineNumbers Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ iOS SDK phiên bản 2.11.2 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản dịch paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con phân cách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Localizations và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong bộ nhớ đệm nếu thất bại. Chúng tôi khuyến nghị lựa chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng kết nối không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu từ bộ nhớ đệm nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối internet kém đến đâu. Bộ nhớ đệm được cập nhật thường xuyên, nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng bộ nhớ đệm vẫn tồn tại sau khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets. Hero image và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm tới các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị ảnh xem trước trước khi phát video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty iOS SDK lên phiên bản 3.7.0 trở lên. ::: Đây là ví dụ về cách bạn có thể cung cấp custom assets thông qua một dictionary đơn giản: ```swift showLineNumbers let customAssets: [String: AdaptyCustomAsset] = [ // Show a local image using a custom ID "custom_image": .image( .uiImage(value: UIImage(named: "image_name")!) ), // Show a local preview image while a remote main image is loading "hero_image": .image( .remote( url: URL(string: "https://example.com/image.jpg")!, preview: UIImage(named: "preview_image") ) ), // Show a local video with a preview image "hero_video": .video( .file( url: Bundle.main.url(forResource: "custom_video", withExtension: "mp4")!, preview: .uiImage(value: UIImage(named: "video_preview")!) ) ), ] let paywallConfig = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, assetsResolver: customAssets ) ``` :::note Nếu không tìm thấy asset, paywall sẽ trở về giao diện mặc định. ::: ## Thiết lập timer do developer định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng custom timer trong ứng dụng mobile, hãy tạo một object tuân theo protocol `AdaptyTimerResolver`. Object này xác định cách mỗi custom timer sẽ được hiển thị. Nếu muốn, bạn có thể dùng trực tiếp dictionary `[String: Date]`, vì nó đã tuân thủ protocol này. Dưới đây là ví dụ: ```swift showLineNumbers @MainActor struct AdaptyTimerResolverImpl: AdaptyTimerResolver { func timerEndAtDate(for timerId: String) -> Date { switch timerId { case "CUSTOM_TIMER_6H": Date(timeIntervalSinceNow: 3600.0 * 6.0) // 6 hours case "CUSTOM_TIMER_NY": Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 1)) ?? Date(timeIntervalSinceNow: 3600.0) default: Date(timeIntervalSinceNow: 3600.0) // 1 hour } } } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là **Timer ID** của các timer do developer tự định nghĩa, được thiết lập trong Adapty Dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động từng timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như ngày Tết Dương lịch. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng 6 giờ bắt đầu từ khi người dùng mở paywall. </SDKv3> --- # File: ios-present-paywalls --- --- title: "Hiển thị flows & paywalls - iOS" description: "Hiển thị flows và paywalls cho người dùng trong ứng dụng iOS của bạn." --- <SDKv4> <MethodPromo method="getFlow" label="Hiển thị flows và paywalls" /> Nếu bạn đã tạo một flow hoặc paywall, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow hoặc paywall đó đã chứa cả nội dung cần hiển thị lẫn cách hiển thị. Để lấy đối tượng `AdaptyUI.FlowConfiguration` dùng bên dưới, xem [Lấy flows và paywalls](get-pb-paywalls). ## Hiển thị flows và paywalls trong SwiftUI \{#present-flows-and-paywalls-in-swiftui\} ### Hiển thị dưới dạng modal view \{#present-as-a-modal-view\} Để hiển thị một flow hoặc paywall trên màn hình thiết bị dưới dạng modal view, hãy dùng modifier `.flow` trong SwiftUI. Lệnh gọi tối thiểu cần có `isPresented`, `flowConfiguration`, và năm callback bắt buộc: ```swift showLineNumbers title="SwiftUI" .flow( isPresented: $flowPresented, flowConfiguration: <AdaptyUI.FlowConfiguration>, didFinishPurchase: { _, _ in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { _, _ in /* handle the error */ }, didFinishRestore: { _ in /* check access level and dismiss */ }, didFailRestore: { _ in /* handle the error */ }, didReceiveError: { _ in flowPresented = false } ) ``` Để kiểm soát nhiều hơn, hãy thêm các callback tùy chọn như `didPerformAction` để xử lý các thao tác nhấn nút: ```swift showLineNumbers title="SwiftUI" @State var flowPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the flow or paywall var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: <AdaptyUI.FlowConfiguration>, didPerformAction: { action in switch action { case .close: flowPresented = false default: // Handle other actions break } }, didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) } ``` (No translatable prose — the input contains only the word "Parameters:" with no content to translate beyond that label.) Parameters: | Tham số | Bắt buộc | Mô tả | |:-----------------------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc hiển thị màn hình flow hoặc paywall. | | **flowConfiguration** | bắt buộc | Một đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Sử dụng phương thức `AdaptyUI.getFlowConfiguration(forFlow:)`. Tham khảo [Lấy flows và paywalls](get-pb-paywalls) để biết thêm chi tiết. | | **didFinishPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` hoàn thành thành công. Flow không tự động đóng — đặt binding hiển thị của bạn thành `false` ở đây, hoặc không làm gì để flow tiếp tục sau khi mua. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn thành thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didReceiveError** | bắt buộc | Được gọi khi có lỗi render hoặc lỗi runtime từ script của flow (ví dụ: ngoại lệ JavaScript, mã `AdaptyUIError` `4105`). Đối với lỗi render, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định xem flow hoặc paywall hiển thị ở chế độ toàn màn hình hay dạng sheet. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của flow hoặc paywall được hiển thị. | | **didDisappear** | tùy chọn | Được gọi khi view của flow hoặc paywall bị đóng. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Hai action ID được định nghĩa sẵn: `close` và `openURL`; các ID khác là tùy chỉnh và có thể được đặt trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web kết thúc. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi có lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các alert item phía trên flow hoặc paywall. | | **showAlertBuilder** | tùy chọn | Một hàm để render view alert. | | **placeholderBuilder** | tùy chọn | Một hàm để render view placeholder trong khi flow hoặc paywall đang tải. Mặc định là `ProgressView`. | Tham khảo chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết thêm chi tiết về các tham số. ### Hiển thị dưới dạng non-modal view \{#present-as-a-non-modal-view\} Bạn cũng có thể hiển thị flow và paywall dưới dạng điểm đến trong navigation hoặc inline view trong flow điều hướng của ứng dụng. Dùng `AdaptyFlowView` trực tiếp trong các SwiftUI view của bạn: ```swift showLineNumbers title="SwiftUI" AdaptyFlowView( flowConfiguration: <AdaptyUI.FlowConfiguration>, didFinishPurchase: { product, purchaseResult in // Dismiss the view, or do nothing to let the flow continue }, didFailPurchase: { product, error in // Handle purchase failure }, didFinishRestore: { profile in // Handle successful restore }, didFailRestore: { error in // Handle restore failure }, didReceiveError: { error in // Handle the error (rendering or JS exception from the flow script). } ) ``` ## Hiển thị flow và paywall trong UIKit \{#present-flows-and-paywalls-in-uikit\} Để hiển thị flow hoặc paywall trên màn hình thiết bị, thực hiện các bước sau: 1. Khởi tạo visual flow bạn muốn hiển thị bằng cách sử dụng phương thức `AdaptyUI.flowController(with:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualFlow = try AdaptyUI.flowController( with: <AdaptyUI.FlowConfiguration>, delegate: <AdaptyFlowControllerDelegate> ) ``` Các tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :---------- | | **flowConfiguration** | required | Một đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Sử dụng phương thức `AdaptyUI.getFlowConfiguration(forFlow:)`. Tham khảo chủ đề [Lấy flow và paywall](get-pb-paywalls) để biết thêm chi tiết. | | **delegate** | required | Một `AdaptyFlowControllerDelegate` để lắng nghe các sự kiện của flow và paywall. Tham khảo chủ đề [Xử lý sự kiện flow & paywall](ios-handling-events) để biết thêm chi tiết. | Trả về: | Object | Mô tả | | :---------------------- | :------------------------------------------------------- | | **AdaptyFlowController** | Đối tượng đại diện cho flow hoặc màn hình paywall được yêu cầu. | 2. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó lên màn hình thiết bị: ```swift showLineNumbers title="Swift" present(visualFlow, animated: true) ``` :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, 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. ::: </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Paywall như vậy đã chứa cả nội dung cần hiển thị lẫn cách thức hiển thị. Để lấy đối tượng `AdaptyUI.PaywallConfiguration` được sử dụng bên dưới, hãy xem [Tải paywall từ Paywall Builder và cấu hình của chúng](get-pb-paywalls). ## Hiển thị paywalls trong SwiftUI \{#present-paywalls-in-swiftui\} ### Hiển thị dưới dạng modal view \{#present-as-a-modal-view\} Để hiển thị paywall trực quan trên màn hình thiết bị dưới dạng modal view, hãy dùng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the paywall var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration: <AdaptyUI.PaywallConfiguration>, didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishPurchase: { product, profile in paywallPresented = false }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } ) } ``` (No translatable prose — output unchanged.) | Tham số | Bắt buộc | Mô tả | |:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc màn hình paywall có được hiển thị hay không. | | **paywallConfiguration** | bắt buộc | Một đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin trực quan của paywall. Sử dụng phương thức `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn thành thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didFailRendering** | bắt buộc | Được gọi nếu xảy ra lỗi trong quá trình render giao diện. Trong trường hợp này, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định paywall hiển thị ở chế độ toàn màn hình hay dạng modal. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi màn hình paywall được hiển thị. | | **didDisappear** | tùy chọn | Được gọi khi màn hình paywall bị đóng. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Các nút khác nhau có các ID hành động khác nhau. Hai ID hành động được định nghĩa sẵn là: `close` và `openURL`, còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi một sản phẩm được chọn để mua (bởi người dùng hoặc hệ thống). | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi `Adapty.makePurchase()` hoàn thành thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web kết thúc. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi xảy ra lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm chỉ được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các mục cảnh báo phía trên paywall. | | **showAlertBuilder** | tùy chọn | Một hàm để render màn hình cảnh báo. | | **placeholderBuilder** | tùy chọn | Một hàm để render màn hình placeholder trong khi paywall đang tải. | Tham khảo phần [iOS - Xử lý sự kiện](ios-handling-events) để biết thêm chi tiết về các tham số. ### Hiển thị dưới dạng non-modal view \{#present-as-a-non-modal-view\} Bạn cũng có thể hiển thị paywall dưới dạng điểm đến trong navigation hoặc view inline trong flow điều hướng của ứng dụng. Sử dụng `AdaptyPaywallView` trực tiếp trong các SwiftUI view của bạn: ```swift showLineNumbers title="SwiftUI" AdaptyPaywallView( paywallConfiguration: <AdaptyUI.PaywallConfiguration>, didFailPurchase: { product, error in // Handle purchase failure }, didFinishRestore: { profile in // Handle successful restore }, didFailRestore: { error in // Handle restore failure }, didFailRendering: { error in // Handle rendering error } ) ``` ## Hiển thị paywall trong UIKit \{#present-paywalls-in-uikit\} Để hiển thị paywall trực quan trên màn hình thiết bị, hãy làm theo các bước sau: 1. Khởi tạo paywall trực quan bạn muốn hiển thị bằng cách sử dụng phương thức `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: <paywall configuration object>, delegate: <AdaptyPaywallControllerDelegate> ) ``` Các tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :---------- | | **paywall configuration** | required | Một đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Tham khảo chủ đề [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) để biết thêm chi tiết. | | **delegate** | required | Một `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện của paywall. Tham khảo chủ đề [Handling paywall events](ios-handling-events) để biết thêm chi tiết. | Trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | **AdaptyPaywallController** | Một đối tượng đại diện cho màn hình paywall được yêu cầu | 2. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó lên màn hình thiết bị: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi trình bày 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. ::: </SDKv3> --- # File: handle-paywall-actions --- --- title: "Phản hồi các hành động flow - iOS" description: "Xử lý các hành động nút bấm và xử lý đầu vào của người dùng từ các flow paywall và onboarding trong ứng dụng iOS của bạn." --- <SDKv4> Nếu bạn đang xây dựng các flow hoặc paywall bằng Adapty Flow Builder hoặc Paywall Builder, việc thiết lập các nút bấm đúng cách là rất quan trọng: 1. Thêm [nút trong paywall builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một action ID tùy chỉnh. 2. Viết code trong ứng dụng để xử lý từng hành động bạn đã gán. Hướng dẫn này trình bày cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn. :::warning **Chỉ việc đóng flow/paywall và mở URL được xử lý tự động.** Tất cả các hành động nút khác đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: :::note iOS SDK có thể phản hồi các yêu cầu quyền hệ thống, chẳng hạn như thông báo đẩy hoặc quyền truy cập camera, thông qua `AdaptySystemRequestsHandler`. Flows chưa kích hoạt các yêu cầu này, vì vậy bạn chưa cần xử lý chúng. ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall: 1. Trong builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai handler cho hành động `close`. :::info Trong iOS SDK, hành động `close` mặc định sẽ kích hoạt việc đóng flow hoặc paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ: đóng một flow có thể kích hoạt mở một flow khác. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false // dismiss the flow or paywall default: break } }, didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) ``` ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục mua hàng), hãy thêm phần tử **Link** trong builder và xử lý nó giống như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết từ flow hoặc paywall (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng, triển khai một handler cho hành động `openURL` để mở URL nhận được trong trình duyệt. :::info Trong iOS SDK, hành động `openURL` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case let .openURL(url): UIApplication.shared.open(url, options: [:]) // default behavior default: break } }, didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) ``` ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong builder, thêm một nút, gán cho nó hành động **Custom**, và gán cho nó một ID. 2. Trong code ứng dụng của bạn, implement một handler cho action ID bạn đã tạo. Ví dụ: nếu bạn có thêm một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút để hiển thị một flow hoặc paywall khác: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case let .custom(id): if id == "openNewPaywall" { // Display another flow or paywall } default: break } }, didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) ``` </SDKv4> <SDKv3> Nếu bạn đang xây dựng paywall bằng Adapty paywall builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm [nút trong paywall builder](paywall-buttons) và gán cho nó một action có sẵn hoặc tạo một custom action ID. 2. Viết code trong ứng dụng để xử lý từng action bạn đã gán. Hướng dẫn này hướng dẫn cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Chỉ có mua hàng, khôi phục, đóng paywall và mở URL được xử lý tự động.** Tất cả các action nút khác đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai handler cho hành động `close` để đóng paywall. :::info Trong iOS SDK, hành động `close` mặc định sẽ tự động đóng paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một paywall có thể kích hoạt mở paywall khác. ::: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case .close: controller.dismiss(animated: true) // default behavior break } } ``` ## Mở URL từ paywall \{#open-urls-from-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch), hãy thêm phần tử **Link** trong paywall builder và xử lý nó theo cách tương tự như các nút có hành động **Open URL**. ::: Để thêm nút mở một liên kết từ paywall (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong paywall builder, thêm một nút, gán cho nó hành động **Open URL** và nhập URL bạn muốn mở. 2. Trong code của ứng dụng, triển khai handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong iOS SDK, hành động `openUrl` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .openURL(url): UIApplication.shared.open(url, options: [:]) // default behavior break } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút đăng nhập cho người dùng vào ứng dụng của bạn: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Login**. 2. Trong code ứng dụng của bạn, triển khai một handler cho hành động `login` để xác định người dùng. ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case .login: // Show a login screen let loginVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LoginViewController") controller.present(loginVC, animated: true) } } ``` ## Xử lý hành động tùy chỉnh \{#handle-custom-actions\} Để thêm nút xử lý các hành động khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và gán cho nó một ID. 2. Trong code ứng dụng của bạn, implement handler cho ID hành động bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm nút để hiển thị một paywall khác: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .custom(id): if id == "openNewPaywall" { // Display another paywall } } break } } ``` </SDKv3> --- # File: ios-handling-events --- --- title: "Xử lý sự kiện flow & paywall - iOS" description: "Xử lý các sự kiện flow và paywall trong ứng dụng iOS của bạn." --- <SDKv4> :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút bấm (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](handle-paywall-actions) của chúng tôi để biết thêm chi tiết. ::: Flow và paywall không cần thêm code để thực hiện hoặc khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm thao tác nhấn nút (nút đóng, URL, lựa chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua. Tìm hiểu cách phản hồi các sự kiện này bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, bao gồm đầy đủ thiết lập, từ hiển thị paywall, thực hiện giao dịch mua cho đến các chức năng cơ bản khác. ::: ## Xử lý sự kiện trong SwiftUI \{#handling-events-in-swiftui\} Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình flow hoặc paywall trong ứng dụng di động của bạn, hãy sử dụng modifier `.flow` trong SwiftUI: ```swift showLineNumbers title="Swift" @State var flowPresented = false var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false case let .openURL(url): // handle opening the URL (incl. for terms and privacy) default: // handle other actions } }, didSelectProduct: { product in /* Handle the event */ }, didStartPurchase: { product in /* Handle the event */ }, didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didStartRestore: { /* Handle the event */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false }, didFailLoadingProducts: { error in // Return `true` to retry loading return false } ) } ``` Bạn chỉ cần đăng ký những tham số closure cần dùng và bỏ qua những tham số không cần thiết. | Tham số | Bắt buộc | Mô tả | |:-----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc hiển thị màn hình flow hoặc paywall. | | **flowConfiguration** | bắt buộc | Một đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Tham khảo [Lấy flow và paywall](get-pb-paywalls) để biết thêm chi tiết. | | **didFinishPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` hoàn tất thành công. Flow không tự động đóng — đặt binding hiển thị của bạn thành `false` tại đây, hoặc không làm gì để flow tiếp tục sau khi mua hàng. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn tất thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didReceiveError** | bắt buộc | Được gọi khi flow gặp lỗi hiển thị hoặc lỗi runtime từ script của flow (ví dụ: ngoại lệ JavaScript, mã `AdaptyUIError` `4105`). Trong trường hợp lỗi hiển thị, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **placeholderBuilder** | tùy chọn | Hàm dùng để hiển thị view placeholder trong khi flow hoặc paywall đang tải. Mặc định là `ProgressView`. | | **fullScreen** | tùy chọn | Xác định flow hoặc paywall hiển thị ở chế độ toàn màn hình hay dạng sheet. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của flow hoặc paywall xuất hiện trên màn hình. | | **didDisappear** | tùy chọn | Được gọi khi view của flow hoặc paywall bị đóng lại. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn vào một nút. Hai action ID được định nghĩa sẵn là `close` và `openURL`; các ID khác là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web hoàn tất. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi xảy ra lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các alert item phía trên flow hoặc paywall. | | **showAlertBuilder** | tùy chọn | Hàm dùng để hiển thị view cảnh báo. | ## Xử lý sự kiện trong UIKit \{#handling-events-in-uikit\} Đối với các ứng dụng UIKit, sự kiện được xử lý thông qua giao thức `AdaptyFlowControllerDelegate`. Xem [Hiển thị flow & paywall - iOS](ios-present-paywalls) để biết cách thiết lập `AdaptyFlowController` với `AdaptyFlowControllerDelegate`. Giao thức khai báo 13 phương thức. Bốn phương thức không có cài đặt mặc định và bắt buộc phải triển khai khi conform: `didFinishPurchase`, `didFailPurchase`, `didFinishRestoreWith`, và `didFailRestoreWith`. Các phương thức còn lại có cài đặt mặc định no-op và có thể override khi bạn muốn tùy chỉnh hành vi. Các phương thức được nhóm theo mục đích dưới đây. ### Vòng đời \{#lifecycle\} ```swift showLineNumbers title="Swift" func flowControllerDidAppear(_ controller: AdaptyFlowController) { } func flowControllerDidDisappear(_ controller: AdaptyFlowController) { } ``` Các hàm này được gọi khi flow hoặc màn hình paywall được hiển thị và bị đóng lại. ### Hành động của người dùng \{#user-actions\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didPerform action: AdaptyUI.Action ) { } ``` Các trường hợp của `AdaptyUI.Action`: - `.close` — hành vi mặc định là đóng controller. Ghi đè nếu bạn muốn giữ controller trên màn hình hoặc thực hiện thêm thao tác dọn dẹp. - `.openURL(url:)` — hành vi mặc định là mở URL bằng `UIApplication.shared.open(...)`. - `.custom(id:)` — được kích hoạt cho các nút có ID hành động tùy chỉnh được thiết lập trong builder. ### Chọn sản phẩm \{#product-selection\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didSelectProduct product: AdaptyPaywallProduct ) { } ``` Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. Sản phẩm mang đầy đủ thông tin ưu đãi (tính đủ điều kiện được xác định tự động trong v4 — không còn kiểu `AdaptyPaywallProductWithoutDeterminingOffer` riêng biệt nữa). ### Sự kiện mua hàng \{#purchase-events\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didStartPurchase product: AdaptyPaywallProduct ) { } func flowController( _ controller: AdaptyFlowController, didFinishPurchase product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { if !purchaseResult.isPurchaseCancelled { controller.dismiss(animated: true) // or do nothing to let the flow continue } } func flowController( _ controller: AdaptyFlowController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError ) { } ``` `didFinishPurchase` và `didFailPurchase` không có cài đặt mặc định và bắt buộc phải được triển khai. Controller sẽ không tự đóng sau khi mua hàng thành công — hãy gọi `controller.dismiss(animated:)` khi phù hợp, hoặc không làm gì để cho phép flow nhiều màn hình tiếp tục sau khi mua hàng. ### Sự kiện khôi phục \{#restore-events\} ```swift showLineNumbers title="Swift" func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { } func flowController( _ controller: AdaptyFlowController, didFinishRestoreWith profile: AdaptyProfile ) { } func flowController( _ controller: AdaptyFlowController, didFailRestoreWith error: AdaptyError ) { } ``` `didFinishRestoreWith` và `didFailRestoreWith` không có implementation mặc định. Hãy kiểm tra xem `AdaptyProfile` trả về có chứa mức độ truy cập mong muốn hay không trước khi dismiss controller. ### Lỗi flow và lỗi tải sản phẩm \{#flow-errors-and-product-loading-errors\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didReceiveError error: AdaptyUIError ) { } func flowController( _ controller: AdaptyFlowController, didFailLoadingProductsWith error: AdaptyError ) -> Bool { // Return `true` to retry product loading; default returns `false`. return false } func flowController( _ controller: AdaptyFlowController, didPartiallyLoadProducts failedIds: [String] ) { } ``` `didReceiveError` kích hoạt khi có lỗi render và lỗi runtime từ script của flow (ngoại lệ JavaScript, mã `AdaptyUIError` `4105`). Với lỗi render, hãy [liên hệ Adapty Support](mailto:support@adapty.io). Với lỗi tải, trả về `true` từ `didFailLoadingProductsWith` để thử lại — hữu ích khi gặp sự cố mạng tạm thời. ### Điều hướng thanh toán web \{#web-payment-navigation\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didFinishWebPaymentNavigation product: AdaptyPaywallProduct?, error: AdaptyError? ) { } ``` Được gọi sau khi điều hướng thanh toán web kết thúc, dù thành công hay thất bại. </SDKv4> <SDKv3> :::important Hướng dẫn này đề cập đến việc xử lý các sự kiện mua hàng, khôi phục, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút bấm (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](handle-paywall-actions) để biết thêm chi tiết. ::: Paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm thao tác nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện trên paywall. Hãy tham khảo bên dưới để biết cách phản hồi các sự kiện này. Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** yêu cầu Adapty SDK v3.0 trở lên. :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Xử lý sự kiện trong SwiftUI \{#handling-events-in-swiftui\} Để kiểm soát hoặc theo dõi các quá trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy sử dụng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="Swift" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: paywall, viewConfiguration: viewConfig, didPerformAction: { action in switch action { case .close: paywallPresented = false case let .openURL(url): // handle opening the URL (incl. for terms and privacy) default: // handle other actions } }, didSelectProduct: { /* Handle the event */ }, didStartPurchase: { /* Handle the event */ }, didFinishPurchase: { product, info in /* Handle the event */ }, didFailPurchase: { product, error in /* Handle the event */ }, didStartRestore: { /* Handle the event */ }, didFinishRestore: { /* Handle the event */ }, didFailRestore: { /* Handle the event */ }, didFailRendering: { error in paywallPresented = false }, didFailLoadingProducts: { error in return false } ) } ``` Bạn chỉ cần đăng ký những tham số closure mà bạn cần, và bỏ qua những tham số không cần thiết. Trong trường hợp này, các tham số closure không được sử dụng sẽ không được tạo ra. | Tham số | Bắt buộc | Mô tả | |:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc màn hình paywall có được hiển thị hay không. | | **paywallConfiguration** | bắt buộc | Một đối tượng `AdaptyUI.PaywallConfiguration` chứa các chi tiết hiển thị của paywall. Sử dụng phương thức `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)`. Tham khảo chủ đề [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi giao dịch mua thất bại do lỗi (ví dụ: thanh toán không được phép, sự cố mạng, sản phẩm không hợp lệ). Không được gọi khi người dùng hủy hoặc thanh toán đang chờ xử lý. | | **didFinishRestore** | bắt buộc | Được gọi khi giao dịch mua hoàn tất thành công. | | **didFailRestore** | bắt buộc | Được gọi khi khôi phục giao dịch mua thất bại. | | **didFailRendering** | bắt buộc | Được gọi nếu xảy ra lỗi trong quá trình render giao diện. Trong trường hợp này, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định paywall hiển thị ở chế độ toàn màn hình hay dạng modal. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view paywall xuất hiện trên màn hình. Cũng được gọi khi người dùng nhấn [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall và web paywall mở trong trình duyệt in-app. | | **didDisappear** | tùy chọn | Được gọi khi view paywall bị đóng. Cũng được gọi khi [web paywall](web-paywall#step-2a-add-a-web-purchase-button) được mở từ một paywall trong trình duyệt in-app biến mất khỏi màn hình. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Các nút khác nhau có ID hành động khác nhau. Hai ID hành động được định nghĩa sẵn là `close` và `openURL`, các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi một sản phẩm được chọn để mua (do người dùng hoặc hệ thống). | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi giao dịch mua hoàn tất thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi sau khi thử mở [web paywall](web-paywall) để mua hàng, dù thành công hay thất bại. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi xảy ra lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các mục cảnh báo phía trên paywall. | | **showAlertBuilder** | tùy chọn | Một hàm để render view cảnh báo. | | **placeholderBuilder** | tùy chọn | Một hàm để render view placeholder trong khi paywall đang tải. | ## Xử lý sự kiện trong UIKit \{#handling-events-in-uikit\} Để kiểm soát hoặc theo dõi các tiến trình diễn ra trên màn hình paywall trong ứng dụng của bạn, hãy triển khai các phương thức `AdaptyPaywallControllerDelegate`. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### Chọn sản phẩm \{#product-selection\} Khi người dùng chọn một sản phẩm để mua, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ``` </Details> #### Bắt đầu mua hàng \{#started-purchase\} Nếu người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didStartPurchase product: AdaptyPaywallProduct) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ``` </Details> Sự kiện này sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [iOS - Hiển thị paywall Paywall Builder trong chế độ Observer](ios-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Bắt đầu mua hàng bằng web paywall \{#started-purchase-using-a-web-paywall\} Nếu người dùng khởi tạo quá trình mua hàng bằng cách sử dụng [web paywall](web-paywall), phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, shouldContinueWebPaymentNavigation product: AdaptyPaywallProduct ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ``` </Details> #### Mua hàng thành công hoặc đã hủy \{#successful-or-canceled-purchase\} Nếu mua hàng thành công, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFinishPurchase product: AdaptyPaywallProductWithoutDeterminingOffer, purchaseResult: AdaptyPurchaseResult ) { } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Cancelled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "cancelled" } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình paywall trong trường hợp đó. Tính năng này sẽ không được gọi trong chế độ Observer. Xem thêm tại [iOS - Hiển thị paywall Paywall Builder trong chế độ Observer](ios-present-paywall-builder-paywalls-in-observer-mode) để biết chi tiết. #### Mua hàng thất bại \{#failed-purchase\} Nếu một giao dịch mua thất bại do lỗi, phương thức này sẽ được gọi. Điều này bao gồm các lỗi StoreKit (hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng), lỗi xác minh giao dịch và lỗi hệ thống. Lưu ý rằng khi người dùng hủy, `didFinishPurchase` sẽ được gọi với kết quả đã hủy thay thế, và các thanh toán đang chờ xử lý không kích hoạt phương thức này. ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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" } } } ``` </Details> Nó sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Mua hàng thất bại khi dùng web paywall \{#failed-purchase-using-a-web-paywall\} Nếu `Adapty.openWebPaywall()` thất bại, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFailWebPaymentNavigation product: AdaptyPaywallProduct, error: AdaptyError ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "web_payment_failed", "message": "Web payment navigation failed", "details": { "underlyingError": "Network connection error" } } } ``` </Details> #### Khôi phục thành công \{#successful-restore\} Nếu việc khôi phục giao dịch thành công, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFinishRestoreWith profile: AdaptyProfile ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```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" } ] } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` yêu cầu. Tham khảo chủ đề [Trạng thái gói đăng ký](subscription-status) để biết cách kiểm tra. #### Khôi phục thất bại \{#failed-restore\} Nếu việc khôi phục giao dịch thất bại, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailRestoreWith error: AdaptyError ) { } ``` <Details> <summary>Ví dụ về sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ``` </Details> ### Tải dữ liệu và hiển thị \{#data-fetching-and-rendering\} #### Lỗi tải sản phẩm \{#product-loading-errors\} Nếu bạn không truyền mảng sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ server. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức sau: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailLoadingProductsWith error: AdaptyError ) -> Bool { return true } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ``` </Details> Nếu bạn trả về `true`, AdaptyUI sẽ lặp lại yêu cầu sau 2 giây. #### Lỗi khi render \{#rendering-errors\} Nếu có lỗi xảy ra trong quá trình render giao diện, lỗi đó sẽ được báo cáo qua phương thức này: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailRenderingWith error: AdaptyError ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ``` </Details> Trong điều kiện bình thường, các lỗi này không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết. </SDKv3> --- # File: ios-use-fallback-paywalls --- --- title: "iOS - Sử dụng fallbacks" description: "Xử lý các trường hợp người dùng ngoại tuyến hoặc máy chủ Adapty không khả dụng" --- Để duy trì trải nghiệm người dùng mượt mà, điều quan trọng là phải thiết lập [paywall dự phòng](/fallback-paywalls) cho các flow, [paywall](paywalls) và [onboarding](onboardings) của bạn. Biện pháp phòng ngừa này giúp mở rộng khả năng của ứng dụng trong trường hợp mất kết nối internet một phần hoặc hoàn toàn. * **Nếu ứng dụng không thể kết nối đến máy chủ Adapty:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng, và truy cập cấu hình onboarding đã lưu cục bộ. * **Nếu ứng dụng không thể kết nối internet:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng. Onboarding chứa nội dung từ xa và cần có kết nối internet để hoạt động. :::important Trước khi thực hiện các bước trong hướng dẫn này, hãy [tải xuống](/local-fallback-paywalls) các file cấu hình dự phòng từ Adapty. ::: ## Cấu hình \{#configuration\} 1. Thêm file JSON fallback vào bundle dự án của bạn: mở menu **File** trong XCode và chọn tùy chọn **Add Files to "YourProjectName"**. 2. Gọi phương thức `.setFallback` **trước khi** bạn tải flow, paywall, hoặc onboarding mục tiêu. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { if let urlPath = Bundle.main.url(forResource: fileName, withExtension: "json") { try await Adapty.setFallback(fileURL: urlPath) } } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers if let url = Bundle.main.url(forResource: "ios_fallback", withExtension: "json") { Adapty.setFallback(fileURL: url) } ``` </TabItem> </Tabs> Tham số: | Tham số | Mô tả | | :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **fileURL** | Đường dẫn đến file cấu hình fallback. | --- # File: localizations-and-locale-codes --- --- title: "Sử dụng localizations và locale codes trong iOS SDK" description: "Quản lý app localizations và locale codes để tiếp cận người dùng toàn cầu trong ứng dụng iOS của bạn." --- ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một vài tình huống mà locale codes phát huy tác dụng — ví dụ, khi bạn muốn lấy đúng paywall cho localization hiện tại của ứng dụng. Vì locale codes khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng localization — từ đó luôn nhận được kết quả như mong muốn. ## Tiêu chuẩn locale code tại Adapty \{#locale-code-standard-at-adapty\} Adapty sử dụng phiên bản được điều chỉnh nhẹ của [chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi code gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Đối khớp locale code \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client kèm theo locale code và bắt đầu tìm localization tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi locale đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay bằng dấu gạch ngang (`-`) 2. Hệ thống tìm kiếm localization có locale code khớp hoàn toàn 3. Nếu không tìm thấy, hệ thống lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tìm localization phù hợp 4. Nếu vẫn không tìm thấy, hệ thống trả về localization mặc định `en` Nhờ vậy, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` đều nhận được cùng một kết quả. ## Triển khai localizations: cách được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang quan tâm đến localizations, nhiều khả năng bạn đã làm việc với các file localized string trong dự án. Nếu vậy, chúng tôi khuyến nghị đặt một cặp key-value chứa Adapty locale code tương ứng vào từng file localization. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như sau: ```swift showLineNumbers // 1. Modify your Localizable.strings files /* Localizable.strings - Spanish */ adapty_paywalls_locale = "es"; /* Localizable.strings - Portuguese (Brazil) */ adapty_paywalls_locale = "pt-br"; // 2. Extract and use the locale code let locale = NSLocalizedString("adapty_paywalls_locale", comment: "") // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Cách này giúp bạn kiểm soát hoàn toàn localization nào sẽ được lấy cho từng người dùng của ứng dụng. ## Triển khai localizations: cách khác \{#implementing-localizations-the-other-way\} Bạn có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống) mà không cần định nghĩa rõ locale code cho từng localization. Điều đó có nghĩa là trích xuất locale code từ các đối tượng khác mà nền tảng cung cấp, như thế này: ```swift showLineNumbers let locale = Locale.current.identifier // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Lưu ý rằng chúng tôi không khuyến nghị cách này vì một số lý do: 1. Trên iOS, ngôn ngữ ưa thích và locale hiện tại không giống nhau. Nếu muốn localization được chọn đúng, bạn phải hoặc dựa vào logic của Apple — vốn hoạt động tốt nếu bạn dùng cách được khuyến nghị với các file localized string — hoặc tự tái tạo lại logic đó. 2. Khó dự đoán chính xác server của Adapty sẽ nhận được gì. Ví dụ, trên iOS, có thể thu được locale như `ar_OM@numbers='latn'` trên thiết bị và gửi lên server. Với lệnh gọi này, bạn sẽ không nhận được localization `ar-om` như mong muốn, mà thay vào đó là `ar` — điều này có thể nằm ngoài dự tính. Nếu bạn vẫn quyết định dùng cách này — hãy đảm bảo đã xử lý hết tất cả các trường hợp liên quan. --- # File: ios-troubleshoot-paywall-builder --- --- title: "Khắc phục sự cố Paywall Builder trong iOS SDK" description: "Khắc phục sự cố Paywall Builder trong iOS SDK" --- Hướng dẫn này giúp bạn giải quyết các sự cố thường gặp khi sử dụng paywall được thiết kế trong Adapty Paywall Builder trên iOS SDK. ## Lấy cấu hình paywall thất bại \{#getting-a-paywall-configuration-fails\} **Sự cố**: Phương thức `getPaywallConfiguration` không thể lấy cấu hình paywall. **Nguyên nhân**: Paywall chưa được bật để hiển thị trên thiết bị trong Paywall Builder. **Giải pháp**: Bật toggle **Show on device** trong Paywall Builder. <img src="/assets/shared/img/show-on-device.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Số lượt xem paywall quá lớn \{#the-paywall-view-number-is-too-big\} **Sự cố**: Số lượt xem paywall hiển thị gấp đôi so với mong đợi. **Nguyên nhân**: Bạn có thể đang gọi `logShowFlow` (iOS SDK v4+) / `logShowPaywall` trong code, khiến số lượt xem bị tính hai lần nếu bạn đang dùng Paywall Builder hoặc Flow Builder. Với các flow và paywall được xây dựng bằng những công cụ này, analytics được theo dõi tự động, nên bạn không cần gọi phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowFlow` (iOS SDK v4+) / `logShowPaywall` trong code nếu đang sử dụng Paywall Builder hoặc Flow Builder. ## Các sự cố khác \{#other-issues\} **Sự cố**: Bạn gặp phải các vấn đề liên quan đến Paywall Builder khác không được đề cập ở trên. **Giải pháp**: Migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](ios-sdk-migration-guides) nếu cần. Nhiều sự cố đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: ios-present-paywall-builder-paywalls-in-observer-mode --- --- title: "Hiển thị paywall Paywall Builder trong Observer mode trên iOS SDK" description: "Tìm hiểu cách hiển thị paywall PB trong observer mode để có thêm thông tin chi tiết." --- Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách thức hiển thị. :::warning Phần này chỉ áp dụng cho [chế độ Observer](observer-vs-full-mode). Nếu bạn không làm việc ở chế độ Observer, hãy xem [iOS - Hiển thị paywall từ Paywall Builder](ios-present-paywalls). ::: <SDKv4> <details> <summary>Trước khi bắt đầu hiển thị flow (Nhấn để mở rộng)</summary> 1. Thiết lập tích hợp ban đầu của Adapty [với App Store](initial_ios). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo [hướng dẫn cài đặt iOS SDK](sdk-installation-ios#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình flow hoặc paywall trong các builder](create-paywall) và gán sản phẩm vào chúng. 5. [Tạo placement và gán flow hoặc paywall vào chúng](create-placement). 6. [Lấy flow và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng di động của bạn. </details> <p> </p> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> 1. Triển khai đối tượng `AdaptyObserverModeResolver`. Giao thức này giống như trong SDK v3 — bản thân observer mode không thay đổi giữa flow và paywall rendering: ```swift showLineNumbers title="Swift" func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // call onStartPurchase / onFinishPurchase to notify AdaptyUI about the purchase progress } func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, onFinishRestore: @escaping () -> Void) { // call onStartRestore / onFinishRestore to notify AdaptyUI about the restore progress } ``` 2. Tạo đối tượng cấu hình flow, truyền resolver của bạn vào tham số `observerModeResolver:`: ```swift showLineNumbers title="Swift" do { let flowConfiguration = try await AdaptyUI.getFlowConfiguration( forFlow: flow, observerModeResolver: <AdaptyObserverModeResolver> ) } catch { // handle the error } ``` Tham số của request: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------- | | **forFlow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow(placementId:)`. Xem [Lấy flow và paywall](get-pb-paywalls). | | **observerModeResolver** | bắt buộc | `AdaptyObserverModeResolver` mà bạn đã triển khai ở trên. | 3. Khởi tạo flow controller bằng `AdaptyUI.flowController(with:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualFlow = try AdaptyUI.flowController( with: flowConfiguration, delegate: <AdaptyFlowControllerDelegate> ) ``` Tham số yêu cầu: | Parameter | Presence | Description | | :------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | | **flowConfiguration** | bắt buộc | Một đối tượng `AdaptyUI.FlowConfiguration` chứa các chi tiết hiển thị của flow. Xem [Lấy flows và paywalls](get-pb-paywalls). | | **delegate** | bắt buộc | Một `AdaptyFlowControllerDelegate` để lắng nghe các sự kiện của flow. Xem [Xử lý sự kiện flow & paywall](ios-handling-events). | Trả về: | Đối tượng | Mô tả | | :------------------- | :----------------------------------------------------- | | AdaptyFlowController | Một đối tượng đại diện cho màn hình flow được yêu cầu. | 4. Hiển thị controller: ```swift showLineNumbers title="Swift" present(visualFlow, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua đó. ::: </TabItem> <TabItem value="swiftui" label="SwiftUI" default> Trong SwiftUI, lấy cấu hình flow bằng resolver và truyền vào modifier `.flow`: ```swift showLineNumbers title="SwiftUI" @State var flowPresented = false @State var flowConfiguration: AdaptyUI.FlowConfiguration? var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false default: break } }, didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) .task { flowConfiguration = try? await AdaptyUI.getFlowConfiguration( forFlow: flow, observerModeResolver: <AdaptyObserverModeResolver> ) } } ``` Tham số `observerModeResolver:` trên `getFlowConfiguration` là thứ giúp flow được render tôn trọng logic mua hàng tùy chỉnh của bạn — modifier này sử dụng cùng các callback như chế độ đầy đủ. :::warning Đừng quên [liên kết paywall với các giao dịch mua hàng](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua hàng. ::: </TabItem> </Tabs> </SDKv4> <SDKv3> <Tabs groupId="current-os" queryString> <TabItem value="sdk3" label="Paywall Builder (SDK 3.x)" default> <details> <summary>Trước khi bắt đầu hiển thị paywall (Nhấn để mở rộng)</summary> 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android) và [với App Store](initial_ios). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo từng framework cho [iOS](sdk-installation-ios#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình paywall, gán sản phẩm vào đó](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 5. [Tạo placement và gán paywall vào đó](create-placement) trong Adapty Dashboard. 6. [Lấy paywall từ Paywall Builder cùng cấu hình của chúng](get-pb-paywalls) trong code ứng dụng di động của bạn. </details> <p> </p> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> 1. Triển khai đối tượng `AdaptyObserverModeResolver`: ```swift showLineNumbers title="Swift" func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, onFinishRestore: @escaping () -> Void) { // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore } ``` Sự kiện `observerMode(didInitiatePurchase:onStartPurchase:onFinishPurchase:)` sẽ thông báo cho bạn rằng người dùng đã khởi tạo một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình để phản hồi callback này. Sự kiện `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)` sẽ thông báo cho bạn rằng người dùng đã khởi tạo một yêu cầu khôi phục. Bạn có thể kích hoạt flow khôi phục tùy chỉnh của mình để phản hồi callback này. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về quá trình mua hàng hoặc khôi phục. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị loader và các thứ khác: | Callback | Mô tả | | :----------------- | :------------------------------------------------------------------------------------------- | | onStartPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã bắt đầu. | | onFinishPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã hoàn tất. | | onStartRestore() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã bắt đầu. | | onFinishRestore() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã hoàn tất. | 2. Tạo đối tượng cấu hình paywall: ```swift showLineNumbers title="Swift" do { let paywallConfiguration = try AdaptyUI.getPaywallConfiguration( forPaywall: <paywall object>, observerModeResolver: <AdaptyObserverModeResolver> ) } catch { // handle the error } ``` Tham số yêu cầu: | Parameter | Presence | Description | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | required | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **ObserverModeResolver** | required | Đối tượng `AdaptyObserverModeResolver` bạn đã triển khai ở bước trước | 3. Khởi tạo paywall trực quan bạn muốn hiển thị bằng cách sử dụng phương thức `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: <paywall configuration object>, delegate: <AdaptyPaywallControllerDelegate> ) ``` Các tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | bắt buộc | Một đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Tham khảo chủ đề [Tải paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Delegate** | bắt buộc | Một `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện của paywall. Tham khảo chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | Kết quả trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | Đối tượng đại diện cho màn hình paywall được yêu cầu | Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó như sau: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua hàng](report-transactions-observer-mode). Nếu không, Adapty sẽ không thể xác định được paywall nguồn của giao dịch mua hàng. ::: </TabItem> <TabItem value="swiftui" label="SwiftUI" default> Để hiển thị paywall trực quan trên màn hình thiết bị, hãy sử dụng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration: <paywall configuration object>, didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } ) } ``` (empty input — nothing to translate) | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | bắt buộc | Một đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Tham khảo chủ đề [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) để biết thêm chi tiết. | | **Products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền vào `nil`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | | **TagResolver** | tùy chọn | Định nghĩa một dictionary chứa các custom tag và giá trị tương ứng. Custom tag đóng vai trò là placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Custom tags in Paywall Builder để biết thêm chi tiết. | | **ObserverModeResolver** | tùy chọn | Đối tượng `AdaptyObserverModeResolver` mà bạn đã triển khai ở bước trước | Các tham số closure: | Tham số closure | Mô tả | | :------------------- | :---------------------------------------------------------------------------------------------- | | **didFinishRestore** | Nếu Adapty.restorePurchases() thành công, callback này sẽ được gọi. | | **didFailRestore** | Nếu Adapty.restorePurchases() thất bại, callback này sẽ được gọi. | | **didFailRendering** | Nếu xảy ra lỗi trong quá trình render giao diện, callback này sẽ được gọi. | Tham khảo chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết các tham số closure khác. :::warning Đừng quên [Liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch. ::: </TabItem> </Tabs> </TabItem> <TabItem value="sdk2" label="Legacy Paywall Builder (SDK up to 2.x)" default> <details> <summary>Trước khi bắt đầu hiển thị paywall (Nhấn để mở rộng)</summary> 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android) và [với App Store](initial_ios). 1. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo từng framework: [iOS](sdk-installation-ios#activate-adapty-module-of-adapty-sdk), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk), và [Unity](sdk-installation-unity#activate-adapty-module-of-adapty-sdk). 2. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 3. [Cấu hình paywall, gán sản phẩm vào paywall](create-paywall) và tùy chỉnh chúng bằng Paywall Builder trong Adapty Dashboard. 4. [Tạo placement và gán paywall vào đó](create-placement) trong Adapty Dashboard. 5. [Lấy paywall từ Paywall Builder và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng di động. </details> <p> </p> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> 1. Triển khai đối tượng `AdaptyObserverModeDelegate`: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } ``` `paywallController(_:didInitiatePurchase:onStartPurchase:onFinishPurchase:)` sẽ thông báo cho bạn biết khi người dùng đã bắt đầu một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình để phản hồi sự kiện này. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về quá trình mua hàng. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị trạng thái tải, v.v.: | Callback | Mô tả | | :--------------- | :--------------------------------------------------------------------------------------------- | | onStartPurchase | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua đã bắt đầu. | | onFinishPurchase | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua đã hoàn thành. | 2. Khởi tạo paywall hiển thị bằng phương thức `.paywallController(for:products:viewConfiguration:delegate:observerModeDelegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( for: <paywall object>, products: <paywall products array>, viewConfiguration: <LocalizedViewConfiguration>, delegate: <AdaptyPaywallControllerDelegate> observerModeDelegate: <AdaptyObserverModeDelegate> ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :----------------------------------------------------------- | | **Paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **Products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền vào `nil`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | | **ViewConfiguration** | bắt buộc | Đối tượng `AdaptyUI.LocalizedViewConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getViewConfiguration(paywall:locale:)`. Tham khảo chủ đề [Tải paywall từ Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Delegate** | bắt buộc | Đối tượng `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện của paywall. Tham khảo chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | | **ObserverModeDelegate** | bắt buộc | Đối tượng `AdaptyObserverModeDelegate` bạn đã triển khai ở bước trước. | | **TagResolver** | tùy chọn | Định nghĩa một dictionary gồm các thẻ tùy chỉnh và giá trị tương ứng. Các thẻ tùy chỉnh đóng vai trò là placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Custom tags in Paywall Builder để biết thêm chi tiết. | Trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | Một đối tượng đại diện cho màn hình paywall được yêu cầu | Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó như sau: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Đừng quên [Liên kết paywall với giao dịch mua hàng](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: </TabItem> <TabItem value="swiftui" label="SwiftUI" default> Để hiển thị paywall trực quan trên màn hình thiết bị, sử dụng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: <paywall object>, configuration: <LocalizedViewConfiguration>, didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false }, observerModeDidInitiatePurchase: { product, onStartPurchase, onFinishPurchase in // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase }, ) } ``` (empty input — no translatable prose or code to output) | Tham số | Bắt buộc | Mô tả | | :---------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **Product** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền vào `nil`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | | **Configuration** | bắt buộc | Đối tượng `AdaptyUI.LocalizedViewConfiguration` chứa thông tin chi tiết về giao diện của paywall. Sử dụng phương thức `AdaptyUI.getViewConfiguration(paywall:locale:)`. Tham khảo chủ đề [Lấy paywall từ Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **TagResolver** | tùy chọn | Định nghĩa một dictionary các thẻ tùy chỉnh và giá trị tương ứng của chúng. Các thẻ tùy chỉnh đóng vai trò là placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Custom tags in paywall builder để biết thêm chi tiết. | Closure parameters: | Tham số closure | Mô tả | | :---------------------------------- | :--------------------------------------------------------------------------------------------- | | **didFinishRestore** | Nếu Adapty.restorePurchases() thành công, callback này sẽ được gọi. | | **didFailRestore** | Nếu Adapty.restorePurchases() thất bại, callback này sẽ được gọi. | | **didFailRendering** | Nếu xảy ra lỗi trong quá trình render giao diện, callback này sẽ được gọi. | | **observerModeDidInitiatePurchase** | Callback này được gọi khi người dùng bắt đầu thực hiện giao dịch mua. | Tham khảo chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết các tham số closure khác. :::warning Đừng quên [Liên kết paywall với giao dịch mua hàng](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua hàng. ::: </TabItem> </Tabs> </TabItem> </Tabs> </SDKv3> --- # File: ios-quickstart-manual --- --- title: "Kích hoạt mua hàng trong paywall tùy chỉnh của bạn trên iOS SDK" description: "Tích hợp Adapty SDK vào các paywall iOS tùy chỉnh của bạn để kích hoạt in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Giữ toàn quyền kiểm soát việc triển khai paywall, trong khi Adapty SDK tự động lấy sản phẩm, xử lý giao dịch mua mới và khôi phục các giao dịch trước đó. :::important **Hướng dẫn này dành cho các nhà phát triển đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để kích hoạt mua hàng, hãy sử dụng [Adapty Flow Builder](ios-quickstart-paywalls). Với Flow Builder, bạn tạo flow trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic mua hàng, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để kích hoạt in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – bất kỳ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi sản phẩm, giá cả và ưu đãi mà không cần chỉnh sửa code ứng dụng. - [**Placements**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trong dashboard, sau đó yêu cầu chúng theo placement ID trong code. Điều này giúp dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho các đối tượng người dùng khác nhau. Hãy đảm bảo bạn hiểu các khái niệm này ngay cả khi làm việc với paywall tùy chỉnh. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy các sản phẩm. Để hiểu những gì cần làm trong dashboard, hãy xem hướng dẫn nhanh [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc có hoặc không có xác thực backend ở phía bạn. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo cách khác nhau. Đọc [hướng dẫn nhanh về xác định người dùng](ios-quickstart-identify) để hiểu các đặc thù và đảm bảo bạn đang làm việc đúng cách với người dùng. ## Bước 1. Lấy sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho paywall tùy chỉnh của bạn, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền [placement](placements) ID vào phương thức `getFlow`. 2. Lấy mảng sản phẩm cho flow này bằng phương thức `getPaywallProducts`. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift func loadPaywall() async { do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") let products = try await Adapty.getPaywallProducts(flow: flow) // Use products to build your custom paywall UI } catch { // Handle the error } } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift func loadPaywall() { Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): Adapty.getPaywallProducts(flow: flow) { result in switch result { case let .success(products): // Use products to build your custom paywall UI case let .failure(error): // Handle the error } } case let .failure(error): // Handle the error } } } ``` </TabItem> </Tabs> ## Bước 2. Chấp nhận thanh toán \{#step-2-accept-purchases\} Khi người dùng nhấn vào một sản phẩm trong paywall tùy chỉnh của bạn, hãy gọi phương thức `makePurchase` với sản phẩm được chọn. Phương thức này sẽ xử lý flow mua hàng và trả về hồ sơ người dùng đã được cập nhật. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift func purchaseProduct(_ product: AdaptyPaywallProduct) async { do { let purchaseResult = try await Adapty.makePurchase(product: product) switch purchaseResult { case .userCancelled: // User canceled the purchase break case .pending: // Purchase is pending (e.g., awaiting parental approval) break case let .success(profile, transaction): // Purchase successful, profile updated break } } catch { // Handle the error } } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift func purchaseProduct(_ product: AdaptyPaywallProduct) { Adapty.makePurchase(product: product) { result in switch result { case let .success(purchaseResult): switch purchaseResult { case .userCancelled: // User canceled the purchase break case .pending: // Purchase is pending (e.g., awaiting parental approval) break case let .success(profile, transaction): // Purchase successful, profile updated break } case let .failure(error): // Handle the error } } } ``` </TabItem> </Tabs> ## Bước 3. Khôi phục giao dịch \{#step-3-restore-purchases\} Apple yêu cầu tất cả ứng dụng có gói đăng ký phải cung cấp cách để người dùng khôi phục giao dịch của họ. Mặc dù giao dịch được tự động khôi phục khi người dùng đăng nhập bằng Apple ID, bạn vẫn phải triển khai nút khôi phục trong ứng dụng. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Phương thức này sẽ đồng bộ lịch sử giao dịch của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift func restorePurchases() async { do { let profile = try await Adapty.restorePurchases() // Restore successful, profile updated } catch { // Handle the error } } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift func restorePurchases() { Adapty.restorePurchases { result in switch result { case let .success(profile): // Restore successful, profile updated case let .failure(error): // Handle the error } } } ``` </TabItem> </Tabs> ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. [Kiểm tra giao dịch của bạn trong chế độ sandbox](test-purchases-in-sandbox) để đảm bảo bạn có thể hoàn thành một giao dịch thử nghiệm từ paywall. Tiếp theo, [kiểm tra xem người dùng đã hoàn thành giao dịch chưa](ios-check-subscription-status) để xác định xem có nên hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí. --- # File: fetch-paywalls-and-products --- --- title: "Lấy paywall và sản phẩm cho paywall remote config trong iOS SDK" description: "Lấy paywall và sản phẩm trong Adapty iOS SDK để tăng cường monetization cho người dùng." --- <SDKv4> Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng phần này đề cập đến Remote Config và paywall tùy chỉnh. Để biết hướng dẫn lấy flow hoặc paywall được tùy chỉnh trong **Flow Builder** hoặc **Paywall Builder**, vui lòng tham khảo <InlineTooltip tooltip="hướng dẫn cách lấy flow và paywall trong ứng dụng của bạn">[iOS](get-pb-paywalls), [Android](android-get-pb-paywalls), [React Native](react-native-get-pb-paywalls), [Flutter](flutter-get-pb-paywalls), và [Unity](unity-get-pb-paywalls)</InlineTooltip>. :::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. ::: <details> <summary>Trước khi bắt đầu lấy flow và sản phẩm trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow hoặc paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow hoặc paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-ios) trong ứng dụng mobile của bạn. </details> ## Lấy thông tin flow \{#fetch-flow-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các flow và paywall, cho phép bạn hiển thị chúng tại các placement cụ thể trong ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy `AdaptyFlow` từ một trong các [placement](placements) của mình bằng phương thức `getFlow`. :::important **Đừng hardcode product ID.** ID duy nhất bạn cần hardcode là placement ID. Các flow được cấu hình từ xa, nên số lượng sản phẩm và ưu đãi có thể thay đổi bất cứ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay flow trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: <Tabs group="current-os"> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") // the requested flow } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị phương án này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có trải nghiệm tải nhanh hơn, bất kể kết nối internet của họ có kém ổn định đến đâu. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi sử dụng trong suốt phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ flow và paywall theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải flow và paywall nhanh hơn, cùng một server dự phòng độc lập trong trường hợp CDN không thể truy cập được.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | :::note Trong v4, tham số `locale` đã được chuyển ra khỏi `getFlow` và sang `getFlowConfiguration` (chỉ dùng khi render với AdaptyUI). Đối với các paywall tùy chỉnh, tất cả các locale khả dụng được trả về cùng nhau trong `flow.remoteConfigs` — hãy chọn locale phù hợp với thiết bị của người dùng hoặc cài đặt trong ứng dụng của bạn. ::: Đừng hardcode ID sản phẩm! Vì các flow được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được các tình huống này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị đúng 2 sản phẩm đó. Nhưng nếu sau này lấy được 3 sản phẩm, ứng dụng phải hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn cần hardcode là placement ID. Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, mảng `remoteConfigs` (một mục cho mỗi ngôn ngữ được cấu hình) và cờ `hasViewConfiguration`. Để lấy sản phẩm cho flow, hãy gọi `getPaywallProducts(flow:)`. | ## Lấy sản phẩm \{#fetch-products\} Sau khi có flow, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: <Tabs group="current-os"> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let products = try await Adapty.getPaywallProducts(flow: flow) // the requested products array } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getPaywallProducts(flow: flow) { result in switch result { case let .success(products): // the requested products array case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> Tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi tự thiết kế paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct). Dưới đây là các thuộc tính được dùng phổ biến nhất — xem tài liệu được liên kết để biết đầy đủ thông tin về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề của sản phẩm, sử dụng `product.localizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, sử dụng `product.localizedPrice`. Bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, sử dụng `product.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm,...), sử dụng `product.localizedSubscriptionPeriod`. Bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo chương trình, sử dụng `product.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy đơn vị thời gian (tức là ngày, tuần, tháng, năm hoặc không xác định). Giá trị `numberOfUnits` sẽ cho bạn biết số lượng đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `.month` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ số khác cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionOffer`. Trong đối tượng này có các thuộc tính hữu ích sau:<br/>• `offerType`: một enum với các giá trị `introductory`, `promotional` và `winBack`. Dùng thử miễn phí và gói đăng ký giảm giá ban đầu sẽ thuộc loại `introductory`.<br/>• `price`: Giá giảm dưới dạng số. Với bản dùng thử miễn phí, giá trị này là `0`.<br/>• `localizedPrice`: Giá đã được định dạng theo ngôn ngữ của người dùng.<br/>• `localizedNumberOfPeriods`: một chuỗi được bản địa hóa theo ngôn ngữ của thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết về chu kỳ ưu đãi thông qua thuộc tính này. Cách hoạt động tương tự như mô tả ở phần trước.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo ngôn ngữ của người dùng. | :::note Trong v4, tất cả sản phẩm được trả về bởi `getPaywallProducts(flow:)` đã bao gồm thông tin về điều kiện áp dụng ưu đãi. Lời gọi `getPaywallProductsWithoutDeterminingOffer` riêng biệt từ v3 đã bị loại bỏ. ::: ## Tăng tốc tải flow với flow đối tượng mặc định \{#speed-up-flow-fetching-with-default-audience-flow\} Thông thường, flow được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong các trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow có thể mất nhiều thời gian hơn bạn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow bằng phương thức `getFlow`, như đã mô tả chi tiết trong phần [Lấy thông tin flow](fetch-paywalls-and-products#fetch-flow-information) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tiềm ẩn về tương thích ngược**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng đang dùng phiên bản hiện tại (cũ) có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã mô tả [ở trên](fetch-paywalls-and-products#fetch-flow-information). ::: <Tabs group="current-os"> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let flow = try await Adapty.getFlowForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") // the requested flow } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getFlowForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu có lỗi xảy ra. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ có tốc độ tải nhanh hơn bất kể kết nối mạng của họ kém đến đâu. Cache được cập nhật thường xuyên, vì vậy hoàn toàn an toàn khi dùng trong suốt phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | </SDKv4> <SDKv3> Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần tải thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và các paywall tùy chỉnh. Để biết hướng dẫn tải paywall cho các paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo <InlineTooltip tooltip="hướng dẫn cách tải paywall Paywall Builder trong ứng dụng của bạn">[iOS](get-pb-paywalls), [Android](android-get-pb-paywalls), [React Native](react-native-get-pb-paywalls), [Flutter](flutter-get-pb-paywalls), và [Unity](unity-get-pb-paywalls)</InlineTooltip>. :::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. ::: <details> <summary>Trước khi bắt đầu tải paywall và sản phẩm trong ứng dụng mobile (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm](create-product) trên Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall](create-paywall) trên Adapty Dashboard. 3. [Tạo placement và thêm paywall vào placement](create-placement) trên Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-ios) trong ứng dụng mobile của bạn. </details> ## Lấy thông tin paywall \{#fetch-paywall-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của bạn bằng phương thức `getPaywall`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: <Tabs group="current-os"> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> | 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản dịch paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là mã ngôn ngữ, thẻ con thứ hai là mã vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Localizations and locale codes](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị phương án này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp tình trạng kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể chất lượng kết nối. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | Đừng hardcode product ID! Vì paywall được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (chẳng hạn như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những trường hợp này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị đúng 2 sản phẩm đó. Nhưng nếu sau này lấy được 3 sản phẩm, ứng dụng cũng phải hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn cần hardcode là placement ID. Tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) gồm: danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy sản phẩm \{#fetch-products\} Sau khi có paywall, bạn có thể truy vấn mảng sản phẩm tương ứng với paywall đó: <Tabs group="current-os"> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let products = try await Adapty.getPaywallProducts(paywall: paywall) // the requested products array } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getPaywallProducts(paywall: paywall) { result in switch result { case let .success(products): // mảng sản phẩm được yêu cầu case let .failure(error): // xử lý lỗi } } ``` </TabItem> </Tabs> Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi tự thiết kế paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct). Dưới đây là các thuộc tính được sử dụng phổ biến nhất — xem tài liệu được liên kết để biết đầy đủ chi tiết về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề của sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, dùng `product.localizedPrice`. Việc bản địa hóa dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể lấy giá dưới dạng số bằng `product.price`. Giá trị sẽ được tính theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm,...), dùng `product.localizedSubscriptionPeriod`. Việc bản địa hóa dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo lập trình, dùng `product.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là ngày, tuần, tháng, năm hoặc không xác định). Giá trị `numberOfUnits` sẽ cho bạn số lượng đơn vị chu kỳ. Ví dụ: với gói đăng ký theo quý, thuộc tính unit sẽ là `.month` và numberOfUnits sẽ là `3`. | | **Ưu đãi giới thiệu** | Để hiển thị badge hoặc chỉ báo khác cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionOffer`. Trong object này có các thuộc tính hữu ích sau:<br/>• `offerType`: enum với các giá trị `introductory`, `promotional` và `winBack`. Dùng thử miễn phí và gói đăng ký giảm giá ban đầu sẽ thuộc loại `introductory`.<br/>• `price`: Giá ưu đãi dưới dạng số. Với dùng thử miễn phí, giá trị này sẽ là `0`.<br/>• `localizedPrice`: Giá ưu đãi đã được định dạng theo ngôn ngữ của người dùng.<br/>• `localizedNumberOfPeriods`: Chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ: ưu đãi dùng thử 3 ngày sẽ hiển thị `3 days` trong trường này.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết riêng lẻ của chu kỳ ưu đãi bằng thuộc tính này. Cách hoạt động giống như phần mô tả ở trên.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký ưu đãi đã được định dạng theo ngôn ngữ của người dùng. | ## Kiểm tra điều kiện nhận ưu đãi giới thiệu trên iOS \{#check-intro-offer-eligibility-on-ios\} Theo mặc định, phương thức `getPaywallProducts` sẽ kiểm tra điều kiện nhận ưu đãi giới thiệu, ưu đãi và ưu đãi thu hút khách hàng cũ. Nếu bạn cần hiển thị sản phẩm trước khi SDK xác định điều kiện nhận ưu đãi, hãy sử dụng phương thức `getPaywallProductsWithoutDeterminingOffer` thay thế. :::note Sau khi hiển thị các sản phẩm ban đầu, hãy nhớ gọi phương thức `getPaywallProducts` thông thường để cập nhật sản phẩm với thông tin điều kiện nhận ưu đãi chính xác. ::: <Tabs group="current-os"> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let products = try await Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall) // the requested products array without subscriptionOffer } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall) { result in switch result { case let .success(products): // mảng sản phẩm yêu cầu không có subscriptionOffer case let .failure(error): // xử lý lỗi } } ``` </TabItem> </Tabs> ## Tăng tốc độ tải paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, cộng với kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong đợi. Trong những tình huống đó, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà, thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ lấy paywall của placement đã chỉ định dành cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị vẫn là lấy paywall bằng phương thức `getPaywall`, như đã mô tả chi tiết trong phần [Lấy thông tin paywall](fetch-paywalls-and-products#fetch-paywall-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số nhược điểm đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng nhắm mục tiêu cá nhân hóa (bao gồm dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để được hưởng lợi từ việc tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` được mô tả [ở trên](fetch-paywalls-and-products#fetch-paywall-information). ::: <Tabs group="current-os"> <TabItem value="swift" label="Swift"> ```swift showLineNumbers do { let paywall = try await Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` </TabItem> <TabItem value="callback" label="Swift-Callback"> ```swift showLineNumbers Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ iOS SDK phiên bản 2.11.2 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này phải là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha của Brazil.</p><p></p><p>Xem [Localizations and locale codes](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối internet kém ổn định. Cache được cập nhật thường xuyên, vì vậy hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p> | </SDKv3> --- # File: present-remote-config-paywalls --- --- title: "Hiển thị paywall được thiết kế bằng remote config trong iOS SDK" description: "Khám phá cách trình bày paywall remote config trong Adapty để cá nhân hóa trải nghiệm người dùng." --- <SDKv4> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng nhìn thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn quyết định nội dung bao gồm những gì và giao diện paywall trông như thế nào. Adapty cung cấp phương thức để lấy cấu hình remote, giúp bạn tự do trình bày paywall tùy chỉnh của mình. Đừng quên [kiểm tra xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu trên iOS không](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios) và điều chỉnh giao diện paywall để xử lý trường hợp họ đủ điều kiện. ## Lấy remote config của flow và hiển thị \{#get-flow-remote-config-and-present-it\} Trong v4, một flow chứa một mục `AdaptyRemoteConfig` cho mỗi locale đã cấu hình trong mảng `remoteConfigs`. Chọn locale phù hợp với tùy chọn của người dùng, sau đó đọc các giá trị bạn cần. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") let config = flow.remoteConfigs.first(where: { $0.locale == "en" }) ?? flow.remoteConfigs.first let headerText = config?.dictionary?["header_text"] as? String } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in let flow = try? result.get() let config = flow?.remoteConfigs.first(where: { $0.locale == "en" }) ?? flow?.remoteConfigs.first let headerText = config?.dictionary?["header_text"] as? String } ``` </TabItem> </Tabs> Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng hiển thị khác nhau, mang lại trải nghiệm mượt mà và thân thiện trên mọi thiết bị. :::warning Hãy chắc chắn [ghi lại sự kiện xem paywall](present-remote-config-paywalls#track-paywall-view-events) như mô tả bên dưới để Adapty analytics có thể thu thập dữ liệu cho funnel và A/B test. ::: Sau khi hoàn tất việc hiển thị paywall, tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ flow của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](making-purchases). Chúng tôi khuyến nghị [tạo paywall dự phòng (fallback paywall)](fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi dữ liệu mua hàng được thu thập tự động, việc ghi lại lượt xem paywall cần bạn thực hiện thủ công vì chỉ bạn mới biết khi nào người dùng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowFlow(flow)` — kết quả sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test. :::important Không cần gọi `.logShowFlow(flow)` nếu bạn đang hiển thị flow hoặc paywall được render bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder). Adapty tự động theo dõi lượt xem trong những trường hợp đó. ::: ```swift showLineNumbers try await Adapty.logShowFlow(flow) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :-------- | :------- |:-----------------------------------------------------------------------------------------| | **flow** | bắt buộc | Đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow(placementId:)`. | </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng nhìn thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn quyết định nội dung bao gồm những gì và giao diện paywall trông như thế nào. Chúng tôi cung cấp phương thức để lấy cấu hình remote, giúp bạn tự do trình bày paywall tùy chỉnh được cấu hình qua Remote Config. Đừng quên [kiểm tra xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu trên iOS không](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios) và điều chỉnh giao diện paywall để xử lý trường hợp họ đủ điều kiện. ## Lấy remote config của paywall và hiển thị \{#get-paywall-remote-config-and-present-it\} Để lấy remote config của một paywall, truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") let headerText = paywall.remoteConfig?.dictionary?["header_text"] as? String } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") { result in let paywall = try? result.get() let headerText = paywall?.remoteConfig?.dictionary?["header_text"] as? String } ``` </TabItem> </Tabs> Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng hiển thị khác nhau, mang lại trải nghiệm mượt mà và thân thiện trên mọi thiết bị. :::warning Hãy chắc chắn [ghi lại sự kiện xem paywall](present-remote-config-paywalls#track-paywall-view-events) như mô tả bên dưới để Adapty analytics có thể thu thập dữ liệu cho funnel và A/B test. ::: Sau khi hoàn tất việc hiển thị paywall, tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](making-purchases). Chúng tôi khuyến nghị [tạo paywall dự phòng (fallback paywall)](fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi dữ liệu mua hàng được thu thập tự động, việc ghi lại lượt xem paywall cần bạn thực hiện thủ công vì chỉ bạn mới biết khi nào người dùng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)` — kết quả sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị paywall được tạo trong [paywall builder](adapty-paywall-builder). ::: ```swift showLineNumbers Adapty.logShowPaywall(paywall) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:-----------------------------------------------------------------------------------------| | **paywall** | bắt buộc | Đối tượng [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall). | </SDKv3> --- # File: making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng trên iOS SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng là bước thiết yếu để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ cần hiển thị paywall là đủ để hỗ trợ mua hàng nếu bạn dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall của mình. Nếu bạn không dùng Paywall Builder, bạn phải sử dụng một phương thức riêng tên `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung mong muốn. Phương thức này là cổng để người dùng tương tác với paywall và tiến hành giao dịch. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm người dùng đang muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng. :::warning Lưu ý rằng ưu đãi giới thiệu chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh tư cách đủ điều kiện nhận ưu đãi giới thiệu của người dùng trên iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Bỏ qua bước này có thể dẫn đến việc ứng dụng bị từ chối khi phát hành, đồng thời có thể tính giá đầy đủ cho những người dùng đủ điều kiện nhận ưu đãi giới thiệu. ::: Hãy đảm bảo bạn đã [hoàn tất cấu hình ban đầu](quickstart) mà không bỏ qua bất kỳ bước nào. Nếu không, chúng tôi không thể xác thực giao dịch mua hàng. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang dùng [Paywall Builder](adapty-paywall-builder)?** Giao dịch được xử lý tự động — bạn có thể bỏ qua bước này. **Cần hướng dẫn từng bước?** Xem [hướng dẫn quickstart](ios-implement-paywalls-manually) để có hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let purchaseResult = try await Adapty.makePurchase(product: product) switch purchaseResult { case .userCancelled: // Handle the case where the user canceled the purchase case .pending: // Handle deferred purchases (e.g., the user will pay offline with cash) case let .success(profile, transaction): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // Grant access to the paid features } } } catch { // Handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.makePurchase(product: product) { result in switch result { case let .success(purchaseResult): switch purchaseResult { case .userCancelled: // Handle the case where the user canceled the purchase case .pending: // Handle deferred purchases (e.g., the user will pay offline with cash) case let .success(profile, transaction): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // Grant access to the paid features } } case let .failure(error): // Handle the error } } ``` </TabItem> </Tabs> Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | bắt buộc | Một đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>Nếu yêu cầu thành công, phản hồi sẽ chứa đối tượng này. Đối tượng [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) cung cấp thông tin toàn diện về các mức độ truy cập, gói đăng ký và sản phẩm mua một lần của người dùng trong ứng dụng.</p><p>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.</p> | :::warning **Lưu ý:** nếu bạn vẫn đang dùng StoreKit của Apple phiên bản thấp hơn v2.0 và Adapty SDK phiên bản thấp hơn v2.9.0, bạn cần cung cấp [Apple App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple ngừng hỗ trợ. ::: ## In-app purchase từ App Store \{#in-app-purchases-from-the-app-store\} Khi người dùng bắt đầu mua hàng trong App Store và giao dịch được chuyển sang ứng dụng của bạn, bạn có hai lựa chọn: - **Xử lý giao dịch ngay lập tức:** Trả về `true` trong `shouldAddStorePayment`. Thao tác này sẽ hiển thị màn hình mua hàng của Apple ngay lập tức. - **Lưu đối tượng sản phẩm để xử lý sau:** Trả về `false` trong `shouldAddStorePayment`, sau đó gọi `makePurchase` với sản phẩm đã lưu vào lúc thích hợp. Điều này có thể hữu ích nếu bạn cần hiển thị nội dung tùy chỉnh cho người dùng trước khi kích hoạt giao dịch mua hàng. Đây là đoạn code hoàn chỉnh: ```swift showLineNumbers title="Swift" final class YourAdaptyDelegateImplementation: AdaptyDelegate { nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool { // 1a. // Return `true` to continue the transaction in your app. The Apple purchase system screen will show automatically. // 1b. // Store the product object and return `false` to defer or cancel the transaction. false } // 2. Continue the deferred purchase later on by passing the product to `makePurchase` when the timing is appropriate func continueDeferredPurchase() async { let storedProduct: AdaptyDeferredProduct = // get the product object from 1b. do { try await Adapty.makePurchase(product: storedProduct) } catch { // handle the error } } } ``` ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} <Details> <summary>Về offer code</summary> Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} :::warning Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. ::: Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác. </Details> Để hiển thị trang đổi mã trong ứng dụng của bạn: ```swift showLineNumbers Adapty.presentCodeRedemptionSheet() ``` :::danger Qua quan sát của chúng tôi, trang Offer Code Redemption trong một số ứng dụng có thể hoạt động không ổn định. Chúng tôi khuyến nghị chuyển hướng người dùng trực tiếp đến App Store. Để làm điều này, bạn cần mở URL theo định dạng sau: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng iOS SDK" description: "Tìm hiểu cách khôi phục giao dịch mua trong Adapty để đảm bảo trải nghiệm người dùng liền mạch." --- Khôi phục giao dịch mua là tính năng cho phép người dùng lấy lại quyền truy cập vào nội dung đã mua trước đây, chẳng hạn như các gói đăng ký hoặc in-app purchase, mà không bị tính phí lần nữa. Tính năng này đặc biệt hữu ích cho những người dùng đã gỡ cài đặt và cài đặt lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập lại nội dung đã mua mà không cần thanh toán thêm. :::note Trong các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), giao dịch mua sẽ được khôi phục tự động mà không cần thêm code từ phía bạn. Nếu đó là trường hợp của bạn — bạn có thể bỏ qua bước này. ::: Để khôi phục giao dịch mua khi bạn không sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let profile = try await Adapty.restorePurchases() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful access restore } } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.restorePurchases { [weak self] result in switch result { case let .success(profile): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful access restore } case let .failure(error): // handle the error } } ``` </TabItem> </Tabs> Tham số phản hồi: | Tham số | Mô tả | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>Một đối tượng [`AdaptyProfile`](https://swift.adapty.io/documentation/adapty/adaptyprofile). Model này chứa thông tin về mức độ truy cập, các gói đăng ký và các sản phẩm mua một lần.</p><p>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.</p> | :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: --- # File: ios-transaction-management --- --- title: "Quản lý giao dịch nâng cao trong iOS SDK" description: "Hoàn tất giao dịch thủ công trong ứng dụng iOS của bạn với Adapty SDK." --- :::note Quản lý giao dịch nâng cao được hỗ trợ trong Adapty iOS SDK bắt đầu từ phiên bản 3.12. ::: Quản lý giao dịch nâng cao trong Adapty cho phép bạn kiểm soát nhiều hơn cách các giao dịch được xử lý, xác minh và hoàn tất. Tính năng này giới thiệu ba tính năng tùy chọn hoạt động cùng nhau: | Tính năng | Mục đích | |-------------------------------------------------------------|----------| | [`appAccountToken`](#assign-appaccounttoken) | Liên kết giao dịch Apple với ID người dùng nội bộ của bạn | | [`jwsTransaction`](#access-the-jws-representation) | Cung cấp payload giao dịch đã ký của Apple để xác thực | | [Hoàn tất thủ công](#control-transaction-finishing-behavior) | Cho phép bạn hoàn tất giao dịch chỉ sau khi backend xác nhận thành công | Kết hợp lại, các công cụ này giúp bạn xây dựng quy trình xác thực tùy chỉnh mạnh mẽ trong khi Adapty vẫn tiếp tục đồng bộ giao dịch với backend của mình. :::important Hầu hết ứng dụng không cần đến tính năng này. Mặc định, Adapty tự động xác thực và hoàn tất các giao dịch StoreKit. Chỉ sử dụng hướng dẫn này nếu bạn chạy xác thực backend riêng hoặc muốn kiểm soát hoàn toàn vòng đời mua hàng. ::: ## Gán `appAccountToken` \{#assign-appaccounttoken\} [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) là một **UUID** cho phép bạn liên kết các giao dịch App Store với danh tính người dùng nội bộ của mình. StoreKit gắn token này với mọi giao dịch, giúp backend của bạn có thể khớp dữ liệu App Store với người dùng của bạn. Hãy sử dụng một UUID ổn định được tạo cho mỗi người dùng và tái sử dụng nó cho cùng một tài khoản trên các thiết bị. Điều này đảm bảo rằng các giao dịch mua và thông báo App Store được liên kết chính xác. Bạn có thể đặt token theo hai cách – trong quá trình khởi tạo SDK hoặc khi xác định người dùng. :::important Bạn phải luôn truyền `appAccountToken` cùng với `customerUserId`. Nếu chỉ truyền token, nó sẽ không được đưa vào giao dịch. ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } // Or when identifying a user: do { try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: <APP_ACCOUNT_TOKEN>) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } // Or when identifying a user: Adapty.identify("YOUR_USER_ID", withAppAccountToken: <APP_ACCOUNT_TOKEN>) { error in if let error { // handle the error } } ``` </TabItem> </Tabs> ## Truy cập biểu diễn JWS \{#access-the-jws-representation\} Khi bạn thực hiện mua hàng, kết quả trả về bao gồm giao dịch của Apple ở [định dạng JWS Compact Serialization](https://developer.apple.com/documentation/storekit/verificationresult/jwsrepresentation-21vgo). Bạn có thể chuyển giá trị này đến backend của mình để xác thực độc lập hoặc ghi log. ```swift let result = try await Adapty.makePurchase(product: paywallProduct) let jwsRepresentation = result.jwsTransaction ``` ## Kiểm soát hành vi hoàn tất giao dịch \{#control-transaction-finishing-behavior\} Mặc định, Adapty tự động hoàn tất các giao dịch StoreKit sau khi xác thực. Nếu bạn cần trì hoãn việc hoàn tất cho đến khi backend xác nhận thành công, hãy đặt hành vi hoàn tất thành thủ công. Trong chế độ này: - Adapty vẫn xác thực giao dịch mua và đồng bộ chúng với backend của mình. - Các giao dịch vẫn chưa được hoàn tất cho đến khi bạn gọi `finish()` một cách tường minh. ```swift var configBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_API_KEY") .with(transactionFinishBehavior: .manual) try await Adapty.activate(with: configBuilder.build()) ``` Khi sử dụng hoàn tất giao dịch thủ công, bạn cần triển khai phương thức delegate `onUnfinishedTransaction` để xử lý các giao dịch chưa hoàn tất: ```swift showLineNumbers title="Swift" extension YourApp: AdaptyDelegate { func onUnfinishedTransaction(_ transaction: AdaptyUnfinishedTransaction) async { // Perform your custom validation logic here // When ready, finish the transaction await transaction.finish() } } ``` Để lấy tất cả các giao dịch chưa hoàn tất hiện tại, hãy sử dụng phương thức `getUnfinishedTransactions()`: ```swift let unfinishedTransactions = try await Adapty.getUnfinishedTransactions() ``` --- # File: implement-observer-mode --- --- title: "Triển khai Observer mode trong iOS SDK" description: "Triển khai observer mode trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong iOS SDK." --- Nếu bạn đã có hạ tầng mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp analytics nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics. Nếu điều này đáp ứng được nhu cầu của bạn, bạn chỉ cần: 1. Bật tính năng này khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. 2. [Báo cáo giao dịch](report-transactions-observer-mode) từ hạ tầng mua hàng hiện có của bạn cho Adapty. Nếu bạn cũng cần paywall và A/B test, cần thực hiện thêm một số bước thiết lập như mô tả bên dưới. ## Thiết lập Observer mode \{#observer-mode-setup\} Bật Observer mode nếu bạn tự xử lý việc mua hàng và trạng thái gói đăng ký, đồng thời sử dụng Adapty để gửi các sự kiện gói đăng ký và analytics. :::important Khi chạy ở Observer mode, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều đó. ::: <Tabs groupId="current-os" queryString> <TabItem value="swiftui" label="SwiftUI"> ```swift showLineNumbers @main struct YourApp: App { init() { // Configure Adapty SDK let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(observerMode: true) let config = configurationBuilder.build() // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: configurationBuilder) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } var body: some Scene { WindowGroup { // Your content view } } } } ``` </TabItem> <TabItem value="swift" label="UIKit" default> ```swift showLineNumbers Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(observerMode: true) let config = configurationBuilder.build() try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` </TabItem> </Tabs> Các tham số: | Tham số | Mô tả | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean kiểm soát [Observer mode](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thực hiện thêm một số thiết lập trong Observer mode. Đây là những việc bạn cần làm ngoài các bước trên: 1. Hiển thị paywall như bình thường đối với [remote config paywalls](present-remote-config-paywalls). Đối với Paywall Builder paywalls, hãy làm theo hướng dẫn thiết lập cụ thể cho [iOS](ios-present-paywall-builder-paywalls-in-observer-mode). 3. [Liên kết paywall](report-transactions-observer-mode) với các giao dịch mua hàng. --- # File: report-transactions-observer-mode --- --- title: "Báo cáo giao dịch trong Observer Mode trên iOS SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trên iOS SDK." --- <Tabs groupId="sdk-version" queryString> <TabItem value="current" label="Adapty SDK v3.4+ (current)" default> Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống thanh toán hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng. Điều quan trọng là phải thiết lập điều này **trước** khi phát hành ứng dụng để tránh lỗi trong analytics. Sử dụng `reportTransaction` để báo cáo từng giao dịch một cách tường minh để Adapty nhận diện. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận diện được giao dịch, giao dịch đó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy bao gồm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```swift showLineNumbers do { // every time when calling transasction.finish() try await Adapty.reportTransaction(transaction, withVariationId: <YOUR_PAYWALL_VARIATION_ID>) } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | --------------- | -------- | ------------------------------------------------------------ | | **transaction** | bắt buộc | <ul><li> Với StoreKit 1: SKPaymentTransaction.</li><li> Với StoreKit 2: Transaction.</li></ul> | | **variationId** | tùy chọn | ID duy nhất của biến thể paywall. Lấy từ thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall). | </TabItem> <TabItem value="old" label="Adapty SDK 3.3.x (legacy)" default> Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống thanh toán hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng hoặc khôi phục chúng. Điều quan trọng là phải thiết lập điều này **trước** khi phát hành ứng dụng để tránh lỗi trong analytics. Sử dụng `reportTransaction` để gửi dữ liệu giao dịch đến Adapty. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận diện được giao dịch, giao dịch đó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy bao gồm `withVariationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```swift showLineNumbers do { // every time when calling transasction.finish() try await Adapty.reportTransaction(transaction, withVariationId: <YOUR_PAYWALL_VARIATION_ID>) } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | --------------- | -------- | ------------------------------------------------------------ | | **transaction** | bắt buộc | <ul><li> Với StoreKit 1: SKPaymentTransaction.</li><li> Với StoreKit 2: Transaction.</li></ul> | | **variationId** | tùy chọn | ID duy nhất của biến thể paywall. Lấy từ thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall). | </TabItem> <TabItem value="old2" label="Adapty SDK up to 3.2.x (legacy)" default> **Báo cáo giao dịch** - Các phiên bản đến 3.1.x tự động lắng nghe giao dịch trên App Store, nên không cần báo cáo thủ công. - Phiên bản 3.2 không hỗ trợ Observer Mode. **Liên kết paywall với giao dịch** Adapty SDK không thể xác định nguồn gốc của các giao dịch mua hàng vì bạn là người xử lý chúng. Do đó, nếu bạn định sử dụng paywall và/hoặc A/B test trong Observer Mode, bạn cần liên kết giao dịch từ cửa hàng ứng dụng với paywall tương ứng trong code ứng dụng của bạn. Điều này quan trọng cần làm đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến lỗi trong analytics. ```swift let variationId = paywall.variationId // There are two overloads: for StoreKit 1 and StoreKit 2 Adapty.setVariationId(variationId, forPurchasedTransaction: transactionId) { error in if error == nil { // successful binding } } ``` Tham số của yêu cầu: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | variationId | bắt buộc | Mã định danh chuỗi của biến thể. Bạn có thể lấy nó bằng thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall). | | transactionId | bắt buộc | <p>Với StoreKit 1: đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).</p><p>Với StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).</p> | </TabItem> </Tabs> --- # File: ios-troubleshoot-purchases --- --- title: "Khắc phục sự cố mua hàng trong iOS SDK" description: "Khắc phục sự cố mua hàng trong iOS SDK" --- Hướng dẫn này giúp bạn giải quyết các vấn đề thường gặp khi triển khai mua hàng thủ công trong iOS SDK. ## AdaptyError.cantMakePayments trong chế độ observer \{#adaptyelrorcantmakepayments-in-observer-mode\} **Vấn đề**: Bạn nhận được `AdaptyError.cantMakePayments` khi sử dụng `makePurchase` trong chế độ observer. **Nguyên nhân**: Trong chế độ observer, bạn phải tự xử lý việc mua hàng ở phía mình, không nên dùng phương thức `makePurchase` của Adapty. **Giải pháp**: Nếu bạn đang dùng `makePurchase` để thực hiện mua hàng, hãy tắt chế độ observer. Bạn chỉ có thể chọn một trong hai: dùng `makePurchase` hoặc tự xử lý mua hàng trong chế độ observer. Xem [Triển khai chế độ Observer](implement-observer-mode) để biết thêm chi tiết. ## Không tìm thấy makePurchasesCompletionHandlers \{#not-found-makepurchasescompletionhandlers\} **Vấn đề**: Bạn gặp lỗi không tìm thấy `makePurchasesCompletionHandlers`. **Nguyên nhân**: Vấn đề này thường liên quan đến lỗi khi kiểm thử trên sandbox. **Giải pháp**: Tạo một tài khoản sandbox mới và thử lại. Cách này thường giải quyết được các vấn đề liên quan đến purchase completion handler trong sandbox. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các vấn đề liên quan đến mua hàng khác chưa được đề cập ở trên. **Giải pháp**: Nếu cần, hãy migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](ios-sdk-migration-guides). Nhiều vấn đề đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: ios-web-paywall --- --- title: "Triển khai web paywall trong iOS SDK" description: "Thiết lập web paywall để nhận thanh toán mà không phải chịu phí và kiểm duyệt của App Store." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trên dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: ## Mở web paywall \{#open-web-paywalls\} Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywall bằng phương thức SDK. Phương thức `.openWebPaywall`: 1. Tạo một URL duy nhất cho phép Adapty liên kết paywall cụ thể hiển thị cho người dùng với trang web mà họ được chuyển hướng đến. 2. Theo dõi khi người dùng quay lại ứng dụng, sau đó gọi `.getProfile` theo chu kỳ ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không. Nhờ vậy, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ được kích hoạt trong ứng dụng gần như ngay lập tức. ```swift showLineNumbers title="Swift" do { try await Adapty.openWebPaywall(for: product) } catch { print("Failed to open web paywall: \(error)") } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall(product)` tạo URL theo paywall và đồng thời thêm dữ liệu sản phẩm vào URL. 2. `openWebPaywall(paywall)` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng phiên bản này khi sản phẩm trong Adapty paywall khác với sản phẩm trong web paywall. ::: ## Xử lý lỗi \{#handle-errors\} | Lỗi | Mô tả | Hành động khuyến nghị | |-----------------------------------------|------------------------------------------------------------|--------------------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | Paywall chưa được cấu hình URL mua hàng qua web | Kiểm tra xem paywall đã được cấu hình đúng trong Adapty Dashboard chưa | | AdaptyError.productWithoutPurchaseUrl | Sản phẩm chưa có URL mua hàng qua web | Xác minh cấu hình sản phẩm trong Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Không thể mở URL trong trình duyệt | Kiểm tra cài đặt thiết bị hoặc cung cấp phương thức thanh toán thay thế | | AdaptyError.failedDecodingWebPaywallUrl | Không thể mã hóa đúng các tham số trong URL | Xác minh rằng các tham số URL hợp lệ và được định dạng đúng | ## Ví dụ triển khai \{#implementation-example\} ```swift showLineNumbers title="Swift" class SubscriptionViewController: UIViewController { var paywall: AdaptyPaywall? @IBAction func purchaseButtonTapped(_ sender: UIButton) { guard let paywall = paywall, let product = paywall.products.first else { return } Task { await offerWebPurchase(for: product) } } func offerWebPurchase(for paywallProduct: AdaptyPaywallProduct) async { do { // Attempt to open web paywall try await Adapty.openWebPaywall(for: paywallProduct) } catch let error as AdaptyError { switch error { case .paywallWithoutPurchaseUrl, .productWithoutPurchaseUrl: showAlert(message: "Web purchase is not available for this product.") case .failedOpeningWebPaywallUrl: showAlert(message: "Could not open web browser. Please try again.") default: showAlert(message: "An error occurred: \(error.localizedDescription)") } } catch { showAlert(message: "An unexpected error occurred.") } } // Helper methods private func showAlert(message: String) { /* ... */ } } ``` :::note Sau khi người dùng quay lại ứng dụng, hãy làm mới giao diện để phản ánh các cập nhật hồ sơ người dùng. `AdaptyDelegate` sẽ nhận và xử lý các sự kiện cập nhật hồ sơ người dùng. ::: ## Mở web paywall trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} :::important Mở web paywall trong trình duyệt trong ứng dụng được hỗ trợ từ Adapty SDK v3.15 trở lên. ::: Mặc định, web paywall sẽ mở trong trình duyệt bên ngoài. Để mang lại trải nghiệm liền mạch cho người dùng, bạn có thể mở web paywall trong trình duyệt tích hợp sẵn trong ứng dụng. Điều này hiển thị trang thanh toán web ngay bên trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt tham số `in` thành `.inAppBrowser`: ```swift showLineNumbers title="Swift" do { try await Adapty.openWebPaywall(for: product, in: .inAppBrowser) // default – .externalBrowser } catch { print("Failed to open web paywall: \(error)") } ``` --- # File: identifying-users --- --- title: "Xác định người dùng trong iOS SDK" description: "Xác định người dùng trong Adapty để cải thiện trải nghiệm gói đăng ký được cá nhân hóa." --- Adapty tạo một ID hồ sơ nội bộ cho mỗi người dùng. Tuy nhiên, nếu bạn có hệ thống xác thực riêng, bạn nên đặt Customer User ID của mình. Bạn có thể tìm người dùng theo Customer User ID trong phần [Profiles](profiles-crm) và sử dụng nó trong [server-side API](getting-started-with-server-side-api) — giá trị này sẽ được gửi đến tất cả các tích hợp. ## Đặt customer user ID khi cấu hình \{#set-customer-user-id-on-configuration\} Nếu bạn đã có user ID trong quá trình cấu hình, chỉ cần truyền nó qua tham số `customerUserId` vào phương thức `.activate()`: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` </TabItem> </Tabs> :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Đặt customer user ID sau khi cấu hình \{#set-customer-user-id-after-configuration\} Nếu bạn chưa có user ID khi cấu hình SDK, bạn có thể đặt nó sau bất kỳ lúc nào bằng phương thức `.identify()`. Trường hợp phổ biến nhất để dùng phương thức này là sau khi đăng ký hoặc đăng nhập, khi người dùng chuyển từ trạng thái ẩn danh sang người dùng đã xác thực. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` </TabItem> </Tabs> Tham số yêu cầu: - **Customer User ID** (bắt buộc): chuỗi định danh người dùng. :::warning Gửi lại dữ liệu người dùng quan trọng Trong một số trường hợp, chẳng hạn khi người dùng đăng nhập lại vào tài khoản, máy chủ Adapty đã có thông tin về người dùng đó. Trong những trường hợp này, Adapty SDK sẽ tự động chuyển sang làm việc với người dùng mới. Nếu bạn đã truyền dữ liệu cho người dùng ẩn danh — chẳng hạn như thuộc tính tùy chỉnh hoặc attribution từ các mạng bên thứ ba — bạn cần gửi lại dữ liệu đó cho người dùng đã xác định. Ngoài ra, cần lưu ý rằng bạn nên yêu cầu lại tất cả paywall và sản phẩm sau khi xác định người dùng, vì dữ liệu của người dùng mới có thể khác. ::: ## Đăng xuất và đăng nhập \{#logging-out-and-logging-in\} Bạn có thể đăng xuất người dùng bất kỳ lúc nào bằng cách gọi phương thức `.logout()`: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { try await Adapty.logout() } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.logout { error in if error == nil { // successful logout } } ``` </TabItem> </Tabs> Sau đó, bạn có thể đăng nhập người dùng bằng phương thức `.identify()`. ## Đặt appAccountToken \{#set-appaccounttoken\} [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) là một UUID giúp StoreKit 2 của Apple xác định người dùng qua các lần cài đặt ứng dụng và thiết bị khác nhau. Từ Adapty iOS SDK 3.10.2 trở đi, bạn có thể truyền `appAccountToken` khi cấu hình SDK hoặc khi xác định người dùng: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } // Or when identifying a user: do { try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } // Or when identifying a user: Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) { error in if let error { // handle the error } } ``` </TabItem> </Tabs> Sau đó, bạn có thể đăng nhập người dùng bằng phương thức `.identify()`. ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong iOS SDK" description: "Tìm hiểu cách thiết lập thuộc tính người dùng trong Adapty để phân khúc đối tượng hiệu quả hơn." --- Bạn có thể thiết lập các thuộc tính tùy chọn như email, số điện thoại, v.v. cho người dùng của ứng dụng. Sau đó, bạn có thể sử dụng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM. ### Thiết lập thuộc tính người dùng \{#setting-user-attributes\} Để thiết lập thuộc tính người dùng, hãy gọi phương thức `.updateProfile()`: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(email: "email@email.com") .with(phoneNumber: "+18888888888") .with(firstName: "John") .with(lastName: "Appleseed") .with(gender: .other) .with(birthday: Date()) do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(email: "email@email.com") .with(phoneNumber: "+18888888888") .with(firstName: "John") .with(lastName: "Appleseed") .with(gender: .other) .with(birthday: Date()) Adapty.updateProfile(params: builder.build()) { error in if error != nil { // handle the error } } ``` </TabItem> </Tabs> Lưu ý rằng các thuộc tính bạn đã thiết lập trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các khóa được phép \{#the-allowed-keys-list\} Các khóa `<Key>` được phép của `AdaptyProfileParameters.Builder` và các giá trị `<Value>` tương ứng được liệt kê bên dưới: | Key | Value | |---|-----| | <p>email</p><p>phoneNumber</p><p>firstName</p><p>lastName</p> | String | | gender | Enum, các giá trị được phép là: `female`, `male`, `other` | | birthday | Date | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể thiết lập các thuộc tính tùy chỉnh của riêng mình. Những thuộc tính này thường liên quan đến cách người dùng sử dụng ứng dụng. Ví dụ, với ứng dụng thể dục, chúng có thể là số buổi tập mỗi tuần; với ứng dụng học ngoại ngữ, chúng có thể là trình độ kiến thức của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi có mục tiêu, đồng thời sử dụng trong phân tích để xác định chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```swift showLineNumbers do { builder = try builder.with(customAttribute: "value1", forKey: "key1") } catch { // handle key/value validation error } ``` Để xóa khóa hiện có, hãy sử dụng phương thức `.withRemoved(customAttributeForKey:)`: ```swift showLineNumbers do { builder = try builder.withRemoved(customAttributeForKey: "key2") } catch { // handle error } ``` Đôi khi bạn cần biết những thuộc tính tùy chỉnh nào đã được cài đặt trước đó. Để làm điều này, hãy sử dụng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không cập nhật kịp thời, vì thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau vào bất kỳ lúc nào, nên các thuộc tính trên máy chủ có thể đã thay đổi sau lần đồng bộ cuối cùng. ::: ### Giới hạn \{#limits\} - Tối đa 30 thuộc tính tùy chỉnh mỗi người dùng - Tên khóa tối đa 30 ký tự. Tên khóa có thể gồm các ký tự chữ và số cùng với các ký tự sau: `_` `-` `.` - Giá trị có thể là chuỗi hoặc số thực (float) với tối đa 50 ký tự. --- # File: subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong iOS SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký của người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải nhúng thủ công các product ID vào code. Thay vào đó, bạn có thể dễ dàng xác nhận trạng thái gói đăng ký của người dùng bằng cách kiểm tra [mức độ truy cập](access-level) đang hoạt động. Trước khi bắt đầu kiểm tra trạng thái gói đăng ký, hãy thiết lập [App Store Server Notifications](enable-app-store-server-notifications). ## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là thuộc tính của đối tượng [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile). Chúng tôi khuyến nghị lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](identifying-users#set-customer-user-id-on-configuration), rồi cập nhật lại mỗi khi có thay đổi. Như vậy, bạn có thể sử dụng đối tượng profile mà không cần phải gọi lại nhiều lần. Để nhận thông báo khi profile được cập nhật, hãy lắng nghe các thay đổi của profile như mô tả trong phần [Lắng nghe cập nhật profile, bao gồm mức độ truy cập](subscription-status#listening-for-subscription-status-updates) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ server \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ server, sử dụng phương thức `.getProfile()`: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` </TabItem> </Tabs> Tham số trả về: | Tham số | Mô tả | | --------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Profile | <p>Đối tượng [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile). Thông thường, bạn chỉ cần kiểm tra trạng thái mức độ truy cập của profile để xác định xem người dùng có quyền truy cập premium vào ứng dụng hay không.</p><p></p><p>Phương thức `.getProfile` luôn cố gắng truy vấn API nên cho kết quả cập nhật nhất. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ server thì dữ liệu trong cache sẽ được trả về. Cũng cần lưu ý rằng Adapty SDK cập nhật cache của `AdaptyProfile` định kỳ để giữ thông tin luôn được đồng bộ nhất có thể.</p> | Phương thức `.getProfile()` trả về hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Ví dụ: nếu bạn có ứng dụng đọc báo và bán gói đăng ký theo từng chủ đề riêng biệt, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết các trường hợp, bạn chỉ cần một mức độ truy cập — khi đó chỉ cần dùng mức mặc định "premium" là đủ. Dưới đây là ví dụ kiểm tra mức độ truy cập "premium" mặc định: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let profile = try await Adapty.getProfile() let isPremium = profile.accessLevels["premium"]?.isActive ?? false // grant access to premium features } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get(), profile.accessLevels["premium"]?.isActive ?? false { // grant access to premium features } } ``` </TabItem> </Tabs> ### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận thông báo từ Adapty, bạn cần thực hiện thêm một số cấu hình: ```swift showLineNumbers Adapty.delegate = self // To receive subscription updates, extend `AdaptyDelegate` with this method: nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { // handle any changes to subscription state } ``` Adapty cũng kích hoạt một sự kiện khi ứng dụng khởi động. Trong trường hợp này, trạng thái gói đăng ký được lưu trong cache sẽ được truyền vào. ### Cache trạng thái gói đăng ký \{#subscription-status-cache\} Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi server không khả dụng, dữ liệu trong cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký. Tuy nhiên, cần lưu ý rằng không thể truy vấn dữ liệu trực tiếp từ cache. SDK định kỳ gửi yêu cầu đến server mỗi phút một lần để kiểm tra xem có cập nhật hay thay đổi nào liên quan đến profile không. Nếu có bất kỳ thay đổi nào — chẳng hạn như giao dịch mới hay các cập nhật khác — chúng sẽ được đồng bộ vào dữ liệu cache để giữ thông tin luôn nhất quán với server. --- # File: ios-deal-with-att --- --- title: "Xử lý ATT trong iOS SDK" description: "Bắt đầu với Adapty trên iOS để đơn giản hóa việc thiết lập và quản lý gói đăng ký." --- Nếu ứng dụng của bạn sử dụng framework AppTrackingTransparency và hiển thị yêu cầu xác nhận quyền theo dõi ứng dụng cho người dùng, bạn cần gửi [trạng thái ủy quyền](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) đến Adapty. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(appTrackingTransparencyStatus: .authorized) do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="Swift-Callback" default> ```swift showLineNumbers if #available(iOS 14, macOS 11.0, *) { let builder = AdaptyProfileParameters.Builder() .with(appTrackingTransparencyStatus: .authorized) Adapty.updateProfile(params: builder.build()) { [weak self] error in if error != nil { // handle the error } } } ``` </TabItem> </Tabs> :::warning Chúng tôi khuyến nghị bạn gửi giá trị này càng sớm càng tốt ngay khi nó thay đổi — chỉ như vậy dữ liệu mới được gửi kịp thời đến các tích hợp bạn đã cấu hình. ::: --- # File: kids-mode --- --- title: "Chế độ Trẻ em trong iOS SDK" description: "Dễ dàng bật Chế độ Trẻ em để tuân thủ chính sách của Apple. Không thu thập IDFA hay dữ liệu quảng cáo trong iOS SDK." --- <SDKv4> Nếu ứng dụng iOS của bạn dành cho trẻ em, bạn phải tuân theo chính sách của [Apple](https://developer.apple.com/kids/). Nếu đang sử dụng Adapty SDK, chỉ cần vài bước đơn giản là bạn có thể 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 App Store. ## Điều kiện cần thiết là gì? \{#whats-required\} Bạn cần cấu hình SDK để tắt thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách cẩn thận. User ID có định dạng `<FirstName.LastName>` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc sử dụng email. Đối với Kids Mode, phương pháp tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã được hash hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ quy định. ## Bật Chế Độ Trẻ Em \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để thực hiện, hãy vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** bên dưới mục **Collect users' IP address**. ### Cập nhật trong code ứng dụng của bạn \{#updates-in-your-mobile-app-code\} Bắt đầu từ SDK 4.0, Kids Mode là một Swift package trait có tên `KidsMode`. Khi bật trait này, IDFA và AdSupport sẽ được loại bỏ hoàn toàn khỏi SDK trong quá trình biên dịch — bạn vẫn giữ nguyên các module **Adapty** và **AdaptyUI** cùng với các câu lệnh `import Adapty` / `import AdaptyUI` thông thường. :::note Trait `KidsMode` có sẵn từ SDK phiên bản 4.0. Bắt đầu từ SDK 4.0, SDK chỉ được cài đặt qua Swift Package Manager — CocoaPods không còn được hỗ trợ nữa. ::: <Tabs> <TabItem value="xcode" label="Xcode" default> 1. [Cài đặt Adapty SDK](sdk-installation-ios) như bình thường, chọn các module **Adapty** và **AdaptyUI**. 2. Trong Xcode 26.4 trở lên, mở cài đặt dự án, vào phần **Package Dependencies**, và bật trait **KidsMode** cho dependency AdaptySDK-iOS. :::note Các phiên bản Xcode trước 26.4 không thể bật trait cho dự án Xcode từ giao diện UI. Trong trường hợp đó, hãy thêm một Swift package cục bộ nhỏ phụ thuộc vào Adapty với trait `KidsMode` được bật (xem tab **Package.swift**), rồi cho app target của bạn phụ thuộc vào package đó. ::: </TabItem> <TabItem value="spm" label="Package.swift"> Nếu bạn thêm Adapty làm dependency trong `Package.swift`, hãy bật trait trong phần khai báo package. Trait yêu cầu `swift-tools-version` 6.1 trở lên. ```swift showLineNumbers title="Package.swift" .package( url: "https://github.com/adaptyteam/AdaptySDK-iOS.git", from: "4.0.0", traits: ["KidsMode"] ) ``` </TabItem> </Tabs> </SDKv4> <SDKv3> Nếu ứng dụng iOS của bạn dành cho trẻ em, bạn phải tuân thủ các chính sách của [Apple](https://developer.apple.com/kids/). Nếu đang sử dụng Adapty SDK, một vài bước đơn giản sau đây sẽ giúp bạn cấu hình SDK để đáp ứng các chính sách đó và vượt qua quá trình kiểm duyệt của cửa hàng. ## Những gì cần thiết? \{#whats-required\} Bạn cần cấu hình SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách thận trọng. User ID có định dạng `<FirstName.LastName>` chắc chắn sẽ bị xem là thu thập dữ liệu cá nhân, tương tự như việc sử dụng email. Đối với Kids Mode, cách thực hành tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã được hash hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ quy định. ## Bật Chế Độ Kids \{#enabling-kids-mode\} ### Cập nhật trong 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, hãy 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 của bạn \{#updates-in-your-mobile-app-code\} Để tuân thủ các chính sách, hãy tắt tính năng thu thập IDFA và địa chỉ IP của người dùng. <Tabs> <TabItem value="spm" label="Swift Package Manager" default> Nếu bạn dùng Swift Package Manager, có thể bật Kids Mode bằng cách chọn module **Adapty_KidsMode** trong Xcode khi cài đặt SDK. Trong Xcode, vào **File** -> **Add Package Dependency...**. Lưu ý rằng các bước thêm package dependency có thể khác nhau tùy phiên bản Xcode, vì vậy hãy tham khảo tài liệu của Xcode nếu cần. 1. Nhập URL của repository: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Chọn phiên bản (khuyến nghị dùng phiên bản ổn định mới nhất) và nhấn **Add Package**. 3. Trong cửa sổ **Choose Package Products**, chọn các module bạn cần: - **Adapty_KidsMode** (module cốt lõi) - **AdaptyUI_KidsMode** (tùy chọn - chỉ cần nếu bạn dự định dùng Paywall Builder) Bạn sẽ không cần bất kỳ package nào khác. 4. Nhấn **Add Package** để hoàn tất cài đặt. 5. Trong code của bạn, hãy viết `import Adapty_KidsMode` thay vì `import Adapty`, và `import AdaptyUI_KidsMode` thay vì `import AdaptyUI`: ```swift ``` </TabItem> <TabItem value="cocoapods" label="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ộ đoạn code bên dưới. - Nếu bạn **đã** có phần `post_install`, hãy merge các dòng được highlight vào đó. ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. Chạy lệnh sau để áp dụng các thay đổi: ```sh showLineNumbers title="Shell" pod install ``` </TabItem> </Tabs> </SDKv3> --- # File: get-onboardings --- --- title: "Lấy onboarding và cấu hình của chúng" description: "Tìm hiểu cách truy xuất onboarding trong Adapty." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flows](get-pb-paywalls) như một giải pháp thay thế mạnh mẽ hơn cho onboarding. Khác với onboarding chạy trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](get-pb-paywalls) và [Hiển thị flows & paywalls](ios-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding của mình](design-onboarding) bằng trình dựng trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên trong quy trình này là lấy onboarding được liên kết với placement và cấu hình hiển thị của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS, Android, React Native, hoặc Flutter SDK](installation-of-adapty-sdks) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Lấy dữ liệu onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng công cụ no-code builder của chúng tôi, nó được lưu trữ dưới dạng một container chứa cấu hình mà ứng dụng cần lấy về và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung hiển thị, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập vào form). Container cũng tự động ghi lại các sự kiện analytics, nên bạn không cần tự triển khai tính năng theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy một onboarding, hãy sử dụng phương thức `getOnboarding`: ```swift showLineNumbers do { let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // the requested onboarding } catch { // handle the error } ``` Các 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh ngôn ngữ của onboarding. Tham số này được kỳ vọng là mã ngôn ngữ gồm một hoặc hai thẻ con phân tách bằng ký tự gạch ngang (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>Xem [Bản địa hóa và mã ngôn ngữ](localizations-and-locale-codes) để biết thêm thông tin về mã ngôn ngữ và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong bộ nhớ 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.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ như thế nào. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ các onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và các onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập 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 các onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>Giá trị này giới hạn thời gian chờ của phương thức này. Nếu hết thời gian chờ, dữ liệu cache hoặc fallback cục bộ sẽ được trả về.</p><p>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.</p> | Các tham số phản hồi: | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://swift.adapty.io/documentation/adapty/adaptyonboarding) bao gồm: mã định danh và cấu hình onboarding, Remote Config, và một số thuộc tính khác. | ## Tăng tốc tải onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và onboarding, và người dùng có kết nối internet yếu, việc tải onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này sẽ lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như đã trình bày chi tiết trong phần [Fetch Onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản app, đòi hỏi thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ 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ỏ khả năng nhắm mục tiêu theo quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ tải nhanh hơn là ưu tiên và bù đắp được những hạn chế này trong trường hợp của bạn, hãy dùng `getOnboardingForDefaultAudience` như hướng dẫn bên dưới. Nếu không, hãy dùng `getOnboarding` như đã mô tả [ở trên](#fetch-onboarding). ::: ```swift showLineNumbers Adapty.getOnboardingForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(onboarding): // the requested onboarding case let .failure(error): // handle the error } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của bản địa hóa onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân tách bằng ký tự dấu trừ (**-**). Thẻ phụ đầu tiên dành cho ngôn ngữ, thẻ thứ hai dành cho vùng.</p><p></p><p>Ví dụ: `en` có nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha của Brazil.</p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố gắng tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong 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 của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp phải kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã lưu trong cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có trải nghiệm tải nhanh hơn, bất kể kết nối internet có bị gián đoạn đến đâu. Cache được cập nhật thường xuyên, nên việc sử dụng nó trong phiên làm việc để tránh các yêu cầu mạng là hoàn toàn an toàn.</p><p></p><p>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 thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập được. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả trong trường hợp kết nối internet yếu.</p> | --- # File: ios-present-onboardings --- --- title: "Hiển thị onboarding trong iOS SDK" description: "Khám phá cách hiển thị onboarding trên iOS để tăng tỷ lệ chuyển đổi và doanh thu." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flow](get-pb-paywalls) như một giải pháp thay thế mạnh mẽ hơn cho onboarding. Khác với onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh onboarding bằng builder, bạn không cần lo việc render nó trong code ứng dụng mobile để hiển thị cho người dùng. Onboarding đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS SDK](sdk-installation-ios) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Hiển thị onboarding trong Swift \{#present-onboardings-in-swift\} Để hiển thị onboarding trực quan trên màn hình thiết bị, thực hiện các bước sau: 1. Lấy cấu hình onboarding bằng phương thức `.getOnboardingConfiguration`. 2. Khởi tạo onboarding trực quan bạn muốn hiển thị bằng phương thức `.onboardingController`: Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | |:-----------------------------|:---------|:------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding configuration** | required | Một đối tượng `AdaptyUI.OnboardingConfiguration` chứa tất cả các thuộc tính của onboarding. Sử dụng phương thức `AdaptyUI.getOnboardingConfiguration` để lấy đối tượng này. | | **delegate** | required | Một `AdaptyOnboardingControllerDelegate` để lắng nghe các sự kiện onboarding. | Kết quả trả về: | Đối tượng | Mô tả | |:-------------------------------|:--------------------------------------------------------| | **AdaptyOnboardingController** | Đối tượng đại diện cho màn hình onboarding được yêu cầu | 3. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó lên màn hình thiết bị: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI // 0. Get an onboarding if you haven't done it yet let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Create Onboarding View Controller let onboardingController = try AdaptyUI.onboardingController( with: configuration, delegate: <AdaptyOnboardingControllerDelegate> ) // 3. Present it to the user present(onboardingController, animated: true) ``` ## Hiển thị onboarding trong SwiftUI \{#present-onboardings-in-swiftui\} Để hiển thị onboarding trực quan trên màn hình thiết bị trong SwiftUI: ```swift showLineNumbers title="SwiftUI" // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Display the Onboarding View within your view hierarchy AdaptyOnboardingView( configuration: configuration, placeholder: { Text("Your Placeholder View") }, onCloseAction: { action in // hide the onboarding view }, onError: { error in // handle the error } ) ``` ## Thêm hiệu ứng chuyển tiếp mượt mà giữa màn hình khởi động và onboarding \{#add-smooth-transitions-between-the-splash-screen-and-onboarding\} Theo mặc định, giữa màn hình khởi động và onboarding, bạn sẽ thấy màn hình loading cho đến khi onboarding được tải xong hoàn toàn. Tuy nhiên, nếu muốn chuyển tiếp mượt mà hơn, bạn có thể tùy chỉnh bằng cách kéo dài màn hình khởi động hoặc hiển thị nội dung khác. Để thực hiện, hãy định nghĩa một placeholder (nội dung sẽ hiển thị trong khi onboarding đang được tải). Nếu bạn định nghĩa placeholder, onboarding sẽ được tải ở nền và tự động hiển thị ngay khi sẵn sàng. <Tabs> <TabItem value="swift" label="UIKit"> ```swift showLineNumbers extension YourOnboardingManagerClass: AdaptyOnboardingControllerDelegate { func onboardingsControllerLoadingPlaceholder( _ controller: AdaptyOnboardingController ) -> UIView? { // instantiate and return the UIView which will be presented while onboarding is being loaded } } ``` </TabItem> <TabItem value="swiftui" label="SwiftUI"> ```swift showLineNumbers AdaptyOnboardingView( configuration: configuration, placeholder: { // define your placeholder view, which will be presented while onboarding is being loaded }, // the rest of the implementation ) ``` </TabItem> </Tabs> ## 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. ::: Mặc định, các liên kết trong onboarding sẽ mở trong trình duyệt nội bộ của ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị trang web ngay trong ứng dụng, không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết trong trình duyệt ngoài thay thế, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `.externalBrowser`: ```swift showLineNumbers let configuration = try AdaptyUI.getOnboardingConfiguration( forOnboarding: onboarding, externalUrlsPresentation: .externalBrowser // default – .inAppBrowser ) ``` --- # File: ios-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong iOS SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong iOS bằng Adapty." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flows](get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboarding. Khác với onboarding chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện iOS nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](get-pb-paywalls) và [Hiển thị flows & paywalls](ios-present-paywalls) để bắt đầu. ::: Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS SDK](sdk-installation-ios) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể phản hồi. Hãy tìm hiểu cách xử lý các sự kiện đó bên dưới. Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình onboarding trong ứng dụng của bạn, hãy triển khai các phương thức `AdaptyOnboardingControllerDelegate`. ## Hành động tùy chỉnh \{#custom-actions\} Trong builder, bạn có thể thêm hành động **custom** vào một nút và gán cho nó một ID. <img src="/assets/shared/img/ios-events-1.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. Ví dụ: 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 trường hợp `.custom(id:)` và tham số `actionId` chính là **Action ID** từ builder. Bạn có thể tự tạo các ID theo ý muốn, chẳng hạn như "allowNotifications". ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCustomAction action: AdaptyOnboardingsCustomAction) { if action.actionId == "allowNotifications" { // Request notification permissions } } func onboardingController(_ controller: AdaptyOnboardingController, didFailWithError error: AdaptyUIError) { // Handle errors } ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ``` </Details> ## Đóng onboarding \{#closing-onboarding\} Onboarding được coi là đã đóng khi người dùng nhấn vào một nút có hành động **Close** được gán. <img src="/assets/shared/img/ios-events-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important Lưu ý rằng bạn cần xử lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ, bạn cần dừng việc hiển thị chính onboarding đó. ::: Ví dụ: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCloseAction action: AdaptyOnboardingsCloseAction) { controller.dismiss(animated: true) } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ``` </Details> ## Mở một 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 nó được đóng lại, có một cách đơn giản hơn – xử lý [`AdaptyOnboardingsCloseAction`](#closing-onboarding) và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong các onboarding là đặt action ID bằng với placement ID của paywall. Theo cách này, sau khi nhận được `AdaptyOnboardingsOpenPaywallAction`, bạn có thể dùng placement ID để lấy và mở paywall ngay lập tức. Lưu ý rằng chỉ có thể hiển thị một màn hình (paywall hoặc onboarding) tại một thời điểm. Nếu bạn hiển thị một paywall chồng lên onboarding, bạn không thể lập trình để điều khiển onboarding ở phía sau. Việc cố gắng đóng onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn đóng màn hình onboarding trước khi hiển thị paywall. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onPaywallAction action: AdaptyOnboardingsOpenPaywallAction) { // Dismiss onboarding before presenting the flow controller.dismiss(animated: true) { Task { do { // Get the flow using the placement ID from the action let flow = try await Adapty.getFlow(placementId: action.actionId) // Get the flow configuration let flowConfiguration = try await AdaptyUI.getFlowConfiguration( forFlow: flow ) // Create and present the flow controller let flowController = try AdaptyUI.flowController( with: flowConfiguration, delegate: self ) // Present the flow from the root view controller if let rootVC = UIApplication.shared.windows.first?.rootViewController { rootVC.present(flowController, animated: true) } } catch { // Handle any errors that occur during flow loading print("Failed to present flow: \(error)") } } } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ``` </Details> ## Hoàn thành tải onboarding \{#finishing-loading-onboarding\} Khi một onboarding hoàn tất quá trình tải, phương thức này sẽ được gọi: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, didFinishLoading action: OnboardingsDidFinishLoadingAction) { // Handle loading completion } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ``` </Details> ## Theo dõi điều hướng \{#tracking-navigation\} Phương thức `onAnalyticsEvent` được gọi khi các sự kiện analytics khác nhau xảy ra trong quá trình onboarding. Đối tượng `event` có thể thuộc một trong các kiểu sau: |Kiểu | 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 hoàn thành. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn thành) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `secondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, [hãy gán ID `final` cho màn hình cuối cùng](design-onboarding). | | `unknown` | Dành cho bất kỳ kiểu sự kiện không được nhận dạng nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (siêu dữ liệu bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của flow onboarding | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | Dưới đây là ví dụ về cách bạn có thể sử dụng các sự kiện analytics để theo dõi: ```swift func onboardingController(_ controller: AdaptyOnboardingController, onAnalyticsEvent event: AdaptyOnboardingsAnalyticsEvent) { switch event { case .onboardingStarted(let meta): // Track onboarding start trackEvent("onboarding_started", meta: meta) case .screenPresented(let meta): // Track screen presentation trackEvent("screen_presented", meta: meta) case .screenCompleted(let meta, let elementId, let reply): // Track screen completion with user response trackEvent("screen_completed", meta: meta, elementId: elementId, reply: reply) case .onboardingCompleted(let meta): // Track successful onboarding completion trackEvent("onboarding_completed", meta: meta) case .unknown(let meta, let name): // Handle unknown events trackEvent(name, meta: meta) // Handle other cases as needed } } ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```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 } } ``` </Details> --- # File: ios-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong iOS SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng iOS của bạn với Adapty SDK." --- :::tip **Bắt đầu từ SDK v4**, bạn có thể xây dựng [flow](get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboarding. Khác với onboarding chạy trong WebView, flow render trực tiếp trên thiết bị — mang lại hiệu ứng chuyển động mượt mà hơn, giao diện iOS nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời câu hỏi trong bài quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Ví dụ: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .select(let params): // Handle single selection case .multiSelect(let params): // Handle multiple selections case .input(let params): // Handle text input case .datePicker(let params): // Handle date selection } } ``` Đối tượng `action` chứa: | Tham số | Mô tả | |----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `elementId` | Định danh duy nhất cho phần tử input. Bạn có thể dùng nó để liên kết câu hỏi với câu trả lời khi lưu lại. | | `params` | Đối tượng dữ liệu đầu vào của người dùng, chứa các thuộc tính type và value. | | `params.type` | Loại phần tử input. Có thể là:<br/>• `"select"` - Chọn một giá trị từ các tùy chọn<br/>• `"multiSelect"` - Chọn nhiều giá trị từ các tùy chọn<br/>• `"input"` - Trường nhập văn bản<br/>• `"datePicker"` - Chọn ngày | | `params.value` | Giá trị được người dùng chọn hoặc nhập. Cấu trúc phụ thuộc vào loại:<br/>• `select`: Đối tượng có `id`, `value`, `label`<br/>• `multiSelect`: Mảng các đối tượng có `id`, `value`, `label`<br/>• `input`: Đối tượng có `type`, `value`<br/>• `datePicker`: Đối tượng có `day`, `month`, `year` | <Details> <summary>Ví dụ dữ liệu đã lưu (có thể khác với triển khai của bạn)</summary> ```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 } } } ``` </Details> ## Các trường hợp sử dụng \{#use-cases\} ### Làm phong phú thêm dữ liệu hồ sơ người dùng \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ hai lần về cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](setting-user-attributes) với dữ liệu đầu vào khi xử lý action. Ví dụ, bạn yêu cầu người dùng nhập tên của họ vào trường văn bản có ID là `name`, và bạn muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code ứng dụng của bạn, nó có thể trông như thế này: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .input(let params): // Handle text input let builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field switch action.elementId { case "name": builder.with(firstName: params.value.value) case "email": builder.with(email: params.value.value) default: break } // Delegate methods are synchronous; kick off the async update in a Task. Task { do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } default: break } } ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Sử dụng quiz trong onboarding, bạn cũng có thể tùy chỉnh các paywall hiển thị cho người dùng sau khi họ hoàn thành onboarding. Ví dụ: bạn có thể hỏi người dùng về kinh nghiệm thể thao của họ và hiển thị các CTA cùng sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) trong trình tạo onboarding và gán ID có ý nghĩa cho các lựa chọn của quiz. 2. Xử lý các phản hồi quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](setting-user-attributes) cho người dùng. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Handle quiz responses and set custom attributes switch action.params { case .select(let params): // Handle quiz selection let builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes switch action.elementId { case "experience": // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) try? builder.with(customAttribute: params.value.value, forKey: "experience") default: break } // Delegate methods are synchronous; kick off the async update in a Task. Task { do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } default: break } } ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho từng phân khúc bạn đã tạo. 5. [Hiển thị paywall](ios-paywalls) cho placement trong code ứng dụng của bạn. Nếu onboarding có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho hành động của nút này](ios-handling-onboarding-events#opening-a-paywall). --- # File: ios-sdk-call-order --- --- title: "Thứ tự gọi trong iOS SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution và lỗi #2002 không liên tục bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự." --- `Adapty.activate()` phải hoàn thành trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi nó hoàn tất, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` sẽ thất bại với lỗi [`#2002 notActivated`](ios-sdk-error-handling#network-errors). Nếu ứng dụng của bạn xác thực người dùng và bạn thu thập customer user ID sau khi khởi chạy, hãy gọi `Adapty.identify()` tại thời điểm đó. Đừng gọi các phương thức liên quan đến hành động người dùng cho đến khi `identify` hoàn tất. Các lệnh gọi thực hiện song song với nó sẽ thất bại với lỗi [`#3006 profileWasChanged`](ios-sdk-error-handling#general-errors), hoặc đổ vào hồ sơ người dùng ẩn danh được tạo lúc activation. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu lượt cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo cùng một quy tắc. Hãy khởi tạo chúng trước và chờ callback UID của chúng trước khi gọi `Adapty.activate`. Nếu không, MMP ID sẽ gắn vào một hồ sơ người dùng ẩn danh tồn tại ngắn và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Về chi tiết cụ thể với AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai yếu tố: khi nào bạn biết customer user ID và bạn có sử dụng SDK MMP hoặc analytics hay không. - **Bước 2 và 5**: Bắt buộc cho mọi ứng dụng. Activate SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ bắt buộc nếu bạn tích hợp SDK MMP hoặc analytics (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ bắt buộc nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn có customer user ID ngay khi ứng dụng khởi chạy, hãy truyền trực tiếp vào `activate()` (bước 2a). Lộ trình này không bao giờ tạo hồ sơ người dùng ẩn danh, do đó bước 4 là không cần thiết. | Bước | Lệnh gọi | Thời điểm | Lưu ý | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi chạy ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty.activate(with: config)` với `customerUserId` được đặt trên config | Khởi chạy ứng dụng, sau bước 1, nếu bạn có customer user ID | Được khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty.activate(with: config)` không có `customerUserId` | Khởi chạy ứng dụng, sau bước 1, nếu bạn không có customer user ID (hoặc không thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. | | 3 | `Adapty.setIntegrationIdentifier(...)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi hành động người dùng nào | Bắt buộc để MMP ID gắn vào đúng hồ sơ người dùng. | | 4 | `try await Adapty.identify("YOUR_USER_ID")` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên lộ trình 2b có xác thực | Luôn `await`. Các lệnh gọi đồng thời trong quá trình `identify` sẽ tạo ra lỗi `#3006 profileWasChanged`. | | 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này sẽ gây ra mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về sai đối tượng. ::: ## Cài đặt web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng qua web checkout (Stripe, Paddle, FunnelFox) và sau đó cài đặt ứng dụng native, lần `activate()` đầu tiên trên thiết bị sẽ tạo một hồ sơ người dùng ẩn danh mới. Hồ sơ này không được liên kết với hồ sơ web. Nếu bạn có thể xác định customer user ID trước khi khởi chạy ứng dụng (từ luồng xác thực hoặc install referrer), hãy truyền trực tiếp vào `activate()`. Nếu không, giao dịch mua hàng trên web sẽ không hiển thị trên thiết bị cho đến khi bạn gọi `identify("YOUR_USER_ID")` và sau đó `restorePurchases`. Về metadata cần gửi kèm theo mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: ios-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong iOS SDK" description: "Tải paywall Adapty đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho iOS." --- Một lần tải paywall đáng tin cậy trên iOS cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng, và xử lý dự phòng tốt khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định `Adapty.activate()` và `Adapty.identify()` đã hoàn thành. Xem [Thứ tự gọi trong iOS SDK](ios-sdk-call-order). ::: ## Quy tắc và những lỗi thường gặp \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement mà bạn sắp hiển thị. | Prefetch tất cả các placement đồng thời khi khởi chạy. | Bulk prefetch chặn main thread và gây màn hình đen trong thời gian burst. | | Gọi `getPaywall` sau khi attribution có cơ hội hoàn tất — ví dụ, 1–2 giây sau `activate` hoặc sau khi `onProfileUpdate` được kích hoạt. | Gọi `getPaywall` tại `App.init()`. | Attribution chưa được xử lý xong. Paywall sẽ được phân giải theo đối tượng mặc định và bỏ qua các phân khúc cũng như cá nhân hóa ASA mà không có cảnh báo. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` vô thời hạn. | Nếu không có timeout, người dùng có kết nối kém sẽ thấy màn hình trống cho đến khi mạng phục hồi — hoặc họ sẽ đóng ứng dụng. | Xem [Tải paywalls và sản phẩm](fetch-paywalls-and-products) để tham khảo tham số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn đúng placement. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối kém liên tục (khu vực nông thôn, phương tiện công cộng, các vùng bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy: .returnCacheDataElseLoad` cho mọi lần tải ngoại trừ lần đầu tiên. - Cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement trong Adapty dashboard. - Đặt `loadTimeout` từ 3–5 giây và chấp nhận phương án dự phòng khi timeout xảy ra. - Không chặn việc hiển thị paywall vào `getProfile()`. Gọi `getPaywall` độc lập để một profile chậm không làm chặn giao diện. --- # File: ios-show-aa-targeted-paywall --- --- title: "Hiển thị paywall được nhắm mục tiêu bởi AA khi khởi chạy lần đầu trong iOS SDK" description: "Chờ attribution từ Apple Ads trước khi yêu cầu paywall trên iOS bằng AdaptyProfile.appliedAttributionSources." --- Attribution từ Apple Ads (AA) được nhận không đồng bộ sau khi gọi `Adapty.activate()`. Nếu bạn gọi `getPaywall` sớm, attribution thường chưa được ghi nhận và Adapty sẽ xử lý placement theo đối tượng mặc định — bỏ qua các paywall được phân khúc theo AA. `AdaptyProfile.appliedAttributionSources` cho phép ứng dụng phát hiện khi nào attribution AA đã được áp dụng vào hồ sơ người dùng, để yêu cầu paywall có thể chờ cho đến khi phân khúc AA được xử lý chính xác. ## Trước khi bắt đầu \{#before-you-start\} Bạn cần: - Adapty iOS SDK **3.17.1** trở lên. - Apple Ads đã được cấu hình cho ứng dụng trong Adapty. Xem [Apple Ads](apple-search-ads). ## Cách hoạt động \{#how-it-works\} Sau khi gọi `Adapty.activate()`, SDK sẽ yêu cầu attribution Apple Ads từ Apple trong nền và chuyển kết quả đến backend của Adapty. Khi AA trở thành nguồn attribution đang hoạt động cho hồ sơ người dùng, SDK sẽ cung cấp một `AdaptyProfile` đã được cập nhật có mảng `appliedAttributionSources` chứa `.appleAds`. Mảng rỗng có thể có nghĩa là: - Attribution Apple Ads chưa được xử lý cho hồ sơ người dùng này. - Chưa có attribution nào đến. Ngay cả với mảng rỗng, `getPaywall` vẫn an toàn để gọi — Adapty sẽ xử lý yêu cầu theo đối tượng phù hợp với trạng thái hồ sơ người dùng hiện tại, thường là đối tượng mặc định. :::important Việc chờ chỉ áp dụng cho **lần khởi chạy đầu tiên**. Sau khi attribution Apple Ads đã được ghi nhận, nó được lưu trữ vĩnh viễn trên hồ sơ người dùng. Ở mọi lần khởi chạy tiếp theo, hồ sơ đã được cache sẵn chứa `.appleAds` trong `appliedAttributionSources`, `didLoadLatestProfile` sẽ kích hoạt với giá trị đó ngay lập tức, và `getPaywall` sẽ trả về paywall được phân khúc theo Apple Ads mà không cần chờ đợi. ::: ## Triển khai \{#implementation\} Ở lần khởi chạy đầu tiên, hãy theo dõi `.appleAds` trong hồ sơ người dùng và áp dụng một timeout cứng — nếu attribution Apple Ads không bao giờ đến, những người dùng đó vẫn cần được xem paywall. 1. **Kích hoạt SDK.** Xem [Cài đặt & cấu hình iOS SDK](sdk-installation-ios). 2. **Đăng ký nhận cập nhật hồ sơ người dùng** bằng cách tuân thủ `AdaptyDelegate` và triển khai `didLoadLatestProfile`. Nếu bạn chưa thiết lập delegate, xem [Lắng nghe cập nhật gói đăng ký](ios-check-subscription-status#listen-to-subscription-updates). 3. **Theo dõi `.appleAds` trong `appliedAttributionSources`.** Khi nó xuất hiện, hãy yêu cầu paywall — Adapty sẽ trả về biến thể được phân khúc theo AA: ```swift extension <YourAdaptyDelegateImpl>: AdaptyDelegate { nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { if profile.appliedAttributionSources.contains(where: { $0 == .appleAds }) { // load paywall via Adapty.getPaywall(placementId:) } } } ``` 4. **Bắt đầu một bộ đếm thời gian 3–5 giây song song với việc đăng ký.** Nếu bộ đếm thời gian kích hoạt trước khi `.appleAds` xuất hiện, hãy yêu cầu paywall ngay: Bất kỳ đường dẫn nào kích hoạt trước đều nên tải paywall; đường dẫn còn lại nên bị bỏ qua. Sử dụng một cờ trạng thái duy nhất (ví dụ: `hasLoadedPaywall`) để loại trùng lặp để tránh tải paywall hai lần. Cấu hình [paywall dự phòng](fallback-paywalls) cho placement để người dùng không bao giờ bị kẹt nếu yêu cầu mạng thất bại. ## Ví dụ đầy đủ \{#complete-example\} Triển khai bên dưới chạy đua attribution với timeout, tải trước paywall của đối tượng mặc định song song, và trả về paywall phù hợp. Người gọi chỉ cần await một hàm async duy nhất — không cần quản lý delegate hay cờ trạng thái ở phía gọi. `ProfileObserver` là một singleton có thể tái sử dụng, phát các cập nhật hồ sơ người dùng từ `AdaptyDelegate`. `PaywallLoader.getPaywallOrDefault` chạy cuộc đua bằng cách sử dụng `TaskGroup` với structured concurrency: - Nếu attribution đến trong `timeout`, nó trả về paywall được phân khúc qua `getPaywall(placementId:)`. - Nếu `timeout` hết hạn trước, nó trả về paywall đối tượng mặc định đã được tải trước qua `getPaywallForDefaultAudience(placementId:)`. ```swift title="PaywallLoader.swift" /// Demonstrates how to fetch a paywall that depends on attribution being applied, /// falling back to the default-audience paywall if attribution doesn't arrive in time. /// /// Stateless and self-contained: every call kicks off its own default-audience /// prefetch and races it against attribution + segmented fetch. enum PaywallLoader { static func getPaywallOrDefault( placementId: String, timeout: TimeInterval ) async throws -> AdaptyPaywall { struct TimedOut: Error {} // Kick off the default-audience request immediately so it has the full // `timeout` window to load. We'll either cancel it on success or await // its result on timeout — never a duplicate network call. let defaultPaywallTask = Task { try await Adapty.getPaywallForDefaultAudience(placementId: placementId) } do { // Race two child tasks: whichever finishes first wins. let result = try await withThrowingTaskGroup(of: AdaptyPaywall.self) { group in // 1. Wait for attribution, then ask Adapty for the segmented paywall. group.addTask { await waitForAttribution() return try await Adapty.getPaywall(placementId: placementId) } // 2. Time-bomb: throws `TimedOut` after `timeout` seconds. group.addTask { try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) throw TimedOut() } guard let value = try await group.next() else { throw CancellationError() } group.cancelAll() // stop the loser (sleeper or the attribution wait). return value } // Segmented paywall won — we no longer need the default-audience prefetch. defaultPaywallTask.cancel() return result } catch is TimedOut { // Attribution didn't apply in time — return the prefetched default // (instant if already done, otherwise we await the in-flight request). return try await defaultPaywallTask.value } } /// Suspends until a profile with the desired attribution source is observed. /// `@Published.values` emits the current profile immediately on subscription, /// so this returns on the first iteration if attribution is already applied. @MainActor private static func waitForAttribution() async { for await profile in ProfileObserver.shared.$profile.values { if profile?.appliedAttributionSources.contains(.appleAds) == true { return } } } } @MainActor final class ProfileObserver: AdaptyDelegate { static let shared = ProfileObserver() @Published private(set) var profile: AdaptyProfile? nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { Task { @MainActor [weak self] in self?.profile = profile } } } ``` Kết nối `ProfileObserver` vào `AdaptyDelegate` một lần, sau khi `Adapty.activate()` hoàn tất: ```swift Adapty.delegate = ProfileObserver.shared ``` Gọi từ màn hình splash: ```swift do { let paywall = try await PaywallLoader.getPaywallOrDefault( placementId: "YOUR_PLACEMENT_ID", timeout: 5 ) // present the paywall } catch { // handle the error or show a fallback paywall } ``` Nếu ứng dụng của bạn đã sử dụng `AdaptyDelegate` cho các mục đích khác (ví dụ: [lắng nghe cập nhật gói đăng ký](ios-check-subscription-status#listen-to-subscription-updates)), hãy chuyển tiếp `didLoadLatestProfile` đến `ProfileObserver.shared` từ delegate hiện có của bạn thay vì đặt `Adapty.delegate = ProfileObserver.shared`. --- # File: ios-test --- --- title: "Kiểm tra & phát hành trong iOS SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng iOS của bạn với Adapty." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng iOS của mình, hãy kiểm tra xem mọi thứ đã được thiết lập đúng chưa và các giao dịch mua có hoạt động như mong đợi không. Quá trình này bao gồm kiểm tra cả tích hợp SDK lẫn giao dịch mua thực tế. ## Kiểm tra ứng dụng của bạn \{#test-your-app\} Để kiểm tra toàn diện các in-app purchase, bao gồm kiểm tra trong sandbox và xác thực TestFlight, hãy xem [hướng dẫn kiểm tra](test-purchases-in-sandbox) của chúng tôi. ## Chuẩn bị phát hành \{#prepare-for-release\} Trước khi gửi ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối cửa hàng và thông báo máy chủ đã được cấu hình - Giao dịch mua hoàn tất và được báo cáo về Adapty - Mức độ truy cập được mở khóa và khôi phục đúng cách - Các yêu cầu về quyền riêng tư và xem xét đã được đáp ứng --- # File: InvalidProductIdentifiers --- --- title: "Cách sửa lỗi Code-1000 noProductIDsFound" description: "Giải quyết lỗi mã định danh sản phẩm không hợp lệ khi quản lý gói đăng ký trong Adapty." --- Lỗi mã 1000, `noProductIDsFound`, có nghĩa là không có sản phẩm nào bạn yêu cầu trên paywall sẵn sàng để mua trong App Store, dù chúng đã được liệt kê ở đó. Đôi khi lỗi này đi kèm với cảnh báo `InvalidProductIdentifiers`. Nếu cảnh báo xuất hiện mà không có lỗi, bạn có thể bỏ qua an toàn. Nếu bạn gặp lỗi `noProductIDsFound`, hãy làm theo các bước sau để khắc phục: ## Bước 1. Kiểm tra bundle ID \{#step-2-check-bundle-id\} 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**. <Zoom> <img src="/docs/img/afd5012-bundle_id_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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**. <Zoom> <img src="/docs/img/2d64163-bundle_id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 4. Quay lại trang **App information** trong App Store Connect và sao chép **Apple ID** tại đó. 5. Trên trang [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) trong Adapty dashboard, dán ID vào trường **Apple app ID**. ## Bước 2. Kiểm tra sản phẩm \{#step-2-check-products\} 1. Vào **App Store Connect** và điều hướng đến [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) trong menu bên trái. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào tên nhóm gói đăng ký. Bạn sẽ thấy các sản phẩm được liệt kê trong mục **Subscriptions**. 3. Đảm bảo sản phẩm bạn đang kiểm tra được đánh dấu là **Ready to Submit**. Nếu chưa, hãy làm theo hướng dẫn trên trang [Product in App Store](app-store-products). <img src="/assets/shared/img/ready-to-submit.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. So sánh ID sản phẩm trong bảng với ID trong tab [**Products**](https://app.adapty.io/products) trên Adapty Dashboard. Nếu các ID không khớp, hãy sao chép ID sản phẩm từ bảng và [tạo sản phẩm](create-product) với ID đó trong Adapty Dashboard. <img src="/assets/shared/img/product-id-copy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 3. Kiểm tra tính khả dụng của sản phẩm \{#step-4-check-product-availability\} 1. Quay lại **App Store Connect** và mở lại mục **Subscriptions**. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống mục **Availability** và kiểm tra rằng tất cả các quốc gia và khu vực cần thiết đều được liệt kê. <img src="/assets/shared/img/product-availability.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 4. Kiểm tra giá sản phẩm \{#step-5-check-product-prices\} 1. Quay lại mục **Monetization** → **Subscriptions** trong **App Store Connect**. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống **Subscription Pricing** và mở rộng mục **Current Pricing for New Subscribers**. <img src="/assets/shared/img/check-prices.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Đảm bảo tất cả các mức giá cần thiết đều được liệt kê. <img src="/assets/shared/img/product-pricing.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 5. Kiểm tra trạng thái thanh toán, tài khoản ngân hàng và biểu mẫu thuế còn hiệu lực \{#step-5-check-app-paid-status-bank-account-and-tax-forms-are-active\} 1. Trên trang chủ [**App Store Connect**](https://appstoreconnect.apple.com/), nhấp vào **Business**. <img src="/assets/shared/img/business.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Chọn tên công ty của bạn. <img src="/assets/shared/img/business-name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Cuộn xuống và kiểm tra rằng **Paid Apps Agreement**, **Bank Account** và **Tax forms** đều hiển thị trạng thái **Active**. <img src="/assets/shared/img/appstore-connect-status.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bằng cách làm theo các bước trên, bạn sẽ có thể khắc phục cảnh báo `InvalidProductIdentifiers` và đưa sản phẩm lên cửa hàng. ## Bước 6. Tạo lại sản phẩm nếu bị kẹt \{#step-6-recreate-the-product-if-its-stuck\} Các bước 1–5 có thể đều vượt qua — trạng thái `Approved`, Bundle ID khớp, API key hợp lệ — nhưng SDK vẫn trả về lỗi `1000 noProductIDsFound`. Trong trường hợp đó, sản phẩm có thể đang bị kẹt trong registry của Apple. Registry sản phẩm của Apple đôi khi rơi vào trạng thái mà sản phẩm tồn tại trong giao diện App Store Connect nhưng không hiển thị qua đường dẫn tra cứu StoreKit. Hãy xóa sản phẩm trong App Store Connect và tạo lại với cùng ID sản phẩm đó. Sau khi tạo lại, hãy chờ tối đa 24 giờ để thay đổi được áp dụng. --- # File: cantMakePayments --- --- title: "Khắc phục lỗi Code-1003 cantMakePayment" description: "Giải quyết lỗi thanh toán khi quản lý gói đăng ký trong Adapty." --- Lỗi 1003, `cantMakePayments`, cho biết thiết bị không thể thực hiện in-app purchase. Nếu bạn gặp lỗi `cantMakePayments`, thường là do một trong các nguyên nhân sau: - Giới hạn thiết bị: Lỗi này không liên quan đến Adapty. Xem cách khắc phục bên dưới. - Cấu hình Observer mode: Không thể dùng đồng thời phương thức `makePurchase` và Observer mode. Xem phần bên dưới. ## Sự cố: Giới hạn thiết bị \{#issue-device-restrictions\} | Sự cố | Giải pháp | |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------| | Giới hạn Screen Time | Tắt giới hạn In-App Purchase trong [Screen Time](https://support.apple.com/en-us/102470) | | Tài khoản bị tạm khóa | Liên hệ Apple Support để giải quyết vấn đề tài khoản | | Giới hạn khu vực | Sử dụng tài khoản App Store từ vùng được hỗ trợ | ## Sự cố: Dùng đồng thời Observer mode và makePurchase \{#issue-using-both-observer-mode-and-makepurchase\} Nếu bạn đang dùng `makePurchases` để xử lý giao dịch mua, bạn không cần dùng Observer mode. [Observer mode](observer-vs-full-mode) chỉ cần thiết khi bạn tự triển khai logic mua hàng. Vì vậy, nếu bạn đang dùng `makePurchase`, bạn có thể xóa phần kích hoạt Observer mode khỏi code khởi tạo SDK một cách an toàn. --- # File: migration-to-ios-sdk-v4 --- --- title: "Migrate Adapty iOS SDK sang v4.0" description: "Migrate sang Adapty iOS SDK v4.0 bằng cách thay thế paywall API bằng flow API, tương thích với cả Flow Builder và Paywall Builder." --- Adapty iOS SDK 4.0 giới thiệu flows và đổi tên các paywall API cho phù hợp. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi cấu hình nào trên Adapty Dashboard. ## Tham chiếu nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `Adapty.getPaywall(placementId:locale:)` | `Adapty.getFlow(placementId:)` | | `AdaptyUI.getPaywallConfiguration(forPaywall:)` | `AdaptyUI.getFlowConfiguration(forFlow:locale:)` | | `Adapty.getPaywallProducts(paywall:)` | `Adapty.getPaywallProducts(flow:)` | | `Adapty.logShowPaywall(_:)` | `Adapty.logShowFlow(_:)` | | `AdaptyPaywallController` | `AdaptyFlowController` | | `AdaptyPaywallControllerDelegate` | `AdaptyFlowControllerDelegate` | | `AdaptyUI.paywallController(with:delegate:)` | `AdaptyUI.flowController(with:delegate:)` | | `.paywall()` (SwiftUI modifier) | `.flow()` | | `AdaptyPaywallView` | `AdaptyFlowView` | | `didFailRenderingWith:` / `didFailRendering:` | `didReceiveError:` | | `didFinishPurchase` (tùy chọn, tự động đóng khi thành công) | `didFinishPurchase` (bắt buộc, không tự động đóng) | | Sản phẩm package `Adapty_KidsMode` / `AdaptyUI_KidsMode` | Package trait `KidsMode` | | `Adapty.updateAttribution(_:source:)` (`source: String`) | `Adapty.updateAttribution(_:source:)` (`source: AdaptyAttributionSource`) | | `Adapty.setIntegrationIdentifier(key:value:)` | `Adapty.setIntegrationIdentifier(_:)` (`AdaptyIntegrationIdentifier`) | ## Phiên bản iOS tối thiểu \{#minimum-ios-version\} Adapty iOS SDK 4.0 nâng yêu cầu deployment target tối thiểu từ iOS 13.0 lên **iOS 15.0**. Hãy đặt iOS Deployment Target của dự án lên 15.0 hoặc cao hơn trước khi nâng cấp. ## Cài đặt: CocoaPods không còn được hỗ trợ \{#installation-cocoapods-no-longer-supported\} Adapty iOS SDK 4.0 không còn hỗ trợ CocoaPods. Hãy cài đặt SDK bằng [Swift Package Manager](sdk-installation-ios#install-adapty-sdk). Nếu dự án của bạn vẫn đang dùng CocoaPods, hãy xóa các pod `Adapty` và `AdaptyUI` khỏi `Podfile`, chạy `pod install` để dọn dẹp, sau đó thêm package trong Xcode qua **File → Add Package Dependency** với đường dẫn `https://github.com/adaptyteam/AdaptySDK-iOS.git`. ## Kids Mode: sản phẩm riêng biệt được thay thế bằng package trait \{#kids-mode-separate-products-replaced-by-a-package-trait\} Trong v3, bạn bật [Kids Mode](kids-mode) bằng cách chọn các sản phẩm package riêng biệt là **Adapty_KidsMode** và **AdaptyUI_KidsMode**, đồng thời đổi tên các import. Trong v4.0, các sản phẩm này đã bị loại bỏ. Kids Mode hiện là một Swift package trait có tên `KidsMode` trên package Adapty thông thường — khi bật, nó sẽ loại bỏ IDFA và AdSupport khỏi toàn bộ SDK trong quá trình biên dịch. Để migrate: 1. Trong cửa sổ **Choose Package Products**, chọn sản phẩm **Adapty** và **AdaptyUI** thông thường thay vì **Adapty_KidsMode** và **AdaptyUI_KidsMode**. 2. Bật trait `KidsMode`. Trong Xcode 26.4 trở lên, bật trait này cho dependency AdaptySDK-iOS trong phần **Package Dependencies** của project. Nếu bạn thêm Adapty làm dependency trong `Package.swift` (yêu cầu `swift-tools-version` 6.1 trở lên), hãy bật tại đó: ```swift showLineNumbers title="Package.swift" .package( url: "https://github.com/adaptyteam/AdaptySDK-iOS.git", from: "4.0.0", traits: ["KidsMode"] ) ``` 3. Đổi lại các import về module thông thường: ```diff showLineNumbers - import Adapty_KidsMode - import AdaptyUI_KidsMode + import Adapty + import AdaptyUI ``` :::note Các phiên bản Xcode trước 26.4 không thể bật trait cho dự án Xcode từ giao diện người dùng. Trong trường hợp đó, hãy tạo một Swift package nội bộ nhỏ phụ thuộc vào Adapty với trait `KidsMode` được bật, rồi để target ứng dụng của bạn phụ thuộc vào package đó. ::: ## Các API đã bị xóa \{#removed-apis\} - **`Adapty.getPaywallProductsWithoutDeterminingOffer(paywall:)`** — đã bị xóa. Tất cả sản phẩm hiện đã bao gồm thông tin ưu đãi, vì vậy không cần thêm bước kiểm tra tính đủ điều kiện riêng nữa. - **`AdaptyPaywallProductWithoutDeterminingOffer`** — đã bị xóa. Các callback trước đây truyền kiểu này (chẳng hạn như `didSelectProduct`) nay sẽ truyền `AdaptyPaywallProduct`. ## Tính năng in-app purchase được quảng bá trên App Store tạm thời bị gỡ bỏ \{#app-store-promoted-in-app-purchases-temporarily-removed\} Trong quá trình migrate sang StoreKit 2, Adapty iOS SDK 4.0 loại bỏ hỗ trợ cho in-app purchase được quảng bá trên App Store. Phương thức delegate `shouldAddStorePayment(for:)` và kiểu `AdaptyDeferredProduct` mà nó nhận đều không khả dụng trong phiên bản 4.0. :::warning Việc gỡ bỏ này chỉ là tạm thời — hỗ trợ in-app purchase được quảng bá sẽ trở lại trong một bản phát hành 4.x sau này. Nếu ứng dụng của bạn phụ thuộc vào tính năng này, hãy tiếp tục sử dụng iOS SDK 3.x cho đến khi hỗ trợ được khôi phục. ::: ## Lấy paywalls \{#fetching-paywalls\} ### getPaywall + getPaywallConfiguration → getFlow + getFlowConfiguration Các kiểu trả về thay đổi từ `AdaptyPaywall` / `AdaptyUI.PaywallConfiguration` sang `AdaptyFlow` / `AdaptyUI.FlowConfiguration`. Tham số `locale` được chuyển ra khỏi lệnh gọi fetch và đưa vào `getFlowConfiguration`: ```diff showLineNumbers - let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") - let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall) + let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") + let flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow, locale: "en") ``` ### getPaywallProducts(paywall:) → getPaywallProducts(flow:) `getPaywallProducts` hiện nhận một `AdaptyFlow` được trả về bởi `Adapty.getFlow`: ```diff showLineNumbers - let products = try await Adapty.getPaywallProducts(paywall: paywall) + let products = try await Adapty.getPaywallProducts(flow: flow) ``` ## Theo dõi lượt xem paywall \{#tracking-paywall-views\} ### logShowPaywall(_:) → logShowFlow(_:) `logShowPaywall` được đổi tên thành `logShowFlow` và nhận vào một `AdaptyFlow` thay vì `AdaptyPaywall`. Sự kiện vẫn được ghi lại theo cùng một biến thể, do đó các chỉ số funnel và A/B test hiện có vẫn hoạt động bình thường mà không cần thay đổi gì trên dashboard. ```diff showLineNumbers - try await Adapty.logShowPaywall(paywall) + try await Adapty.logShowFlow(flow) ``` Như trong v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được render bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi các lượt xem đó. ## didFinishPurchase hiện là bắt buộc \{#didfinishpurchase-is-now-required\} Trong v3, `didFinishPurchase` là tùy chọn: nếu bạn không triển khai nó, paywall sẽ tự đóng sau khi mua thành công. Trong v4.0, cơ chế tự động đóng mặc định này đã bị xóa để flow có thể tiếp tục sau khi mua thành công — ví dụ: để hiển thị các màn hình còn lại trong flow của bạn. Bây giờ bạn tự quyết định điều gì xảy ra sau khi mua: đóng màn hình, hoặc không làm gì để flow tiếp tục. - **UIKit**: Những đối tượng tuân theo `AdaptyFlowControllerDelegate` phải triển khai `didFinishPurchase` — phương thức này không còn có phần triển khai mặc định nữa. - **SwiftUI**: closure `didFinishPurchase` của `.flow(...)` và `AdaptyFlowView(...)` giờ là bắt buộc (non-optional), giống với `didFailPurchase` và `didFinishRestore`. Để giữ nguyên hành vi của v3, hãy tự dismiss màn hình: ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didFinishPurchase product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { if !purchaseResult.isPurchaseCancelled { controller.dismiss(animated: true) } } ``` ## UIKit \{#uikit\} ### AdaptyPaywallController → AdaptyFlowController Đổi tên kiểu controller và factory method: ```diff showLineNumbers - let controller = try AdaptyUI.paywallController( - with: paywallConfiguration, - delegate: self - ) + let controller = try AdaptyUI.flowController( + with: flowConfiguration, + delegate: self + ) ``` ### AdaptyPaywallControllerDelegate → AdaptyFlowControllerDelegate Đổi tên protocol và cập nhật tất cả các method signature. Lưu ý rằng `didSelectProduct` hiện nhận `AdaptyPaywallProduct` thay vì `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xóa, và `didFinishPurchase` [phải được implement](#didfinishpurchase-is-now-required) — nó không còn có default implementation nữa. ```diff showLineNumbers - class YourClass: AdaptyPaywallControllerDelegate { + class YourClass: AdaptyFlowControllerDelegate { - func paywallControllerDidAppear(_ controller: AdaptyPaywallController) { } + func flowControllerDidAppear(_ controller: AdaptyFlowController) { } - func paywallControllerDidDisappear(_ controller: AdaptyPaywallController) { } + func flowControllerDidDisappear(_ controller: AdaptyFlowController) { } - func paywallController(_ controller: AdaptyPaywallController, - didPerform action: AdaptyUI.Action) { } + func flowController(_ controller: AdaptyFlowController, + didPerform action: AdaptyUI.Action) { } - func paywallController(_ controller: AdaptyPaywallController, - didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer) { } + func flowController(_ controller: AdaptyFlowController, + didSelectProduct product: AdaptyPaywallProduct) { } - func paywallController(_ controller: AdaptyPaywallController, - didStartPurchase product: AdaptyPaywallProduct) { } + func flowController(_ controller: AdaptyFlowController, + didStartPurchase product: AdaptyPaywallProduct) { } - func paywallController(_ controller: AdaptyPaywallController, - didFinishPurchase product: AdaptyPaywallProduct, - purchaseResult: AdaptyPurchaseResult) { } + func flowController(_ controller: AdaptyFlowController, + didFinishPurchase product: AdaptyPaywallProduct, + purchaseResult: AdaptyPurchaseResult) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailPurchase product: AdaptyPaywallProduct, - error: AdaptyError) { } + func flowController(_ controller: AdaptyFlowController, + didFailPurchase product: AdaptyPaywallProduct, + error: AdaptyError) { } - func paywallControllerDidStartRestore(_ controller: AdaptyPaywallController) { } + func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { } - func paywallController(_ controller: AdaptyPaywallController, - didFinishRestoreWith profile: AdaptyProfile) { } + func flowController(_ controller: AdaptyFlowController, + didFinishRestoreWith profile: AdaptyProfile) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailRestoreWith error: AdaptyError) { } + func flowController(_ controller: AdaptyFlowController, + didFailRestoreWith error: AdaptyError) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailRenderingWith error: AdaptyUIError) { } + func flowController(_ controller: AdaptyFlowController, + didReceiveError error: AdaptyUIError) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailLoadingProductsWith error: AdaptyError) -> Bool { } + func flowController(_ controller: AdaptyFlowController, + didFailLoadingProductsWith error: AdaptyError) -> Bool { } - func paywallController(_ controller: AdaptyPaywallController, - didPartiallyLoadProducts failedIds: [String]) { } + func flowController(_ controller: AdaptyFlowController, + didPartiallyLoadProducts failedIds: [String]) { } - func paywallController(_ controller: AdaptyPaywallController, - didFinishWebPaymentNavigation product: AdaptyPaywallProduct?, - error: AdaptyError?) { } + func flowController(_ controller: AdaptyFlowController, + didFinishWebPaymentNavigation product: AdaptyPaywallProduct?, + error: AdaptyError?) { } } ``` ## SwiftUI \{#swiftui\} ### Modifier `.paywall()` → `.flow()` \{#paywall-modifier--flow\} Đổi tên modifier, cập nhật tên tham số cấu hình, và thêm closure `didFinishPurchase` [bắt buộc](#didfinishpurchase-is-now-required): ```diff showLineNumbers @State var flowPresented = false // rename freely — the variable name is your choice var body: some View { Text("Hello, AdaptyUI!") - .paywall( + .flow( isPresented: $flowPresented, - paywallConfiguration: paywallConfiguration, + flowConfiguration: flowConfiguration, + didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, - didFailRendering: { error in flowPresented = false } + didReceiveError: { error in flowPresented = false } ) } ``` Callback được đổi tên này kích hoạt cho những lỗi render giống như `didFailRendering` trước đây, cộng thêm các lỗi runtime mới từ flow script (JavaScript exception với mã `AdaptyUIError` là `4105` — `.jsException`). Phần thân handler hiện có không cần thay đổi code — chỉ cần đổi tên tham số. ### AdaptyPaywallView → AdaptyFlowView Đổi tên view, cập nhật tham số cấu hình, thêm closure `didFinishPurchase` [bắt buộc](#didfinishpurchase-is-now-required), và cập nhật closure `didSelectProduct` nếu có — giờ đây nó nhận `AdaptyPaywallProduct` thay cho `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xóa: ```diff showLineNumbers - AdaptyPaywallView( - paywallConfiguration: paywallConfiguration, - didSelectProduct: { product: AdaptyPaywallProductWithoutDeterminingOffer in /* handle */ }, + AdaptyFlowView( + flowConfiguration: flowConfiguration, + didSelectProduct: { product: AdaptyPaywallProduct in /* handle */ }, + didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, - didFailRendering: { error in /* handle the error */ } + didReceiveError: { error in /* handle the error */ } ) ``` ## Tài nguyên tùy chỉnh của AdaptyUI \{#adaptyui-custom-assets\} ### AdaptyUICustomVideoAsset Có hai thay đổi ảnh hưởng đến tất cả các call site hiện có: - `.player` hiện nhận `AVPlayer` thay vì `AVQueuePlayer`. - Mỗi case đều có thêm tham số `resolution: CGSize?` ở cuối. Truyền `nil` để giữ nguyên hành vi hiện tại, hoặc truyền kích thước pixel thực tế để player có thể dành chỗ bố cục (tỉ lệ khung hình = `width / height`) trước khi video tải xong. ```diff showLineNumbers - case file(url: URL, preview: AdaptyUICustomImageAsset?) - case remote(url: URL, preview: AdaptyUICustomImageAsset?) - case player(item: AVPlayerItem, player: AVQueuePlayer, preview: AdaptyUICustomImageAsset?) + case file(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?) + case remote(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?) + case player(item: AVPlayerItem, player: AVPlayer, preview: AdaptyUICustomImageAsset?, resolution: CGSize?) ``` ## Định danh attribution và tích hợp \{#attribution-and-integration-identifiers\} ### updateAttribution(_:source:) Tham số `source` thay đổi từ kiểu `String` sang kiểu `AdaptyAttributionSource` mới, và `AdaptyProfile.AttributionSource` trước đây được đặt lồng nhau nay được đổi tên thành `AdaptyAttributionSource` ở cấp độ top-level. Sử dụng một trong các source được định nghĩa sẵn, hoặc truyền một string literal cho bất kỳ source nào khác — `AdaptyAttributionSource` tuân theo `ExpressibleByStringLiteral`, vì vậy các lệnh gọi dùng string literal hiện có vẫn tiếp tục biên dịch được. ```diff showLineNumbers - try await Adapty.updateAttribution(attribution, source: "adjust") + try await Adapty.updateAttribution(attribution, source: .adjust) ``` Nguồn được định sẵn: `.appleAds`, `.adjust`, `.appsflyer`, `.branch`, `.tenjin`. Nếu bạn lưu nguồn trong biến `String`, hãy bọc nó lại: `AdaptyAttributionSource(rawValue: yourSource)`. ### setIntegrationIdentifier(_:) `setIntegrationIdentifier(key:value:)` được thay thế bằng một phương thức variadic nhận một hoặc nhiều giá trị `AdaptyIntegrationIdentifier`. Hãy dùng các factory method có sẵn thay vì chuỗi key thô: ```diff showLineNumbers - try await Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid) + try await Adapty.setIntegrationIdentifier(.appsflyerId(uid)) ``` Bạn có thể đặt nhiều identifier trong một lần gọi: ```swift showLineNumbers try await Adapty.setIntegrationIdentifier( .appsflyerId(uid), .adjustDeviceId(adid) ) ``` Thay thế từng chuỗi key cũ bằng factory method tương ứng: | v3 key | v4 factory | |---|---| | `"adjust_device_id"` | `.adjustDeviceId(_:)` | | `"airbridge_device_id"` | `.airbridgeDeviceId(_:)` | | `"amplitude_user_id"` | `.amplitudeUserId(_:)` | | `"amplitude_device_id"` | `.amplitudeDeviceId(_:)` | | `"appmetrica_device_id"` | `.appmetricaDeviceId(_:)` | | `"appmetrica_profile_id"` | `.appmetricaProfileId(_:)` | | `"appsflyer_id"` | `.appsflyerId(_:)` | | `"branch_id"` | `.branchId(_:)` | | `"facebook_anonymous_id"` | `.facebookAnonymousId(_:)` | | `"firebase_app_instance_id"` | `.firebaseAppInstanceId(_:)` | | `"mixpanel_user_id"` | `.mixpanelUserId(_:)` | | `"one_signal_subscription_id"` | `.oneSignalSubscriptionId(_:)` | | `"one_signal_player_id"` | `.oneSignalPlayerId(_:)` | | `"posthog_distinct_user_id"` | `.posthogDistinctUserId(_:)` | | `"pushwoosh_hwid"` | `.pushwooshHWID(_:)` | | `"tenjin_analytics_installation_id"` | `.tenjinAnalyticsInstallationId(_:)` | --- # File: migration-to-ios-315 --- --- title: "Migrate Adapty iOS SDK to v3.15" description: "Migrate to Adapty iOS SDK v3.15 for better performance and new monetization features." --- Nếu bạn đang dùng [Paywall Builder](adapty-paywall-builder) trong [Observer mode](observer-vs-full-mode), bắt đầu từ iOS SDK 3.15, bạn cần triển khai một phương thức mới là `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)`. Phương thức này cung cấp khả năng kiểm soát chi tiết hơn đối với logic khôi phục, cho phép bạn xử lý việc khôi phục giao dịch mua trong flow tùy chỉnh của mình. Để biết chi tiết triển khai đầy đủ, tham khảo [Hiển thị paywall Paywall Builder trong Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode). ```diff showLineNumbers func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } + func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, + onFinishRestore: @escaping () -> Void) { + // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore + } ``` --- # File: migration-to-ios-sdk-34 --- --- title: "Migrate Adapty iOS SDK to v3.4" description: "Migrate to Adapty iOS SDK v3.4 for better performance and new monetization features." --- Adapty SDK 3.4.0 là một bản phát hành lớn, giới thiệu các cải tiến yêu cầu bạn thực hiện các bước migration. ## Cập nhật khởi tạo Adapty SDK \{#update-adapty-sdk-activation\} <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```diff showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") - Adapty.activate(with: configurationBuilder) { error in + Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` **Cập nhật file paywall dự phòng** Cập nhật các file paywall dự phòng để đảm bảo tương thích với phiên bản SDK mới: 1. [Tải xuống các file paywall dự phòng đã được cập nhật](fallback-paywalls) từ Adapty Dashboard. 2. [Thay thế các paywall dự phòng hiện có trong ứng dụng mobile của bạn](ios-use-fallback-paywalls) bằng các file mới. </TabItem> <TabItem value="swiftui" label="SwiftUI" default> ```diff showLineNumbers @main struct SampleApp: App { init() { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") Task { - try await Adapty.activate(with: configurationBuilder) + try await Adapty.activate(with: configurationBuilder.build()) } } var body: some Scene { WindowGroup { ContentView() } } } ``` **Cập nhật file paywall dự phòng** Cập nhật các file paywall dự phòng để đảm bảo tương thích với phiên bản SDK mới: 1. [Tải xuống các file paywall dự phòng đã được cập nhật](fallback-paywalls) từ Adapty Dashboard. 2. [Thay thế các paywall dự phòng hiện có trong ứng dụng mobile của bạn](ios-use-fallback-paywalls) bằng các file mới. </TabItem> </Tabs> --- # File: migration-to-ios330 --- --- title: "Migrate Adapty iOS SDK sang v3.3" description: "Migrate lên Adapty iOS SDK v3.3 để cải thiện hiệu năng và sử dụng các tính năng kiếm tiền mới." --- Adapty SDK 3.3.0 là một bản phát hành lớn mang lại nhiều cải tiến, tuy nhiên bạn có thể cần thực hiện một số bước migration. 1. Đổi tên `Adapty.Configuration` thành `AdaptyConfiguration`. 2. Đổi tên phương thức `getViewConfiguration` thành `getPaywallConfiguration`. 3. Xóa các tham số `didCancelPurchase` và `paywall` khỏi SwiftUI, đồng thời đổi tên tham số `viewConfiguration` thành `paywallConfiguration`. 4. Cập nhật cách xử lý promotional in-app purchase từ App Store bằng cách xóa tham số `defermentCompletion` khỏi phương thức `AdaptyDelegate`. 5. Xóa phương thức `getProductsIntroductoryOfferEligibility`. 6. Cập nhật cấu hình tích hợp cho Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase và Google Analytics, Mixpanel, OneSignal, Pushwoosh. 7. Cập nhật cách triển khai Observer mode. <div style={{ textAlign: 'center' }}> <iframe width="560" height="315" src="https://www.youtube.com/embed/9Xs8d0lt_RY?si=xvWhUO2tlG1tKP5f" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen> </iframe> </div> ## Đổi tên Adapty.Configuration thành AdaptyConfiguration \{#rename-adaptyconfiguration-to-adaptyconfiguration\} Cập nhật code khởi động Adapty iOS SDK như sau: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```diff showLineNumbers // In your AppDelegate class: let configurationBuilder = - Adapty.Configuration + AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) .with(customerUserId: "YOUR_USER_ID") .with(idfaCollectionDisabled: false) .with(ipAddressCollectionDisabled: false) Adapty.activate(with: configurationBuilder) { error in // handle the error } ``` </TabItem> <TabItem value="swiftui" label="SwiftUI" default> ```diff showLineNumbers @main struct SampleApp: App { init() let configurationBuilder = - Adapty.Configuration + AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional Task { try await Adapty.activate(with: configurationBuilder) } } var body: some Scene { WindowGroup { ContentView() } } } ``` </TabItem> </Tabs> ## Đổi tên phương thức getViewConfiguration thành getPaywallConfiguration \{#rename-getviewconfiguration-method-to-getpaywallconfiguration\} Cập nhật tên phương thức để lấy `viewConfiguration` của paywall: ```diff showLineNumbers guard paywall.hasViewConfiguration else { // use your custom logic return } do { - let paywallConfiguration = try await AdaptyUI.getViewConfiguration( + let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall ) // use loaded configuration } catch { // handle the error } ``` Để biết thêm chi tiết về phương thức này, hãy xem [Lấy view configuration của paywall được thiết kế bằng Paywall Builder](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). ## Thay đổi tham số trong SwiftUI \{#change-parameters-in-swiftui\} Các cập nhật sau đã được thực hiện với SwiftUI: 1. Tham số `didCancelPurchase` đã bị xóa. Hãy dùng `didFinishPurchase` thay thế. 2. Phương thức `.paywall()` không còn nhận đối tượng paywall nữa. 3. Tham số `paywallConfiguration` đã thay thế tham số `viewConfiguration`. Cập nhật code của bạn như sau: ```diff showLineNumbers @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, - paywall: <paywall object>, - viewConfiguration: <LocalizedViewConfiguration>, + paywallConfiguration: <AdaptyUI.PaywallConfiguration>, didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, - didFinishPurchase: { product, profile in paywallPresented = false }, + didFinishPurchase: { product, purchaseResult in /* handle the result*/ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } - didCancelPurchase: { product in /* handle the result*/} ) } ``` ## Cập nhật cách xử lý promotional in-app purchase từ App Store \{#update-handling-of-promotional-in-app-purchases-from-app-store\} Cập nhật cách xử lý promotional in-app purchase từ App Store bằng cách xóa tham số `defermentCompletion` khỏi phương thức `AdaptyDelegate`, như ví dụ dưới đây: ```swift showLineNumbers title="Swift" final class YourAdaptyDelegateImplementation: AdaptyDelegate { nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool { // 1a. // Return `true` to continue the transaction in your app. // 1b. // Store the product object and return `false` to defer or cancel the transaction. false } // 2. Continue the deferred purchase later on by passing the product to `makePurchase` func continueDeferredPurchase() async { let storedProduct: AdaptyDeferredProduct = // get the product object from the 1b. do { try await Adapty.makePurchase(product: storedProduct) } catch { // handle the error } } } ``` ## Xóa phương thức getProductsIntroductoryOfferEligibility \{#remove-getproductsintroductoryoffereligibility-method\} Trước Adapty iOS SDK 3.3.0, đối tượng sản phẩm luôn bao gồm các ưu đãi, bất kể người dùng có đủ điều kiện hay không. Bạn phải tự kiểm tra điều kiện hợp lệ trước khi sử dụng ưu đãi. Hiện tại, đối tượng sản phẩm chỉ bao gồm ưu đãi khi người dùng đủ điều kiện. Điều này có nghĩa là bạn không cần kiểm tra điều kiện hợp lệ nữa — nếu có ưu đãi, người dùng đã đủ điều kiện. Nếu bạn vẫn muốn xem ưu đãi cho những người dùng không đủ điều kiện, hãy tham khảo `sk1Product` và `sk2Product`. ## Cập nhật cấu hình SDK tích hợp bên thứ ba \{#update-third-party-integration-sdk-configuration\} Bắt đầu từ Adapty iOS SDK 3.3.0, chúng tôi đã cập nhật API công khai cho phương thức `updateAttribution`. Trước đây, nó nhận dictionary `[AnyHashable: Any]`, cho phép bạn truyền trực tiếp các đối tượng attribution từ nhiều dịch vụ khác nhau. Hiện tại, nó yêu cầu `[String: any Sendable]`, vì vậy bạn cần chuyển đổi các đối tượng attribution trước khi truyền vào. Để đảm bảo các tích hợp hoạt động đúng với Adapty iOS SDK 3.3.0 trở lên, hãy cập nhật cấu hình SDK cho các tích hợp sau theo hướng dẫn trong các phần bên dưới. ### Adjust Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Adjust](adjust#connect-your-app-to-adjust). <Tabs groupId="current-os" queryString> <TabItem value="v5" label="Adjust 5.x+" default> ```diff showLineNumbers class AdjustModuleImplementation { - func updateAdjustAttribution() { - Adjust.attribution { attribution in - guard let attributionDictionary = attribution?.dictionary()?.toSendableDict() else { return } - - Adjust.adid { adid in - guard let adid else { return } - - Adapty.updateAttribution(attributionDictionary, source: .adjust, networkUserId: adid) { error in - // handle the error - } - } - } - } + func updateAdjustAdid() { + Adjust.adid { adid in + guard let adid else { return } + + Adapty.setIntegrationIdentifier(key: "adjust_device_id", value: adid) + } + } + + func updateAdjustAttribution() { + Adjust.attribution { attribution in + guard let attribution = attribution?.dictionary() else { + return + } + + Adapty.updateAttribution(attribution, source: "adjust") + } + } } ``` </TabItem> <TabItem value="v4" label="Adjust 4.x" default> ```diff showLineNumbers class YourAdjustDelegateImplementation { // Find your implementation of AdjustDelegate // and update adjustAttributionChanged method: func adjustAttributionChanged(_ attribution: ADJAttribution?) { - if let attribution = attribution?.dictionary()?.toSendableDict() { - Adapty.updateAttribution(attribution, source: .adjust) + if let attribution = attribution?.dictionary() { + Adapty.updateAttribution(attribution, source: "adjust") } } } ``` </TabItem> </Tabs> ### AirBridge Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp AirBridge](airbridge#connect-your-app-to-airbridge). ```diff showLineNumbers import AirBridge - let builder = AdaptyProfileParameters.Builder() - .with(airbridgeDeviceId: AirBridge.deviceUUID()) - - Adapty.updateProfile(params: builder.build()) + do { + try await Adapty.setIntegrationIdentifier( + key: "airbridge_device_id", + value: AirBridge.deviceUUID() + ) + } catch { + // handle the error + } ``` ### Amplitude Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Amplitude](amplitude#sdk-configuration). ```diff showLineNumbers import Amplitude - let builder = AdaptyProfileParameters.Builder() - .with(amplitudeUserId: Amplitude.instance().userId) - .with(amplitudeDeviceId: Amplitude.instance().deviceId) - - Adapty.updateProfile(params: builder.build()) + do { + try await Adapty.setIntegrationIdentifier( + key: "amplitude_user_id", + value: Amplitude.instance().userId + ) + try await Adapty.setIntegrationIdentifier( + key: "amplitude_device_id", + value: Amplitude.instance().deviceId + ) + } catch { + // handle the error + } ``` ### AppMetrica Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp AppMetrica](appmetrica#sdk-configuration). ```diff showLineNumbers import AppMetricaCore - if let deviceID = AppMetrica.deviceID { - let builder = AdaptyProfileParameters.Builder() - .with(appmetricaDeviceId: deviceID) - .with(appmetricaProfileId: "YOUR_ADAPTY_CUSTOMER_USER_ID") - - Adapty.updateProfile(params: builder.build()) - } + if let deviceID = AppMetrica.deviceID { + do { + try await Adapty.setIntegrationIdentifier( + key: "appmetrica_device_id", + value: deviceID + ) + try await Adapty.setIntegrationIdentifier( + key: "appmetrica_profile_id", + value: "YOUR_ADAPTY_CUSTOMER_USER_ID" + ) + } catch { + // handle the error + } + } ``` ### AppsFlyer Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp AppsFlyer](appsflyer#connect-your-app-to-appsflyer). ```diff showLineNumbers class YourAppsFlyerLibDelegateImplementation { // Find your implementation of AppsFlyerLibDelegate // and update onConversionDataSuccess method: func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) { let uid = AppsFlyerLib.shared().getAppsFlyerUID() - Adapty.updateAttribution( - conversionInfo.toSendableDict(), - source: .appsflyer, - networkUserId: uid - ) + Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid) + Adapty.updateAttribution(conversionInfo, source: "appsflyer") } } ``` ### Branch Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Branch](branch#connect-your-app-to-branch). ```diff showLineNumbers class YourBranchImplementation { func initializeBranch() { // Pass the attribution you receive from the initializing method of Branch iOS SDK to Adapty. Branch.getInstance().initSession(launchOptions: launchOptions) { (data, error) in - if let data = data?.toSendableDict() { - Adapty.updateAttribution(data, source: .branch) - } + if let data { + Adapty.updateAttribution(data, source: "branch") + } } } } ``` ### Facebook Ads Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Facebook Ads](facebook-ads#connect-your-app-to-facebook-ads). ```diff showLineNumbers import FacebookCore - let builder = AdaptyProfileParameters.Builder() - .with(facebookAnonymousId: AppEvents.shared.anonymousID) - - do { - try Adapty.updateProfile(params: builder.build()) - } catch { - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "facebook_anonymous_id", + value: AppEvents.shared.anonymousID + ) + } catch { + // handle the error + } ``` ### Firebase và Google Analytics Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Firebase và Google Analytics](firebase-and-google-analytics). ```diff showLineNumbers import FirebaseCore import FirebaseAnalytics FirebaseApp.configure() - if let appInstanceId = Analytics.appInstanceID() { - let builder = AdaptyProfileParameters.Builder() - .with(firebaseAppInstanceId: appInstanceId) - Adapty.updateProfile(params: builder.build()) { error in - // handle error - } - } + if let appInstanceId = Analytics.appInstanceID() { + do { + try await Adapty.setIntegrationIdentifier( + key: "firebase_app_instance_id", + value: appInstanceId + ) + } catch { + // handle the error + } + } ``` ### Mixpanel Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Mixpanel](mixpanel#sdk-configuration). ```diff showLineNumbers import Mixpanel - let builder = AdaptyProfileParameters.Builder() - .with(mixpanelUserId: Mixpanel.mainInstance().distinctId) - - do { - try await Adapty.updateProfile(params: builder.build()) - } catch { - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "mixpanel_user_id", + value: Mixpanel.mainInstance().distinctId + ) + } catch { + // handle the error + } ``` ### OneSignal Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp OneSignal](onesignal#sdk-configuration). ```diff showLineNumbers // PlayerID (pre-v5 OneSignal SDK) // in your OSSubscriptionObserver implementation func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) { if let playerId = stateChanges.to.userId { - let params = AdaptyProfileParameters.Builder() - .with(oneSignalPlayerId: playerId) - .build() - - Adapty.updateProfile(params:params) { error in - // check error - } + Task { + try await Adapty.setIntegrationIdentifier( + key: "one_signal_player_id", + value: playerId + ) + } } } // SubscriptionID (v5+ OneSignal SDK) OneSignal.Notifications.requestPermission({ accepted in - let id = OneSignal.User.pushSubscription.id - - let builder = AdaptyProfileParameters.Builder() - .with(oneSignalSubscriptionId: id) - - Adapty.updateProfile(params: builder.build()) + Task { + try await Adapty.setIntegrationIdentifier( + key: "one_signal_subscription_id", + value: OneSignal.User.pushSubscription.id + ) + } }, fallbackToSettings: true) ``` ### Pushwoosh Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Pushwoosh](pushwoosh#sdk-configuration). ```diff showLineNumbers - let params = AdaptyProfileParameters.Builder() - .with(pushwooshHWID: Pushwoosh.sharedInstance().getHWID()) - .build() - - Adapty.updateProfile(params: params) { error in - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "pushwoosh_hwid", + value: Pushwoosh.sharedInstance().getHWID() + ) + } catch { + // handle the error + } ``` ## Cập nhật cách triển khai Observer mode \{#update-observer-mode-implementation\} Cập nhật cách bạn liên kết paywall với giao dịch. Trước đây, bạn dùng phương thức `setVariationId` để gán `variationId`. Hiện tại, bạn có thể đưa `variationId` trực tiếp khi ghi lại giao dịch bằng phương thức `reportTransaction` mới. Xem ví dụ code hoàn chỉnh tại [Liên kết paywall với giao dịch mua trong Observer mode](report-transactions-observer-mode). :::warning Nhớ ghi lại giao dịch bằng phương thức `reportTransaction`. Nếu bỏ qua bước này, Adapty sẽ không nhận ra giao dịch, không cấp mức độ truy cập, không đưa vào analytics, cũng không gửi đến các tích hợp. Đây là bước bắt buộc! ::: ```diff showLineNumbers - let variationId = paywall.variationId - - // There are two overloads: for StoreKit 1 and StoreKit 2 - Adapty.setVariationId(variationId, forPurchasedTransaction: transaction) { error in - if error == nil { - // successful binding - } - } + do { + // every time when calling transaction.finish() + try await Adapty.reportTransaction(transaction, withVariationId: <YOUR_PAYWALL_VARIATION_ID>) + } catch { + // handle the error + } ``` --- # File: migration-to-ios-sdk-v3 --- --- title: "Migrate Adapty iOS SDK to v3.0" description: "Migrate sang Adapty iOS SDK v3.0 để có hiệu suất tốt hơn và các tính năng kiếm tiền mới." --- Adapty SDK v3.0 hỗ trợ [Adapty Paywall Builder](adapty-paywall-builder) mới — công cụ no-code thân thiện với người dùng để tạo paywall. Với tính linh hoạt tối đa và khả năng thiết kế phong phú, các paywall của bạn sẽ trở nên hiệu quả và sinh lời hơn. :::info Lưu ý rằng thư viện AdaptyUI đã bị deprecated và hiện được tích hợp trực tiếp vào AdaptySDK. ::: ## Cài đặt lại Adapty SDK v3.x qua Swift Package Manager \{#reinstall-adapty-sdk-v3x-via-swift-package-manager\} 1. Xóa package dependency AdaptyUI SDK khỏi project của bạn, bạn sẽ không cần nó nữa. 2. Dù bạn đã có sẵn, bạn vẫn cần thêm lại dependency Adapty SDK. Để làm điều này, trong Xcode, mở **File** -> **Add Package Dependency...**. Lưu ý rằng cách thêm package dependency có thể khác nhau tùy phiên bản Xcode. Tham khảo tài liệu Xcode nếu cần. 3. Nhập URL repository `https://github.com/adaptyteam/AdaptySDK-iOS.git` 4. Chọn phiên bản và nhấn nút **Add package**. 5. Chọn các module bạn cần: 1. **Adapty** là module bắt buộc 2. **AdaptyUI** là module tùy chọn, cần thiết nếu bạn dự định sử dụng [Adapty Paywall Builder](adapty-paywall-builder). 6. Xcode sẽ thêm package dependency vào project của bạn và bạn có thể import nó. Trong cửa sổ **Choose Package Products**, nhấn nút **Add package** một lần nữa. Package sẽ xuất hiện trong danh sách **Packages**. ## Cài đặt lại Adapty SDK v3.x qua CocoaPods \{#reinstall-adapty-sdk-v3x-via-cocoapods\} 1. Thêm Adapty vào `Podfile` của bạn. Chọn các module bạn cần: 1. **Adapty** là module bắt buộc. 2. **AdaptyUI** là module tùy chọn, cần thiết nếu bạn dự định sử dụng [Adapty Paywall Builder](adapty-paywall-builder). 2. ```shell showLineNumbers title="Podfile" pod 'Adapty', '~> 3.2.0' pod 'AdaptyUI', '~> 3.2.0' # optional module needed only for Paywall Builder ``` 3. Chạy lệnh: ```sh showLineNumbers title="Shell" pod install ``` Lệnh này tạo ra file `.xcworkspace` cho app của bạn. Sử dụng file này cho toàn bộ quá trình phát triển ứng dụng sau này. Kích hoạt các module Adapty và AdaptyUI SDK. Trước v3.0, bạn không cần kích hoạt AdaptyUI, vì vậy hãy nhớ **thêm phần kích hoạt AdaptyUI**. Các tham số không thay đổi, hãy giữ nguyên chúng. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) .with(customerUserId: "YOUR_USER_ID") .with(idfaCollectionDisabled: false) .with(ipAddressCollectionDisabled: false) Adapty.activate(with: configurationBuilder) { error in // handle the error } // Only if you are going to use AdaptyUI AdaptyUI.activate() ``` </TabItem> <TabItem value="swiftui" label="SwiftUI" default> ```swift title="" showLineNumbers @main struct SampleApp: App { init() let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional Adapty.activate(with: configurationBuilder) { error in // handle the error } // Only if you are going to use AdaptyUI AdaptyUI.activate() } var body: some Scene { WindowGroup { ContentView() } } } ``` </TabItem> </Tabs> --- # End of Documentation _Generated on: 2026-07-24T13:01:43.470Z_ _Successfully processed: 44/44 files_ # KMP - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.473Z Total files: 48 --- # File: kmp-sdk-overview --- --- title: "Tổng quan về Kotlin Multiplatform SDK" description: "Tìm hiểu về Adapty Kotlin Multiplatform SDK và các tính năng chính của nó." --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-KMP.svg?style=flat&logo=kotlin)](https://github.com/adaptyteam/AdaptySDK-KMP/releases) Chào mừng! Chúng tôi ở đây để giúp in-app purchase trở nên dễ dàng hơn bao giờ hết 🚀 Chúng tôi đã xây dựng Adapty Kotlin Multiplatform SDK để giúp bạn không còn phải đau đầu với in-app purchase, để bạn có thể tập trung vào điều bạn làm tốt nhất – xây dựng những ứng dụng tuyệt vời. Đây là những gì chúng tôi xử lý cho bạn: - Xử lý giao dịch mua, xác thực biên lai và quản lý gói đăng ký ngay từ đầu - Tạo và kiểm tra paywall mà không cần cập nhật ứng dụng - Nhận phân tích mua hàng chi tiết với cấu hình bằng không – bao gồm cohort, LTV, churn và phân tích phễu - Luôn cập nhật trạng thái gói đăng ký của người dùng trên các phiên và thiết bị - Tích hợp ứng dụng của bạn với các dịch vụ attribution marketing và analytics chỉ bằng một dòng code :::note Trước khi bắt tay vào code, bạn cần tích hợp Adapty với Google Play Console và thiết lập sản phẩm trong dashboard. Xem [hướng dẫn quickstart](quickstart) của chúng tôi để cấu hình mọi thứ trước. ::: ## Bắt đầu \{#get-started\} 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. Đây là những gì chúng tôi sẽ đề cập trong hướng dẫn tích hợp: 1. [Cài đặt & cấu hình SDK](sdk-installation-kotlin-multiplatform): Thêm SDK làm dependency vào dự án của bạn và kích hoạt nó trong code. 2. [Kích hoạt mua hàng qua flow](kmp-quickstart-paywalls): Thiết lập luồng mua hàng để người dùng có thể mua sản phẩm. Để tự xây dựng UI, xem [Triển khai paywall thủ công](kmp-quickstart-manual). 3. [Kiểm tra trạng thái gói đăng ký](kmp-check-subscription-status): Tự động kiểm tra trạng thái gói đăng ký của người dùng và kiểm soát quyền truy cập của họ vào nội dung trả phí. 4. [Xác định người dùng (tùy chọn)](kmp-quickstart-identify): Liên kết người dùng với hồ sơ người dùng Adapty của họ để đảm bảo dữ liệu được lưu trữ nhất quán trên các thiết bị. ### Xem thực tế \{#see-it-in-action\} Muốn xem tất cả kết hợp lại như thế nào? Chúng tôi đã chuẩn bị sẵn cho bạn: - **Ứng dụng mẫu**: Xem [ví dụ đầy đủ](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example) của chúng tôi để biết cách thiết lập toàn bộ - **Video hướng dẫn**: Theo dõi video triển khai từng bước bên dưới <iframe width="560" height="315" src="https://www.youtube.com/embed/JfwJvwnloNw?si=HskPxRk4WGkF_u9s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> ## Các khái niệm chính \{#main-concepts\} Trước khi bắt tay vào code, hãy làm quen với các khái niệm chính giúp Adapty hoạt động. Điểm hay trong cách tiếp cận của Adapty là chỉ có placement được hardcode trong ứng dụng của bạn. Mọi thứ khác – sản phẩm, thiết kế paywall, giá cả và ưu đãi – đều có thể được quản lý linh hoạt từ Adapty dashboard mà không cần cập nhật ứng dụng: 1. [**Sản phẩm**](product) - Bất kỳ thứ gì có thể mua trong ứng dụng của bạn – gói đăng ký, consumable product, hoặc quyền truy cập trọn đời. 2. **Flow hoặc paywall** - Các sản phẩm được gộp với cấu hình, gắn vào một placement. Có hai dạng: - **[Flow](adapty-flow-builder)** - Giao diện trực quan, no-code được xây dựng trong Flow Builder. Adapty hiển thị UI và xử lý giao dịch mua cho bạn. - **[Paywall](paywalls)** - Không có cấu hình trực quan; bạn tự xây dựng UI trong code của mình và tự gọi `makePurchase`. Xem [Triển khai paywall thủ công](kmp-quickstart-manual). Trong code SDK, cả hai đều được lấy qua cùng một phương thức `getFlow`. 3. [**Placement**](placements) - Một điểm chiến lược trong hành trình người dùng của bạn, nơi bạn muốn hiển thị flow hoặc paywall. Hãy nghĩ về placement như "ở đâu" và "khi nào" trong chiến lược monetization của bạn. Các placement phổ biến bao gồm: - `main` - Vị trí paywall chính của bạn - `onboarding` - Hiển thị trong flow onboarding của người dùng - `settings` - Có thể truy cập từ phần cài đặt ứng dụng của bạn Bắt đầu với những thứ cơ bản như `main` hoặc `onboarding` cho lần tích hợp đầu tiên, sau đó [hãy suy nghĩ xem còn những nơi nào khác trong ứng dụng người dùng có thể sẵn sàng mua hàng](choose-meaningful-placements). 4. [**Hồ sơ người dùng**](profiles-crm) - Khi người dùng mua một sản phẩm, hồ sơ người dùng của họ sẽ được gán **mức độ truy cập** mà bạn sử dụng để xác định quyền truy cập vào các tính năng trả phí. --- # File: sdk-installation-kotlin-multiplatform --- --- title: "Cài đặt & cấu hình Adapty Kotlin Multiplatform SDK" description: "Cài đặt và cấu hình Adapty SDK cho các ứng dụng Kotlin Multiplatform." --- SDK của Adapty bao gồm hai module chính để tích hợp liền mạch vào ứng dụng di động của bạn: - **Core Adapty**: SDK thiết yếu này là bắt buộc để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI** (`io.adapty:adapty-kmp-ui`): Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder) với lớp rendering Compose Multiplatform (`view.present()`). Nếu dự án của bạn không sử dụng Compose Multiplatform, bạn có thể dùng [`createNativePaywallView`](kmp-present-paywalls#without-compose-multiplatform) và [`createNativeOnboardingView`](kmp-present-onboardings#without-compose-multiplatform) từ core module thay thế. :::tip Bạn muốn xem một 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-KMP/tree/main/example) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: Để xem hướng dẫn triển khai đầy đủ, bạn cũng có thể xem video: <iframe width="560" height="315" src="https://www.youtube.com/embed/JfwJvwnloNw?si=HskPxRk4WGkF_u9s" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> ## Yêu cầu \{#requirements\} Adapty Kotlin Multiplatform SDK tương thích với Xcode 16.2 trở lên. :::info Bắt đầu từ SDK v3.17, Adapty SDK sử dụng Google Play Billing Library v8.0.0 theo mặc định. ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK qua Gradle \{#install-adapty-sdk-via-gradle\} Cài đặt Adapty SDK bằng Gradle là bắt buộc cho cả ứng dụng Android và iOS. Chọn phương thức thiết lập dependency của bạn: - Gradle tiêu chuẩn: Thêm dependencies vào **module-level** `build.gradle` - Nếu dự án của bạn sử dụng tệp `.gradle.kts`, hãy thêm dependencies vào **module-level** `build.gradle.kts` - Nếu bạn sử dụng version catalogs, hãy thêm dependencies vào tệp `libs.versions.toml` rồi tham chiếu đến nó trong `build.gradle.kts` :::important Adapty Kotlin Multiplatform SDK 4.0 là phiên bản pre-release. Gradle không tự động chọn các phiên bản pre-release thông qua dải phiên bản động (như `+` hoặc `latest.release`), vì vậy bạn phải chỉ định chính xác phiên bản — ví dụ `io.adapty:adapty-kmp:4.0.0-beta.1`, hoặc `adapty-kmp = "4.0.0-beta.1"` trong `libs.versions.toml`. Xem [Migrate Adapty Kotlin Multiplatform SDK sang v4](migration-to-kmp-sdk-v4). ::: <Tabs> <TabItem value="module-level build.gradle" label="module-level build.gradle" default> ```kotlin showLineNumbers kotlin { sourceSets { commonMain { dependencies { implementation libs.adapty.kmp } } } } ``` </TabItem> <TabItem value="module-level build.gradle.kts" label="module-level build.gradle.kts" default> ```kotlin showLineNumbers kotlin { sourceSets { val commonMain by getting { dependencies { implementation(libs.adapty.kmp) } } } } ``` </TabItem> <TabItem value="version-catalog" label="Thư viện phiên bản" default> ```toml showLineNumbers // libs.versions.toml [versions] .. adapty-kmp = "<the latest SDK version>" [libraries] .. adapty-kmp = { module = "io.adapty:adapty-kmp", version.ref = "adapty-kmp" } // build.gradle.kts kotlin { sourceSets { val commonMain by getting { dependencies { implementation(libs.adapty.kmp) } } } } ``` </TabItem> </Tabs> :::note Nếu bạn gặp lỗi liên quan đến Maven, hãy đảm bảo rằng bạn đã có `mavenCentral()` trong các Gradle script của mình. <details> <summary>Hướng dẫn cách thêm</summary> Nếu dự án của bạn không có `dependencyResolutionManagement` trong `settings.gradle`, hãy thêm đoạn sau vào `build.gradle` cấp cao nhất ở cuối phần repositories: ```groovy showLineNumbers title="top-level build.gradle" allprojects { repositories { ... mavenCentral() } } ``` Nếu không, hãy thêm đoạn sau vào `settings.gradle` trong phần `repositories` của `dependencyResolutionManagement`: ```groovy showLineNumbers title="settings.gradle" dependencyResolutionManagement { ... repositories { ... google() mavenCentral() } } ``` </details> ::: ## Kích hoạt Adapty SDK \{#activate-adapty-sdk\} ### Thiết lập cơ bản \{#basic-setup\} Thêm lệnh khởi tạo càng sớm càng tốt — thường là trong code Kotlin dùng chung cho cả hai nền tảng. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } ``` :::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 Kotlin Multiplatform SDK](kmp-sdk-call-order) để biết toàn bộ trình tự. ::: Để lấy **Public SDK Key** của bạn: 1. Vào 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. :::info - Đảm bảo bạn sử dụng Public SDK key để khởi tạo Adapty — Secret key chỉ dùng cho [server-side API](getting-started-with-server-side-api). - SDK key 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 chắc chắn chọn đúng key. ::: 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), hãy [kích hoạt AdaptyUI module](#activate-adaptyui-module-of-adapty-sdk) bên dưới trước, rồi làm theo [hướng dẫn nhanh về Paywall Builder](kmp-quickstart-paywalls). - Nếu bạn tự xây dựng UI paywall, xem [hướng dẫn nhanh cho paywall tùy chỉnh](kmp-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch kích hoạt module **AdaptyUI** để sử dụng [Adapty Paywall Builder](kmp-present-paywalls), hãy đảm bảo thiết lập `.withActivateUI(true)` trong cấu hình của bạn. :::info quan trọng Trong code của bạn, bạn phải kích hoạt module Adapty core trước khi kích hoạt AdaptyUI. ::: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withActivateUI(true) // true for activating the AdaptyUI module .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } ``` ## Cấu hình Proguard (Android) \{#configure-proguard-android\} Trước khi ra mắt ứng dụng trên production, bạn có thể cần thêm `-keep class com.adapty.** { *; }` vào cấu hình Proguard. ## Cài đặt tùy chọn \{#optional-setup\} ### Ghi nhật ký \{#logging\} #### Thiết lập hệ thống ghi nhật ký \{#set-up-the-logging-system\} Adapty ghi lại các lỗi và thông tin quan trọng khác để giúp bạn hiểu những gì đang xảy ra. Các cấp độ ghi nhật ký có sẵn như sau: | Level | Description | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `AdaptyLogLevel.ERROR` | Chỉ ghi log các lỗi. | | `AdaptyLogLevel.WARN` | Ghi log các lỗi và các 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. Giá trị mặc định. | | `AdaptyLogLevel.VERBOSE` | Ghi log mọi thông tin bổ sung có thể hữu ích khi debug, chẳng hạn như các lời gọi hàm, truy vấn API, v.v. | | `AdaptyLogLevel.DEBUG` | Ghi log thông tin chi tiết nhất, bao gồm cả dữ liệu debug nội bộ. | Bạn có thể đặt mức độ log trong ứng dụng trước khi cấu hình Adapty: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withLogLevel(AdaptyLogLevel.VERBOSE) // recommended for development .build() ``` ### Chính sách dữ liệu \{#data-policies\} #### Tắt tính năng thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi kích hoạt module Adapty, hãy đặt `ipAddressCollectionDisabled` thành `true` để tắt tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư của người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm thiểu việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không được yêu cầu trong ứng dụng của bạn. ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build() ``` #### Tắt tính năng thu thập và chia sẻ advertising ID \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `appleIdfaCollectionDisabled` (iOS) hoặc `googleAdvertisingIdCollectionDisabled` (Android) thành true để tắt tính năng thu thập advertising identifier. Giá trị mặc định là false. Sử dụng tham số này để tuân thủ các chính sách của App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc nếu ứng dụng của bạn không cần attribution quảng cáo hoặc phân tích dựa trên ID quảng cáo. ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withGoogleAdvertisingIdCollectionDisabled(true) // Android only .withAppleIdfaCollectionDisabled(true) // iOS only .build() ``` #### Thiết lập cấu hình bộ nhớ cache media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Theo mặc định, AdaptyUI lưu cache các tệp media (như hình ảnh và video) để cải thiện hiệu suất và giảm lưu lượng mạng. Bạn có thể tùy chỉnh cài đặt cache bằng cách cung cấp cấu hình tùy chỉnh. Dùng `mediaCache` để ghi đè cài đặt cache mặc định: ```kotlin val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withMediaCacheConfiguration( AdaptyConfig.MediaCacheConfiguration( memoryStorageTotalCostLimit = 200 * 1024 * 1024, // 200 MB memoryStorageCountLimit = Int.MAX_VALUE, diskStorageSizeLimit = 200 * 1024 * 1024 // 200 MB ) ) .build() ``` ### Bật local access levels (Android) \{#enable-local-access-levels-android\} Theo mặc định, [local access levels](local-access-levels) bị tắt cho Android. Để bật chúng, đặt `withLocalAccessLevelAllowed` thành `true`: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withGoogleLocalAccessLevelAllowed(true) .build() ``` ### Xóa dữ liệu khi khôi phục từ bản sao lưu \{#clear-data-on-backup-restore\} Khi `withAppleClearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ bản sao lưu iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng đã cache, chi tiết sản phẩm và các paywall. SDK sau đó sẽ khởi tạo lại với trạng thái mới hoàn toàn. 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. ::: ```swift showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withAppleClearDataOnBackup(true) .build() ``` ## Xử lý sự cố \{#troubleshooting\} #### Quy tắc sao lưu Android (cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `<manifest>` có chứa tools: ```xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app"> ... </manifest> ``` #### 2. Ghi đè các thuộc tính backup trong `<application>` \{#2-override-backup-attributes-in-application\} Trong cùng file `AndroidManifest.xml`, cập nhật thẻ `<application>` để ứ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 <application android:name=".App" android:allowBackup="true" android:fullBackupContent="@xml/sample_backup_rules" android:dataExtractionRules="@xml/sample_data_extraction_rules" tools:replace="android:fullBackupContent,android:dataExtractionRules"> ... </application> ``` 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" <?xml version="1.0" encoding="utf-8"?> <data-extraction-rules> <cloud-backup> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </cloud-backup> <device-transfer> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </device-transfer> </data-extraction-rules> ``` **Dành cho Android 11 trở xuống** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" <?xml version="1.0" encoding="utf-8"?> <full-backup-content> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> :::important Trong dự án Kotlin Multiplatform, hãy áp dụng các thay đổi này trong module ứng dụng Android (module tạo ra APK/AAB), ví dụ như `androidApp` hoặc `app`: - Manifest: `androidApp/src/main/AndroidManifest.xml` - Backup rules XML: `androidApp/src/main/res/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 nó 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 chính xác, chỉ sử dụng `standard` hoặc `singleTop` làm launch mode cho Activity khởi động flow mua hàng, và tránh các chế độ 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 <activity android:name=".MainActivity" android:launchMode="standard" /> ``` --- # File: kmp-quickstart-paywalls --- --- title: "Bật tính năng mua hàng với Flow Builder trong Kotlin Multiplatform SDK" description: "Hướng dẫn nhanh để bật in-app purchase với Adapty Flow Builder." --- Hướng dẫn này sử dụng API của Adapty Kotlin Multiplatform SDK v4 (beta). Nếu bạn đang dùng v3, hãy xem [hướng dẫn migration](migration-to-kmp-sdk-v4) để biết tên các phương thức tương ứng. Để kích hoạt in-app purchase, bạn cần nắm rõ ba khái niệm quan trọng: - [**Sản phẩm**](product) – mọi thứ người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flow**](adapty-flow-builder) – chuỗi màn hình trình bày sản phẩm đến người dùng, được xây dựng trong Flow Builder không cần code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện trong code của mình, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](kmp-quickstart-manual). - [**Placement**](placements) – nơi và thời điểm bạn hiển thị flow trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trong dashboard, sau đó yêu cầu chúng theo 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 flow khác nhau cho các nhóm người dùng khác nhau. Adapty cung cấp cho bạn ba cách để kích hoạt tính năng mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Triển khai | Độ phức tạp | Khi nào nên dùng | |---|---|---| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng để mua hàng trong trình tạo no-code](quickstart-paywalls). Adapty tự động hiển thị nó và xử lý toàn bộ flow 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ự 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 flow từ Adapty để duy trì sự linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](kmp-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ó những hạn chế nhất định trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, hãy xem [Triển khai paywall thủ công](kmp-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code của ứng dụng, bạn chỉ cần: 1. **Lấy flow**: Lấy từ Adapty. 2. **Hiển thị flow và Adapty sẽ xử lý thanh toán cho bạn**: Hiển thị view trong ứng dụng. 3. **Xử lý các hành động nút**: Liên kết các tương tác của người dùng với phản hồi của ứng dụng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. Kết nối ứng dụng của bạn với [App Store](initial_ios) và/hoặc [Google Play](initial-android) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo một flow và thêm sản phẩm vào đó](create-paywall). 4. [Tạo một placement và thêm flow vào đó](create-placement). 5. [Cài đặt và kích hoạt SDK](sdk-installation-kotlin-multiplatform) 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 quickstart](quickstart) hoặc tạo flow và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy flow \{#1-get-the-flow\} Các flow của bạn được liên kết với các placement được cấu hình trên dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy một flow được tạo trong Adapty Flow Builder, bạn cần: 1. Lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow`. 2. Tạo flow view bằng phương thức `createFlowView`. View chứa các thành phần UI và kiểu dáng cần thiết để hiển thị flow. Nếu flow chưa được cấu hình view, `createFlowView` sẽ trả về lỗi — hãy xử lý lỗi đó trong `onError`. :::important Để hiển thị view, bạn phải bật toggle **Show on device** trong Flow Builder. Nếu không, `createFlowView` sẽ trả về lỗi và flow sẽ không được hiển thị. ::: ```kotlin showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID") .onSuccess { flow -> AdaptyUI.createFlowView(flow) .onSuccess { view -> view.present() } .onError { error -> // the flow has no view configured, or view creation failed } } .onError { error -> // handle the error } ``` ## 2. Hiển thị flow \{#2-display-the-flow\} Bây giờ khi bạn đã có flow, chỉ cần thêm vài dòng code để hiển thị nó. Để hiển thị flow trực quan trên màn hình thiết bị, trước tiên bạn phải tạo view. Để làm điều đó, hãy gọi phương thức `AdaptyUI.createFlowView()`: ```kotlin showLineNumbers AdaptyUI.createFlowView(flow) .onSuccess { view -> view.present() } .onError { error -> // handle the error } ``` Sau khi view được tạo thành công, bạn có thể hiển thị nó trên màn hình thiết bị. Mỗi view chỉ được sử dụng một lần: sau khi gọi `dismiss()`, hãy gọi lại `createFlowView` để hiển thị flow thêm một lần nữa. :::tip Để biết thêm chi tiết về cách hiển thị một flow, hãy xem [hướng dẫn](kmp-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút \{#handle-button-actions\} Khi người dùng nhấn các nút trong flow, Kotlin Multiplatform SDK tự động xử lý việc mua hàng, khôi phục, đóng flow và mở liên kết. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được định sẵn và yêu cầu xử lý hành động trong code của bạn. Hoặc, bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ, đây là hành vi mặc định của nút đóng. Bạn không cần thêm nó vào code, nhưng ở đây bạn có thể thấy cách thực hiện nếu cần. Lưu ý rằng theo mặc định, flow vẫn mở sau khi mua hàng thành công. Nếu bạn muốn đóng flow sau khi mua xong, hãy dismiss view trong callback `flowViewDidFinishPurchase`. :::tip Xem các hướng dẫn của chúng tôi về cách xử lý [hành động](kmp-handle-paywall-actions) và [sự kiện](kmp-handling-events) của nút bấm. ::: ```kotlin showLineNumbers AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver { override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CloseAction -> mainUiScope.launch { view.dismiss() } else -> Unit } } override fun flowViewDidFinishPurchase( view: AdaptyUIFlowView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { if (purchaseResult !is AdaptyPurchaseResult.UserCanceled) { mainUiScope.launch { view.dismiss() } } } }) ``` ## Các bước tiếp theo \{#next-steps\} Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra mua hàng của bạn trong [App Store sandbox](test-purchases-in-sandbox) hoặc trong [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. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](kmp-check-subscription-status) để đảm bảo bạn hiển thị paywall hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Dưới đây là cách tích hợp tất cả các bước đó vào ứng dụng của bạn. ```kotlin showLineNumbers // Set up the observer for handling flow events AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver { override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CloseAction -> mainUiScope.launch { view.dismiss() } else -> Unit } } override fun flowViewDidFinishPurchase( view: AdaptyUIFlowView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { if (purchaseResult !is AdaptyPurchaseResult.UserCanceled) { mainUiScope.launch { view.dismiss() } } } }) // Get and display the flow Adapty.getFlow("YOUR_PLACEMENT_ID") .onSuccess { flow -> AdaptyUI.createFlowView(flow) .onSuccess { view -> view.present() } .onError { error -> // the flow has no view configured — use custom logic } } .onError { error -> // handle the error } ``` --- # File: kmp-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Kotlin Multiplatform 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 một 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 những gì họ cần thấy - hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí. ## Lấy trạng thái gói đăng ký \{#get-subscription-status\} Khi quyết định có nên hiển thị paywall hay nội dung trả phí cho người dùng, bạn cần kiểm tra [mức độ truy cập](access-level) trong hồ sơ người dùng của họ. Bạn có hai lựa chọn: - Gọi `getProfile` nếu bạn cần dữ liệu hồ sơ người dùng mới nhất ngay lập tức (ví dụ: khi khởi động ứng dụng) hoặc muốn ép buộc cập nhật. - Thiết lập **cập nhật hồ sơ tự động** để lưu 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` để truy cập hồ sơ người dùng: ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access } .onError { error -> // handle the error } ``` ### Lắng nghe các 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 của bạn: 1. Dùng `Adapty.setOnProfileUpdatedListener()` để 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ể dùng nó trong toàn bộ ứng dụng mà không cần thực hiện thêm các yêu cầu mạng. ```kotlin showLineNumbers class SubscriptionManager { private var currentProfile: AdaptyProfile? = null init { // Listen for profile updates Adapty.setOnProfileUpdatedListener { profile -> currentProfile = profile // Update UI, unlock content, etc. } } // Use stored profile instead of calling getProfile() fun hasAccess(): Boolean { return currentProfile?.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true } } ``` :::note Adapty tự động gọi trình lắng nghe cập nhật hồ sơ khi ứng dụng của bạn khởi động, cung cấp dữ liệu gói đăng ký được lưu trong cache ngay cả khi thiết bị ngoại tuyến. ::: ## Kết nối hồ sơ người dùng với logic paywall \{#connect-profile-with-paywall-logic\} Khi bạn cần đưa ra quyết định ngay lập tức về việc hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách tiếp cận này hữu ích cho các tình huống như khi khởi động ứng dụng, khi vào các phần nội dung premium, hoặc trước khi hiển thị nội dung cụ thể. ```kotlin showLineNumbers private fun checkAccessAndShowPaywall() { // First, check if user has access Adapty.getProfile() .onSuccess { profile -> val hasAccess = profile.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true if (!hasAccess) { // User doesn't have access, show paywall showPaywall() } else { // User has access, show premium content showPremiumContent() } } .onError { error -> // If we can't check access, show paywall as fallback showPaywall() } } private fun showPaywall() { // Get and display paywall using the KMP SDK Adapty.getPaywall("YOUR_PLACEMENT_ID") .onSuccess { paywall -> if (paywall.hasViewConfiguration) { val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() } else { // Handle remote config paywall or show custom UI handleRemoteConfigPaywall(paywall) } } .onError { error -> // Handle paywall loading error showError("Unable to load paywall") } } private fun showPremiumContent() { // Show your premium content here // This is where you unlock paid features } ``` ## Các bước tiếp theo \{#next-steps\} Bây giờ, khi bạn đã biết cách theo dõi trạng thái gói đăng ký, hãy tìm hiểu cách [làm việc với hồ sơ người dùng](kmp-quickstart-identify) để đảm bảo họ có thể truy cập những gì họ đã trả tiền. --- # File: kmp-quickstart-identify --- --- title: "Xác định người dùng trong Kotlin Multiplatform 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 KMP." --- :::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ó phù hợp với hệ thống xác thực hiện có của bạn. ::: Cách bạn quản lý 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, hãy 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, hãy 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** để đối chiếu hồ sơ trong Adapty với hệ thống xác thực nội bộ của bạn. Dưới đây là sự khác biệt giữa người dùng ẩn danh và người dùng đã xác định: | | Người dùng ẩn danh | Người dùng đã xác định | |-------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------| | **Quản lý 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 sau mỗi lần cài đặt lại | Cùng một hồ sơ trên các phiên và thiết bị | | **Lưu trữ dữ liệu** | Dữ liệu của người dùng ẩn danh gắn với lần cài đặt ứng dụng | Dữ liệu của người dùng đã xác định 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 mã ứ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 cứ thứ gì trong ứng dụng, giao dịch mua này được **liên kết với hồ sơ Adapty và tài khoản cửa hàng của họ**. 3. Khi người dùng **cài đặt lại** ứng dụng hoặc cài đặt nó 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 đã thực hiện giao dịch mua trước đây trong ứng dụng của bạn, theo mặc định, các giao dịch mua của họ sẽ được tự động đồng bộ từ App Store khi SDK kích hoạt. 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 phân tích của Adapty, bạn có thể [cấu hình những gì sẽ được tính 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 **ID thiết bị**. Trong trường hợp này, mỗi lần cài đặt ứng dụng trên một thiết bị được tính là một lần cài đặt, bao gồm cả việc cài đặt lại. :::note Khôi phục bản sao lưu hoạt động khác với việc cài đặt lại. Theo mặc định, khi người dùng khôi phục từ bản sao lưu, SDK bảo toàn dữ liệu được lưu trong bộ nhớ cache và không tạo hồ sơ mới. Bạn có thể cấu hình hành vi này bằng cài đặt `withAppleClearDataOnBackup`. [Tìm hiểu thêm](sdk-installation-kotlin-multiplatform#clear-data-on-backup-restore). ::: ## Người dùng đã xác định \{#identified-users\} Bạn có hai lựa 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 chạy, 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 một giao dịch mua từ Customer User ID hiện đang được liên kết với một Customer User ID khác, mức độ truy cập được chia sẻ, vì vậy cả hai hồ sơ đều có quyền truy cập trả phí. Bạn có thể cấu hình cài đặt này để chuyển quyền truy cập trả phí từ hồ sơ này sang hồ sơ khác hoặc tắt hoàn toàn việc chia sẻ. Xem [bài viết](general#6-sharing-paid-access-between-user-accounts) để biết thêm chi tiết. ::: <img src="/assets/shared/img/identify-diagram.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Trong quá trình đăng nhập/đăng ký \{#during-loginsignup\} Nếu bạn đang xác định người dùng sau khi ứng dụng khởi chạy (ví dụ: sau khi họ đăng nhập vào ứng dụng hoặc đăng ký), hãy sử dụng phương thức `identify` để đặt customer user ID của họ. - Nếu bạn **chưa sử dụng customer user ID này trước đây**, Adapty sẽ tự động liên kết nó với hồ sơ hiện tại. - Nếu bạn **đã sử dụng customer user ID này để xác định người dùng trước đây**, Adapty sẽ chuyển sang làm việc với hồ sơ được liên kết với customer user ID này. :::important Customer user ID phải là duy nhất cho mỗi người dùng. Nếu bạn hardcode giá trị tham số, tất cả người dùng sẽ được coi là một người. ::: Hãy chờ `identify` hoàn thành (trong callback `onSuccess` của nó) trước khi gọi các phương thức SDK khác. Các lệnh gọi đồng thời có thể rơi vào hồ sơ ẩn danh. Xem [Thứ tự gọi trong Kotlin Multiplatform SDK](kmp-sdk-call-order). ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` ### Trong quá trình kích hoạt SDK \{#during-the-sdk-activation\} Nếu bạn đã biết customer user ID khi kích hoạt SDK, bạn có thể gửi nó trong phương thức `activate` thay vì gọi `identify` riêng. Nếu bạn biết customer user ID nhưng chỉ đặt nó sau khi kích hoạt, điều đó có nghĩa là khi kích hoạt, Adapty sẽ tạo một hồ sơ ẩn danh mới và chỉ chuyển sang hồ sơ hiện có sau khi bạn gọi `identify`. Bạn có thể truyền customer user ID hiện có (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ẽ được tự động 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 phân tích, vì số lần cài đặt được tính dựa trên ID thiết bị. ID thiết bị đại diện cho một lần cài đặt duy nhất của ứng dụng từ cửa hàng trên một thiết bị và chỉ được tạo lại sau khi ứng dụng được cài đặt lại. Nó không phụ thuộc vào việc đây là lần cài đặt đầu tiên hay lần lặp lại, hoặc liệu customer user ID hiện có có được sử dụng 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 ứng dụng sẽ không tạo 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). ::: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. .build() ``` ### Đăng xuất người dùng \{#log-users-out\} Nếu bạn có nút đăng xuất cho 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. ::: ```kotlin showLineNumbers Adapty.logout() .onSuccess { // successful logout } .onError { error -> // handle the 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 gắn 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, vì vậy 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 đổi hồ sơ. Bạn có thể gọi [`getProfile`](kmp-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe các cập nhật hồ sơ](kmp-check-subscription-status) để dữ liệu tự động đồng bộ. ## Các bước tiếp theo \{#next-steps\} Xin chúc mừng! Bạn đã triển khai logic thanh toán trong ứng dụng! Chúc bạn thành công với việc kiếm tiền từ ứng dụng! Để khai thác nhiều hơn từ Adapty, bạn có thể khám phá các chủ đề sau: - [**Kiểm thử**](troubleshooting-test-purchases): Đảm bảo rằng mọi thứ hoạt động như mong đợi - [**Tích hợp**](configuration): Tích hợp với các dịch vụ attribution marketing và phân tích chỉ bằng một dòng code - [**Đặt thuộc tính hồ sơ tùy chỉnh**](kmp-setting-user-attributes): Thêm các thuộc tính tùy chỉnh vào hồ sơ người dùng và tạo phân khúc, để bạn có thể chạy A/B test hoặc hiển thị các paywall khác nhau cho những người dùng khác nhau --- # File: adapty-sdk-integration-skill-kmp --- --- title: "Tích hợp Adapty vào ứng dụng Kotlin Multiplatform 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 Kotlin Multiplatform của bạn từ đầu đến cuối với công cụ AI coding." --- :::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-kmp) — hướng dẫn này sẽ dẫn dắt công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn form phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt xong, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-kmp --- --- title: "Tích hợp Adapty vào ứng dụng Kotlin Multiplatform 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 Kotlin Multiplatform 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ẽ đưa bạn qua từng bước tích hợp Adapty vào ứng dụng Kotlin Multiplatform với sự hỗ trợ của công cụ AI — bạn chỉ cần cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ dòng code SDK nào. Bạn có thể thực hiện điều này thông qua một LLM skill tương tác, hoặc thủ công qua Dashboard. ### Phương pháp dùng Skill (khuyến nghị) \{#skill-approach-recommended\} Adapty CLI skill cho phép LLM của bạn thiết lập ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng của mình](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm 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ả khi nào cần mở Dashboard để kết nối cửa hàng. ### Phương pháp dùng Dashboard \{#dashboard-approach\} Nếu bạn muốn cấu hình mọi thứ thủ công, đây là những gì bạn cần trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard thay bạn — bạn sẽ cần tự cung cấp chúng. 1. **Kết nối 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 KMP của bạn nhắm đến cả hai nền tảng. Đây là yêu cầu bắt buộc để thanh toán 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 Adapty configuration builder. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu trực tiếp sản phẩm trong code — Adapty cung cấp chúng thông qua paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo paywall và placement**: Trong Adapty Dashboard, tạo paywall trên trang **Paywalls**, sau đó gán nó vào placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty.getPaywall("YOUR_PLACEMENT_ID")`. [Tạo paywall](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình theo 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 truy cập 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 mục trên, bạn đã sẵn sàng viết code. Hãy cho LLM của bạn biết: "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 nhiều lệnh 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 tùy thuộc vào từng 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 cập nhật 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 sẽ phát hiện editor của bạn và cấu hình Context7 server. Để thiết lập thủ công, xem [kho GitHub của Context7](https://github.com/upstash/context7). Sau khi cấu hình, hãy tham chiếu thư viện Adapty trong các prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the Kotlin Multiplatform SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn rất 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ột để đảm bảo mọi thứ hoạt động đúng. ::: ### Dùng tài liệu dạng văn bản thuần \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng văn bản Markdown thuần. Thêm `.md` vào cuối URL, hoặc nhấp vào **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor-kmp.md](https://adapty.io/docs/vi/adapty-cursor-kmp.md). Mỗi giai đoạn trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới có một khối "Gửi cho LLM của bạn" với các link `.md` để dán vào. Để lấy nhiều tài liệu cùng lúc, xem [file index và các tập con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này sẽ đưa bạn qua quá trình tích hợp Adapty theo đúng thứ tự triển khai. Mỗi giai đoạn bao gồm tài liệu cần gửi cho LLM, kết quả mong đợi 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 của bạn phân tích dự án và tạo kế hoạch triển khai. Nếu công cụ AI của bạn có chế độ lập kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy 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 bạn dùng phương pháp nào để xử lý thanh toán — điều này ảnh hưởng đến các hướng dẫn nó cần theo: - [**Adapty Paywall Builder**](adapty-paywall-builder): Bạn tạo paywall trong công cụ no-code của Adapty, và SDK tự động hiển thị chúng. - [**Paywall tự tạo thủ công**](kmp-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ý thanh toán. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên hạ tầng thanh toán 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](kmp-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm dependency Adapty SDK qua Gradle và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — mọi thứ khác đều không hoạt động nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-kotlin-multiplatform) 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-kotlin-multiplatform.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Ứng dụng build và chạy được. Logcat (Android) hoặc Xcode console (iOS) 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 từ App settings chưa. ::: ### Hiển thị paywall và xử lý thanh toán \{#show-paywalls-and-handle-purchases\} Lấy paywall theo placement ID, hiển thị nó và xử lý các sự kiện thanh toán. Các hướng dẫn bạn cần phụ thuộc vào cách bạn xử lý thanh toán. Hãy kiểm tra từng giao dịch trong sandbox khi bạn tiến hành — đừng đợi đến cuối. Xem [Kiểm tra thanh toán trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. <Tabs groupId="paywall-approach"> <TabItem value="builder" label="Paywall Builder" default> **Hướng dẫn:** - [Kích hoạt thanh toán bằng paywall (quickstart)](kmp-quickstart-paywalls) - [Lấy paywall từ Paywall Builder và cấu hình của chúng](kmp-get-pb-paywalls) - [Hiển thị paywall](kmp-present-paywalls) - [Xử lý sự kiện paywall](kmp-handling-events) - [Phản hồi các hành động nút](kmp-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/kmp-quickstart-paywalls.md - https://adapty.io/docs/vi/kmp-get-pb-paywalls.md - https://adapty.io/docs/vi/kmp-present-paywalls.md - https://adapty.io/docs/vi/kmp-handling-events.md - https://adapty.io/docs/vi/kmp-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 một sản phẩm sẽ kích hoạt hộp thoại thanh toán sandbox. - **Lưu ý:** Paywall trống hoặc lỗi `getPaywall` → kiểm tra placement ID khớp chính xác với dashboard và placement đã được gán đối tượng. ::: </TabItem> <TabItem value="manual" label="Manual paywalls"> **Hướng dẫn:** - [Kích hoạt thanh toán trong paywall tùy chỉnh (quickstart)](kmp-quickstart-manual) - [Lấy paywall và sản phẩm](fetch-paywalls-and-products-kmp) - [Hiển thị paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-kmp) - [Thực hiện thanh toán](kmp-making-purchases) - [Khôi phục thanh toán](kmp-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/kmp-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products-kmp.md - https://adapty.io/docs/vi/present-remote-config-paywalls-kmp.md - https://adapty.io/docs/vi/kmp-making-purchases.md - https://adapty.io/docs/vi/kmp-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 một sản phẩm sẽ kích hoạt hộp thoại thanh toán sandbox. - **Lưu ý:** Mảng sản phẩm trống → kiểm tra paywall đã được gán sản phẩm trong dashboard và placement đã có đối tượng. ::: </TabItem> <TabItem value="observer" label="Observer mode"> **Hướng dẫn:** - [Tổng quan về Observer mode](observer-vs-full-mode) - [Triển khai Observer mode](implement-observer-mode-kmp) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode-kmp) 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-kmp.md - https://adapty.io/docs/vi/report-transactions-observer-mode-kmp.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau một giao dịch sandbox sử dụng flow thanh toán 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 nào → kiểm tra xem bạn đã báo cáo giao dịch cho Adapty chưa và thông báo server đã được cấu hình cho cả hai cửa hàng chưa. ::: </TabItem> </Tabs> ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua hàng, kiểm tra hồ sơ người dùng để xem có mức độ truy cập nào đang hoạt động không nhằm giới hạn nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](kmp-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/kmp-check-subscription-status.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau một giao dịch sandbox, `profile.accessLevels["premium"]?.isActive` trả về `true`. - **Lưu ý:** `accessLevels` trống sau khi mua → kiểm tra 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 với hồ sơ Adapty để các 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](kmp-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/kmp-quickstart-identify.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi gọi `Adapty.identify("your-user-id")`, phần **Profiles** trên dashboard hiển thị ID người dùng tùy chỉnh của bạn. - **Lưu ý:** Gọi `identify` sau khi kích hoạt nhưng trước khi lấy paywall để tránh lỗi 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 xem qua checklist phát hành để đảm bảo mọi thứ sẵn sàng cho môi trường production. **Hướng dẫn:** [Checklist 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 checklist đã xác nhận: kết nối cửa hàng, thông báo server, flow thanh toán, kiểm tra mức độ truy cập và yêu cầu về 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 văn bản thuần \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM bối cảnh rộng hơn ngoài các trang riêng lẻ, chúng tôi có các file index liệt kê hoặc kết hợp toàn bộ tài liệu Adapty: - [`llms.txt`](https://adapty.io/docs/vi/llms.txt): Liệt kê tất cả các trang với link `.md`. Đây là [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho các website có thể truy cập bở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ộ tài liệu Adapty được kết hợp thành một file duy nhất. Rất lớn — chỉ dùng khi bạn cần toàn bộ nội dung. - Kotlin Multiplatform-specific [`kmp-llms.txt`](https://adapty.io/docs/vi/kmp-llms.txt) và [`kmp-llms-full.txt`](https://adapty.io/docs/vi/kmp-llms-full.txt): Các tập con theo nền tảng giúp tiết kiệm token so với toàn bộ tài liệu. --- # File: kmp-paywalls --- --- title: "Flow và paywall - Kotlin Multiplatform" description: "Hiển thị và quản lý các flow và paywall được xây dựng bằng Adapty Flow Builder hoặc Paywall Builder trong ứng dụng Kotlin Multiplatform của bạn." --- ## Hiển thị paywall \{#display-paywalls\} ### Adapty Flow Builder & Paywall Builder <CustomDocCardList ids={['kmp-get-pb-paywalls', 'kmp-present-paywalls', 'kmp-handling-events', 'kmp-handle-paywall-actions']} /> :::tip Để bắt đầu nhanh với các paywall trong Adapty Paywall Builder, hãy xem [hướng dẫn quickstart](kmp-quickstart-paywalls) của chúng tôi. ::: ### Tự triển khai paywall \{#implement-paywalls-manually\} <CustomDocCardList ids={['kmp-quickstart-manual', 'fetch-paywalls-and-products-kmp', 'present-remote-config-paywalls-kmp', 'kmp-making-purchases']} /> Để xem thêm các hướng dẫn về việc tự triển khai paywall và xử lý giao dịch mua thủ công, hãy xem [danh mục](kmp-implement-paywalls-manually). ## Tính năng hữu ích \{#useful-features\} <CustomDocCardList ids={['kmp-use-fallback-paywalls', 'kmp-web-paywalls']} /> --- # File: kmp-get-pb-paywalls --- --- title: "Lấy flows & paywalls - Kotlin Multiplatform" description: "Tải flows và paywalls từ Adapty trong ứng dụng Kotlin Multiplatform của bạn." --- <SDKv4> <MethodPromo method="getFlow" /> Sau khi [bạn đã thiết kế flow hoặc paywall trong Paywall Builder](adapty-paywall-builder), bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên là lấy flow hoặc paywall được liên kết với placement cùng cấu hình hiển thị của nó như mô tả bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, 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. ::: <details> <summary>Trước khi bắt đầu hiển thị flow trong ứng dụng di động (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow/paywall và tích hợp sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và tích hợp flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-kotlin-multiplatform) vào ứng dụng di động của bạn. </details> ## Lấy flow/paywall \{#fetch-flowpaywall\} Nếu bạn đã thiết kế một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view của nó, rồi mới trình bày nó trong ứng dụng di động. Để đảm bảo hiệu suất tối ưu, bạn nên lấy flow hoặc paywall cùng với [cấu hình view](kmp-get-pb-paywalls#fetch-the-view-configuration) càng sớm càng tốt, để có đủ thời gian tải hình ảnh trước khi hiển thị cho người dùng. Để lấy flow hoặc paywall, sử dụng phương thức `getFlow`: ```kotlin showLineNumbers Adapty.getFlow( placementId = "YOUR_PLACEMENT_ID", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { flow -> // the requested flow/paywall }.onError { error -> // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp vấn đề với kết nối internet không ổn định, hãy cân nhắc sử dụng `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có trải nghiệm tải nhanh hơn, bất kể kết nối internet của họ có bị gián đoạn hay không. Cache được cập nhật thường xuyên, vì vậy có thể sử dụng an toàn trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ các flow và paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải chúng 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 đồng thời đảm bảo độ tin cậy ngay cả trong trường hợp kết nối internet hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p><p>Đối với Kotlin Multiplatform: Bạn có thể tạo `Duration` với các hàm mở rộng như `5.seconds`, trong đó `.seconds` là từ `kotlin.time.Duration.Companion.seconds`.</p> | Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`instanceIdentity`, `variationId`), tên, các biến thể paywall (`paywalls` — danh sách các `AdaptyFlowPaywall`), và Remote Config (`remoteConfigs` — danh sách với một mục cho mỗi ngôn ngữ). Để lấy sản phẩm thực tế phục vụ việc tải trước, tùy chỉnh giao diện, hoặc kiểm tra theo chương trình, hãy gọi `getPaywallProducts(flow)`. | ## Lấy cấu hình view \{#fetch-the-view-configuration\} Sau khi đã lấy được flow hoặc paywall, hãy tải cấu hình view của nó và tạo view trong một bước duy nhất bằng phương thức `createFlowView`. Không có cờ riêng nào để kiểm tra: nếu placement được thiết kế trong **Flow Builder** (một flow) hoặc **Paywall Builder** (một paywall), `createFlowView` sẽ trả về view đã sẵn sàng để hiển thị. Nếu placement là paywall tùy chỉnh không có giao diện Builder, `createFlowView` sẽ trả về `AdaptyResult.Error` — [xử lý nó như một remote config paywall](present-remote-config-paywalls-kmp). :::important Hãy đảm bảo bật toggle **Show on device** trong Flow Builder. Nếu tùy chọn này chưa được bật, cấu hình view sẽ không thể truy xuất được. ::: ```kotlin showLineNumbers AdaptyUI.createFlowView( flow = flow, loadTimeout = 5.seconds, preloadProducts = true ).onSuccess { view -> // use view }.onError { error -> // the flow has no view configured, or view creation failed } ``` | Parameter | Presence | Description | | :--------------------------- | :------------- | :----------------------------------------------------------- | | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow`. | | **loadTimeout** | tùy chọn | Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã lưu trong cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới. Bạn có thể sử dụng các hàm mở rộng như `5.seconds` từ `kotlin.time.Duration.Companion`. | | **preloadProducts** | tùy chọn | Đặt thành `true` để tải trước sản phẩm nhằm cải thiện hiệu năng. Khi được bật, sản phẩm sẽ được tải trước, giảm thời gian cần thiết để hiển thị flow hoặc paywall. | | **productPurchaseParams** | tùy chọn | Một map từ [`AdaptyProductIdentifier`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-product-identifier/) đến [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). Dùng để cấu hình các tham số mua hàng cụ thể như ưu đãi cá nhân hóa hoặc tham số cập nhật gói đăng ký cho từng sản phẩm trong flow hoặc paywall. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [localization cho Builder](add-paywall-locale-in-adapty-paywall-builder). ::: Sau khi tải xong, [hiển thị flow hoặc paywall](kmp-present-paywalls). ## Lấy flow hoặc paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, flow và paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow hoặc paywall có thể mất nhiều thời gian hơn mong muốn. Trong tình huống đó, bạn có thể muốn hiển thị flow hoặc paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này lấy flow hoặc paywall của placement đã chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow hoặc paywall bằng phương thức `getFlow`, như được mô tả chi tiết trong phần [Lấy flow/paywall](#fetch-flowpaywall) ở trên. :::warning Tại sao chúng tôi khuyến nghị sử dụng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế đó để đổi lấy tốc độ tải flow hoặc paywall nhanh hơn, hãy dùng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã mô tả [ở trên](#fetch-flowpaywall). ::: ```kotlin showLineNumbers Adapty.getFlowForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", fetchPolicy = AdaptyPaywallFetchPolicy.Default, ).onSuccess { flow -> // the requested flow }.onError { error -> // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng kết nối không ổn định, hãy cân nhắc dùng `AdaptyPaywallFetchPolicy.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, nhưng tốc độ tải sẽ nhanh hơn bất kể kết nối internet chập chờn đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong flow hoặc paywall của bạn, hãy triển khai custom assets. Hình ảnh hero và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt một custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh xem trước trước khi chạy video. Đây là ví dụ về cách bạn có thể cung cấp các asset tùy chỉnh thông qua một map: :::info SDK Kotlin Multiplatform chỉ hỗ trợ asset cục bộ. Đối với nội dung từ xa, bạn cần tải xuống và lưu vào bộ nhớ đệm cục bộ trước khi sử dụng chúng trong các asset tùy chỉnh. ::: ```kotlin showLineNumbers // Import generated Res class for accessing resources viewModelScope.launch { // Get URIs for bundled resources using Res.getUri() val heroImagePath = Res.getUri("files/images/hero_image.png") val demoVideoPath = Res.getUri("files/videos/demo_video.mp4") // Or read image as byte data val imageByteData = Res.readBytes("files/images/avatar.png") // Create custom assets map val customAssets: Map<String, AdaptyCustomAsset> = mapOf( // Load image from app resources (bundled with the app) // Files should be placed in commonMain/composeResources/files/ "hero_image" to AdaptyCustomAsset.localImageResource( path = heroImagePath ), // Or use image byte data "avatar" to AdaptyCustomAsset.localImageData( data = imageByteData ), // Load video from app resources "demo_video" to AdaptyCustomAsset.localVideoResource( path = demoVideoPath ), // Or use a video file from device storage "intro_video" to AdaptyCustomAsset.localVideoFile( path = "/path/to/local/video.mp4" ), // Apply custom brand colors "brand_primary" to AdaptyCustomAsset.color( colorHex = "#FF6B35" ), // Create gradient background "card_gradient" to AdaptyCustomAsset.linearGradient( colors = listOf("#1E3A8A", "#3B82F6", "#60A5FA"), stops = listOf(0.0f, 0.5f, 1.0f) ) ) // Use custom assets when creating the flow view AdaptyUI.createFlowView( flow = flow, customAssets = customAssets ).onSuccess { view -> // Present the flow with custom assets view.present() }.onError { error -> // Handle the error - the flow will fall back to default appearance } } ``` :::note Nếu một asset không tìm thấy hoặc tải thất bại, flow hoặc paywall sẽ quay về giao diện mặc định được cấu hình trong Builder. ::: </SDKv4> <SDKv3> Sau khi [bạn đã thiết kế phần giao diện cho paywall của mình](adapty-paywall-builder) bằng Paywall Builder mới trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động. Bước đầu tiên trong quá trình này là lấy paywall được liên kết với placement và cấu hình hiển thị của nó như mô tả bên dưới. Vui lòng lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall theo cách thủ công, vui lòng tham khảo chủ đề [Fetch paywalls và sản phẩm cho paywall remote config trong ứng dụng mobile của bạn](fetch-paywalls-and-products-kmp). :::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. ::: <details> <summary>Trước khi bắt đầu hiển thị paywall trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 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-kotlin-multiplatform) vào ứng dụng di động của bạn. </details> ## 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 việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới hiển thị nó trong ứng dụng. Để đảm bảo hiệu suất tối ưu, điều quan trọng là phải truy xuất paywall và [cấu hình view](kmp-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, tạo đủ thời gian để hình ảnh tải xuống trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `getPaywall`: ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này được kỳ vọng là mã ngôn ngữ gồm một hoặc hai subtag phân tách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>Xem [Các 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.</p> | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>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 sử dụng `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` để trả về dữ liệu đã cache nếu tồn tại. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ kém đến đâu. Cache được cập nhật thường xuyên, vì vậy có thể sử dụng an toàn trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>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 thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p><p>Đối với Kotlin Multiplatform: Bạn có thể tạo `TimeInterval` bằng các extension function (như `5.seconds`, trong đó `.seconds` là từ `import com.adapty.utils.seconds`), hoặc `TimeInterval.seconds(5)`. Để không giới hạn, sử dụng `TimeInterval.INFINITE`.</p> | Tham số phản hồi: | Tham số | Mô tả | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | Một đối tượng [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) chứa danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình 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 toggle **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` không — đây là dấu hiệu cho thấy paywall được tạo bằng Paywall Builder. Thông tin này sẽ giúp bạn xác định cách hiển thị paywall. Nếu có `ViewConfiguration`, hãy xử lý nó như một paywall của Paywall Builder; nếu không, [xử lý nó như một paywall Remote Config](present-remote-config-paywalls-kmp). Sử dụng phương thức `createPaywallView` để tải cấu hình view. ```kotlin showLineNumbers if (paywall.hasViewConfiguration) { AdaptyUI.createPaywallView( paywall = paywall, loadTimeout = 5.seconds, preloadProducts = true ).onSuccess { paywallView -> // use paywallView }.onError { error -> // handle the error } } else { // use your custom logic } ``` | Parameter | Presence | Description | | :--------------------------- | :------------- | :----------------------------------------------------------- | | **paywall** | required | Một đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **loadTimeout** | optional | Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã lưu trong cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều request khác nhau bên dưới. Bạn có thể sử dụng các hàm mở rộng như `5.seconds` từ `kotlin.time.Duration.Companion`. | | **preloadProducts** | optional | Đặt thành `true` để tải trước sản phẩm nhằm cải thiện hiệu năng. Khi được bật, các sản phẩm sẽ được tải trước, giảm thời gian cần thiết để hiển thị paywall. | | **productPurchaseParams** | optional | Một map từ [`AdaptyProductIdentifier`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-product-identifier/) đến [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). Dùng để cấu hình các tham số mua hàng cụ thể như ưu đãi cá nhân hóa hoặc các tham số cập nhật gói đăng ký cho từng sản phẩm trong paywall. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder). ::: Sau khi tải xong, [hiển thị paywall](kmp-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ 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-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng dùng phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa nhắm mục tiêu (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy dùng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` đã mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản dịch paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều subtag được phân tách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha của Brazil.</p><p></p><p>Xem [Localizations và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong bộ nhớ đệm nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng kết nối internet không ổn định, hãy cân nhắc sử dụng `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` để trả về dữ liệu đã lưu trong bộ nhớ đệm nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể chất lượng kết nối internet của họ ra sao. Bộ nhớ đệm được cập nhật thường xuyên, nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng bộ nhớ đệm vẫn giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets. Hero image và video có ID được xác định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm tới các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác nhau cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh xem trước trước khi phát video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty SDK lên phiên bản 3.7.0 trở lên. ::: Dưới đây là ví dụ về cách bạn có thể cung cấp custom asset thông qua một map: :::info Kotlin Multiplatform SDK chỉ hỗ trợ asset cục bộ. Đối với nội dung từ xa, bạn cần tải xuống và lưu cache asset cục bộ trước khi sử dụng chúng trong custom asset. ::: ```kotlin showLineNumbers // Import generated Res class for accessing resources viewModelScope.launch { // Get URIs for bundled resources using Res.getUri() val heroImagePath = Res.getUri("files/images/hero_image.png") val demoVideoPath = Res.getUri("files/videos/demo_video.mp4") // Or read image as byte data val imageByteData = Res.readBytes("files/images/avatar.png") // Create custom assets map val customAssets: Map<String, AdaptyCustomAsset> = mapOf( // Load image from app resources (bundled with the app) // Files should be placed in commonMain/composeResources/files/ "hero_image" to AdaptyCustomAsset.localImageResource( path = heroImagePath ), // Or use image byte data "avatar" to AdaptyCustomAsset.localImageData( data = imageByteData ), // Load video from app resources "demo_video" to AdaptyCustomAsset.localVideoResource( path = demoVideoPath ), // Or use a video file from device storage "intro_video" to AdaptyCustomAsset.localVideoFile( path = "/path/to/local/video.mp4" ), // Apply custom brand colors "brand_primary" to AdaptyCustomAsset.color( colorHex = "#FF6B35" ), // Create gradient background "card_gradient" to AdaptyCustomAsset.linearGradient( colors = listOf("#1E3A8A", "#3B82F6", "#60A5FA"), stops = listOf(0.0f, 0.5f, 1.0f) ) ) // Use custom assets when creating paywall view AdaptyUI.createPaywallView( paywall = paywall, customAssets = customAssets ).onSuccess { paywallView -> // Present the paywall with custom assets paywallView.present() }.onError { error -> // Handle the error - paywall will fall back to default appearance } } ``` :::note Nếu một tài nguyên không được tìm thấy hoặc tải thất bại, paywall sẽ trở về giao diện mặc định được cấu hình trong Paywall Builder. ::: </SDKv3> --- # File: kmp-present-paywalls --- --- title: "Hiển thị flows & paywalls - Kotlin Multiplatform" description: "Hiển thị flows và paywalls cho người dùng trong ứng dụng Kotlin Multiplatform của bạn." --- <SDKv4> <MethodPromo method="getFlow" label="Hiển thị flows và paywalls" /> Nếu bạn đã tạo một flow hoặc paywall, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này đề cập đến các flow và **paywall Paywall Builder mới** được render bởi Adapty. Quy trình sẽ khác đối với paywall Remote Config và [Observer mode](observer-vs-full-mode). - Để trình bày **Remote Config paywalls**, xem [Render paywall designed by remote config](present-remote-config-paywalls-kmp). - Để trình bày flows trong **Observer mode**, xem [Present flows in Observer mode](kmp-present-flows-in-observer-mode). ::: Để lấy đối tượng `flow` sử dụng bên dưới, xem [Get flows & paywalls](kmp-get-pb-paywalls). Adapty Kotlin Multiplatform SDK cung cấp hai cách để trình bày flows và paywalls: - **With Compose Multiplatform** - **Without Compose Multiplatform** ## Với Compose Multiplatform \{#with-compose-multiplatform\} Để hiển thị một flow hoặc paywall, hãy dùng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createFlowView`](kmp-get-pb-paywalls#fetch-the-view-configuration). Mỗi `view` chỉ có thể dùng một lần. Nếu cần hiển thị lại flow, hãy gọi `createFlowView` thêm một lần nữa để tạo một `view` mới. :::warning Việc tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi. ::: ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { AdaptyUI.createFlowView(flow = flow).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ### Hiển thị dialog \{#show-dialog\} Dùng phương thức này thay cho các dialog cảnh báo gốc khi một flow hoặc paywall đang được hiển thị trên Android. Trên Android, các alert thông thường sẽ xuất hiện phía sau view của flow, khiến người dùng không nhìn thấy được. Phương thức này đảm bảo dialog được hiển thị đúng cách phía trên flow trên tất cả các nền tảng. ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { view.showDialog( title = "Close this screen?", content = "You will lose access to exclusive offers.", primaryActionTitle = "Stay", secondaryActionTitle = "Close" ).onSuccess { action -> if (action == AdaptyUIDialogActionType.SECONDARY) { // User confirmed - close the flow view.dismiss() } // If primary - do nothing, user stays }.onError { error -> // 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 hiển thị flow hoặc paywall trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này nhận giá trị `AdaptyUIIOSPresentationStyle.FULLSCREEN` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.PAGESHEET`. ```kotlin showLineNumbers viewModelScope.launch { val view = AdaptyUI.createFlowView(flow = flow).getOrNull() view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET) } ``` ## Không dùng Compose Multiplatform \{#without-compose-multiplatform\} :::note `createNativeFlowView` là một phần của module `io.adapty:adapty-kmp` cốt lõi. Nếu dự án của bạn không sử dụng Compose Multiplatform, bạn không cần dependency `io.adapty:adapty-kmp-ui`. ::: Để nhúng một flow hoặc paywall mà không dùng Compose Multiplatform, hãy gọi `createNativeFlowView`. Hàm này trả về một `AdaptyNativeFlowView` mà bạn thêm vào layout của mình: <Tabs> <TabItem value="android" label="Android"> ```kotlin showLineNumbers title="Kotlin Multiplatform (Android)" val nativeView = AdaptyUI.createNativeFlowView( context = context, viewModelStoreOwner = activity, flow = flow, observer = myFlowObserver, ) // Embed in your Compose layout: AndroidView( factory = { nativeView.view }, modifier = Modifier.fillMaxSize() ) ``` Theo mặc định, embedded view không áp dụng safe-area paddings — layout của bạn cần tự xử lý insets. Nếu muốn view tự áp dụng chúng, hãy truyền `androidEnableSafeArea = true` vào `createNativeFlowView`. Tham số này chỉ dành cho Android. </TabItem> <TabItem value="ios" label="iOS"> Vì các phương thức mặc định của interface KMP trở thành `@required` trong Swift, bạn không thể triển khai `AdaptyUIFlowsEventsObserver` trực tiếp từ Swift. Hãy khai báo một base class open trong `iosMain` trước: ```kotlin showLineNumbers title="iosMain (Kotlin)" open class BaseFlowObserver : AdaptyUIFlowsEventsObserver ``` Sau đó, kế thừa nó trong Swift, chỉ override những gì bạn cần: ```swift showLineNumbers title="Swift" class MyFlowObserver: BaseFlowObserver { override func flowViewDidPerformAction(view: AdaptyUIFlowView, action: any AdaptyUIAction) { if action is AdaptyUIActionCloseAction { // remove nativeView from your view hierarchy } } } let nativeView = AdaptyUI.shared.createNativeFlowView( flow: flow, observer: MyFlowObserver() ) // nativeView.viewController is a UIViewController. // Add it to your SwiftUI view or UIKit hierarchy. ``` </TabItem> </Tabs> ### Giải phóng view \{#dispose-the-view\} Gọi `dispose()` khi xóa view khỏi layout. Thao tác này sẽ hủy đăng ký trình lắng nghe sự kiện và giải phóng tài nguyên nội bộ. ```kotlin showLineNumbers title="Kotlin Multiplatform" nativeView.dispose() ``` ## Thẻ tùy chỉnh \{#custom-tags\} Thẻ tùy chỉnh giúp bạn không cần tạo nhiều flow hay paywall riêng biệt cho từng tình huống khác nhau. Hãy tưởng tượng một flow duy nhất có thể thay đổi linh hoạt dựa trên dữ liệu người dùng. Ví dụ, thay vì một câu chào chung chung "Xin chào!", bạn có thể chào đích danh từng người với "Xin chào, John!" hoặc "Xin chào, Ann!" Dưới đây là một số cách sử dụng thẻ tùy chỉnh: - Hiển thị tên hoặc email của người dùng trên flow hoặc paywall. - Hiển thị ngày trong tuần để thúc đẩy doanh số (ví dụ: "Chào Thứ Năm vui vẻ"). - Thêm thông tin cá nhân hóa về các sản phẩm bạn đang bán (chẳng hạn như tên của một chương trình thể dục hoặc số điện thoại trong ứng dụng VoIP). Các thẻ tùy chỉnh giúp bạn tạo ra một flow linh hoạt, thích ứng với nhiều tình huống khác nhau, giúp giao diện ứng dụng trở nên cá nhân hóa và hấp dẫn hơn. :::warning Trong một số trường hợp, ứng dụng của bạn có thể không biết phải thay thế thẻ tùy chỉnh bằng nội dung gì—đặc biệt khi người dùng đang dùng phiên bản cũ hơn của AdaptyUI SDK. Để tránh điều này, hãy luôn thêm văn bản dự phòng để thay thế các dòng chứa thẻ không xác định. Nếu không, người dùng có thể thấy các thẻ được hiển thị dưới dạng code (`<USERNAME/>`). ::: Để sử dụng thẻ tùy chỉnh trong flow hoặc paywall của bạn, hãy truyền chúng khi tạo flow view: <Tabs> <TabItem value="standalone" label="With Compose Multiplatform" default> ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { val customTags = mapOf( "USERNAME" to "John", "DAY_OF_WEEK" to "Thursday" ) AdaptyUI.createFlowView( flow = flow, customTags = customTags ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` </TabItem> <TabItem value="native" label="Without Compose Multiplatform"> ```kotlin showLineNumbers title="Kotlin Multiplatform (Android)" val customTags = mapOf( "USERNAME" to "John", "DAY_OF_WEEK" to "Thursday" ) val nativeView = AdaptyUI.createNativeFlowView( context = context, viewModelStoreOwner = activity, flow = flow, observer = myFlowObserver, customTags = customTags, ) ``` ```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)" val customTags = mapOf( "USERNAME" to "John", "DAY_OF_WEEK" to "Thursday" ) val nativeView = AdaptyUI.createNativeFlowView( flow = flow, observer = myFlowObserver, customTags = customTags, ) ``` </TabItem> </Tabs> ## Bộ đếm thời gian tùy chỉnh \{#custom-timers\} Bộ đếm thời gian là công cụ tuyệt vời để quảng bá các ưu đãi đặc biệt và theo mùa có giới hạn thời gian. Tuy nhiên, cần lưu ý rằng bộ đếm này không kết nối với thời hạn hiệu lực của ưu đãi hay thời gian chạy của chiến dịch. Đây đơn giản chỉ là một đồng hồ đếm ngược độc lập, bắt đầu từ giá trị bạn đặt và giảm dần về không. Khi bộ đếm về không, không có gì xảy ra—nó chỉ dừng lại ở không. Bạn có thể tùy chỉnh văn bản trước và sau bộ đếm để tạo thông điệp mong muốn, ví dụ: "Ưu đãi kết thúc sau: 10:00 giây." Để sử dụng bộ đếm thời gian tùy chỉnh trong flow hoặc paywall của bạn, hãy truyền chúng khi tạo flow view: <Tabs> <TabItem value="standalone" label="With Compose Multiplatform" default> ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { val customTimers = mapOf( "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0), "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59) ) AdaptyUI.createFlowView( flow = flow, customTimers = customTimers ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` </TabItem> <TabItem value="native" label="Without Compose Multiplatform"> ```kotlin showLineNumbers title="Kotlin Multiplatform (Android)" val customTimers = mapOf( "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0), "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59) ) val nativeView = AdaptyUI.createNativeFlowView( context = context, viewModelStoreOwner = activity, flow = flow, observer = myFlowObserver, customTimers = customTimers, ) ``` ```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)" val customTimers = mapOf( "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0), "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59) ) val nativeView = AdaptyUI.createNativeFlowView( flow = flow, observer = myFlowObserver, customTimers = customTimers, ) ``` </TabItem> </Tabs> </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Paywall như vậy chứa cả nội dung cần hiển thị lẫn cách hiển thị nó. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới**. Quy trình hiển thị paywall khác nhau đối với paywall được thiết kế bằng remote config và [Observer mode](observer-vs-full-mode). Để hiển thị **paywall Remote config**, xem [Render paywall được thiết kế bằng remote config](present-remote-config-paywalls-kmp). ::: SDK Kotlin Multiplatform của Adapty cung cấp hai cách để hiển thị paywall: - **Với Compose Multiplatform** - **Không có Compose Multiplatform** ## Với Compose Multiplatform \{#with-compose-multiplatform\} Để hiển thị một paywall, sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createPaywallView`](kmp-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo một `view` instance mới. :::warning Việc tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi. ::: ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { AdaptyUI.createPaywallView(paywall = paywall).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ### Hiển thị hộp thoại \{#show-dialog\} Dùng phương thức này thay cho các hộp thoại alert gốc khi paywall đ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, khiến người dùng không nhìn thấy được. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng vị trí, phía trên paywall, trên mọi nền tảng. ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { view.showDialog( title = "Close paywall?", content = "You will lose access to exclusive offers.", primaryActionTitle = "Stay", secondaryActionTitle = "Close" ).onSuccess { action -> if (action == AdaptyUIDialogActionType.SECONDARY) { // User confirmed - close the paywall view.dismiss() } // If primary - do nothing, user stays }.onError { error -> // 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 paywall hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `AdaptyUIIOSPresentationStyle.FULLSCREEN` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.PAGESHEET`. ```kotlin showLineNumbers viewModelScope.launch { val view = AdaptyUI.createPaywallView(paywall = paywall).getOrNull() view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET) } ``` ## Không dùng Compose Multiplatform \{#without-compose-multiplatform\} :::note `createNativePaywallView` là một phần của module core `io.adapty:adapty-kmp`. Nếu dự án của bạn không dùng Compose Multiplatform, bạn không cần dependency `io.adapty:adapty-kmp-ui`. ::: Để nhúng paywall mà không dùng Compose Multiplatform, hãy gọi `createNativePaywallView`. Hàm này trả về một `AdaptyNativePaywallView` mà bạn thêm vào layout của mình: <Tabs> <TabItem value="android" label="Android"> ```kotlin showLineNumbers title="Kotlin Multiplatform (Android)" val nativeView = AdaptyUI.createNativePaywallView( context = context, viewModelStoreOwner = activity, paywall = paywall, observer = myPaywallObserver, ) // Embed in your Compose layout: AndroidView( factory = { nativeView.view }, modifier = Modifier.fillMaxSize() ) ``` </TabItem> <TabItem value="ios" label="iOS"> Vì các phương thức mặc định của interface KMP trở thành `@required` trong Swift, bạn không thể implement `AdaptyUIPaywallsEventsObserver` trực tiếp từ Swift. Hãy khai báo một open base class trong `iosMain` trước: ```kotlin showLineNumbers title="iosMain (Kotlin)" open class BasePaywallObserver : AdaptyUIPaywallsEventsObserver ``` Sau đó kế thừa nó trong Swift, chỉ override những gì bạn cần: ```swift showLineNumbers title="Swift" class MyPaywallObserver: BasePaywallObserver { override func paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: any AdaptyUIAction) { if action is AdaptyUIActionCloseAction { // remove nativeView from your view hierarchy } } } let nativeView = AdaptyUI.shared.createNativePaywallView( paywall: paywall, observer: MyPaywallObserver() ) // nativeView.viewController is a UIViewController. // Add it to your SwiftUI view or UIKit hierarchy. ``` </TabItem> </Tabs> ### Hủy view \{#dispose-the-view\} Gọi `dispose()` khi xóa view khỏi layout. Thao tác này hủy đăng ký event listener và giải phóng tài nguyên nội bộ. ```kotlin showLineNumbers title="Kotlin Multiplatform" nativeView.dispose() ``` ## Thẻ tùy chỉnh \{#custom-tags\} Thẻ tùy chỉnh giúp bạn tránh phải tạo nhiều paywall riêng lẻ cho từng tình huống khác nhau. Hãy tưởng tượng một paywall duy nhất có thể tự động thích nghi dựa trên dữ liệu người dùng. Ví dụ, thay vì hiển thị "Hello!" chung chung, bạn có thể chào người dùng cá nhân hóa như "Hello, John!" hay "Hello, Ann!" Dưới đây là một số cách bạn có thể sử dụng thẻ tùy chỉnh: - Hiển thị tên hoặc email của người dùng trên paywall. - Hiển thị ngày trong tuần để thúc đẩy doanh số (ví dụ: "Happy Thursday"). - Thêm thông tin cá nhân hóa về sản phẩm bạn đang bán (chẳng hạn tên chương trình tập luyện hoặc số điện thoại trong ứng dụng VoIP). Các custom tag giúp bạn tạo paywall linh hoạt, thích ứng với nhiều tình huống khác nhau, giúp giao diện ứng dụng trở nên cá nhân hóa và hấp dẫn hơn. :::warning Trong một số trường hợp, ứng dụng của bạn có thể không biết cần thay thế custom tag bằng gì—đặc biệt khi người dùng đang dùng phiên bản cũ của AdaptyUI SDK. Để tránh điều này, hãy luôn thêm văn bản dự phòng để thay thế các dòng chứa custom tag không xác định. Nếu không, người dùng có thể thấy các tag hiển thị dưới dạng code (`<USERNAME/>`). ::: Để sử dụng custom tag trong paywall, hãy truyền chúng khi tạo paywall view: <Tabs> <TabItem value="standalone" label="With Compose Multiplatform" default> ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { val customTags = mapOf( "USERNAME" to "John", "DAY_OF_WEEK" to "Thursday" ) AdaptyUI.createPaywallView( paywall = paywall, customTags = customTags ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` </TabItem> <TabItem value="native" label="Without Compose Multiplatform"> ```kotlin showLineNumbers title="Kotlin Multiplatform (Android)" val customTags = mapOf( "USERNAME" to "John", "DAY_OF_WEEK" to "Thursday" ) val nativeView = AdaptyUI.createNativePaywallView( context = context, viewModelStoreOwner = activity, paywall = paywall, observer = myPaywallObserver, customTags = customTags, ) ``` ```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)" val customTags = mapOf( "USERNAME" to "John", "DAY_OF_WEEK" to "Thursday" ) val nativeView = AdaptyUI.createNativePaywallView( paywall = paywall, observer = myPaywallObserver, customTags = customTags, ) ``` </TabItem> </Tabs> ## Bộ đếm ngược tùy chỉnh \{#custom-timers\} Bộ đếm ngược trên paywall là công cụ tuyệt vời để quảng bá các ưu đãi đặc biệt và theo mùa có giới hạn thời gian. Tuy nhiên, cần lưu ý rằng bộ đếm này không liên kết với thời hạn hiệu lực của ưu đãi hay thời gian chạy chiến dịch. Đây đơn giản chỉ là một bộ đếm ngược độc lập, bắt đầu từ giá trị bạn đặt và giảm dần về không. Khi bộ đếm về không, không có gì xảy ra—nó chỉ dừng lại ở không. Bạn có thể tùy chỉnh văn bản trước và sau bộ đếm để tạo thông điệp mong muốn, ví dụ: "Ưu đãi kết thúc sau: 10:00 giây." Để sử dụng bộ đếm ngược tùy chỉnh trong paywall, hãy truyền chúng khi tạo paywall view: <Tabs> <TabItem value="standalone" label="With Compose Multiplatform" default> ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { val customTimers = mapOf( "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0), "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59) ) AdaptyUI.createPaywallView( paywall = paywall, customTimers = customTimers ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` </TabItem> <TabItem value="native" label="Without Compose Multiplatform"> ```kotlin showLineNumbers title="Kotlin Multiplatform (Android)" val customTimers = mapOf( "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0), "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59) ) val nativeView = AdaptyUI.createNativePaywallView( context = context, viewModelStoreOwner = activity, paywall = paywall, observer = myPaywallObserver, customTimers = customTimers, ) ``` ```kotlin showLineNumbers title="Kotlin Multiplatform (iOS)" val customTimers = mapOf( "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0), "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59) ) val nativeView = AdaptyUI.createNativePaywallView( paywall = paywall, observer = myPaywallObserver, customTimers = customTimers, ) ``` </TabItem> </Tabs> </SDKv3> --- # File: kmp-handle-paywall-actions --- --- title: "Phản hồi các hành động trong flow - Kotlin Multiplatform" description: "Xử lý các hành động nút từ flow và paywall trong ứng dụng Kotlin Multiplatform của bạn." --- <SDKv4> Nếu bạn đang xây dựng flow hoặc paywall bằng Adapty Flow Builder hoặc 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 builder](paywall-buttons) và gán cho nó một action có sẵn hoặc tạo một custom action ID. 2. Viết code trong ứng dụng của bạn để xử lý từng action bạn đã gán. Hướng dẫn này cho thấy cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Chỉ có các thao tác mua hàng, khôi phục, đóng flow/paywall và mở liên kết được xử lý tự động.** Tất cả các action nút khác, chẳng hạn như custom action, đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: ## Thiết lập AdaptyUIFlowsEventsObserver \{#set-up-the-adaptyuiflowseventsobserver\} Để xử lý các hành động trong flow, bạn cần implement interface `AdaptyUIFlowsEventsObserver` và thiết lập nó với `AdaptyUI.setFlowsEventsObserver()`. Bước này nên được thực hiện sớm trong vòng đời ứng dụng, thường là trong activity chính hoặc khi khởi tạo ứng dụng. ```kotlin // In your app initialization AdaptyUI.setFlowsEventsObserver(MyAdaptyUIFlowsEventsObserver()) ``` Tất cả các hành động của nút đều được truyền vào callback `flowViewDidPerformAction(view, action)` dưới dạng sealed class `AdaptyUIAction`: `CloseAction`, `AndroidSystemBackAction`, `OpenUrlAction`, hoặc `CustomAction`. :::warning Việc override `flowViewDidPerformAction` sẽ thay thế toàn bộ xử lý mặc định cho **tất cả** các hành động, không chỉ hành động bạn quan tâm. Hãy giữ nguyên các nhánh mặc định cho `CloseAction` (đóng flow) và `OpenUrlAction` (mở URL) trừ khi bạn muốn thay đổi chúng, như được minh họa trong các ví dụ bên dưới. ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall: 1. Trong builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code ứng dụng, triển khai trình xử lý cho hành động `close` để đóng flow. :::info Trong Kotlin Multiplatform SDK, `CloseAction` mặc định sẽ kích hoạt việc đóng flow hoặc paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một flow có thể kích hoạt mở một flow khác. ::: ```kotlin class MyAdaptyUIFlowsEventsObserver : AdaptyUIFlowsEventsObserver { override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CloseAction -> mainUiScope.launch { view.dismiss() } // default behavior is AdaptyUIAction.OpenUrlAction -> AdaptyUI.openWebUrl(action.url, action.openIn) // default behavior else -> Unit } } } // Set up the observer AdaptyUI.setFlowsEventsObserver(MyAdaptyUIFlowsEventsObserver()) ``` Nếu bạn đang dùng [`createNativeFlowView`](kmp-present-paywalls#without-compose-multiplatform), việc gọi `view.dismiss()` sẽ không có tác dụng — view được nhúng vào layout của bạn, không được hiển thị qua stack KMP. Hãy xóa view khỏi layout và gọi `dispose()` trên đó thay thế. ## Xử lý nút back của hệ thống Android \{#handle-the-android-system-back-button\} Khi nhấn nút back của hệ thống Android (hoặc dùng cử chỉ vuốt back), sự kiện `AdaptyUIAction.AndroidSystemBackAction` sẽ được phát ra. Mặc định, hành động này bị bỏ qua — flow vẫn mở, và người dùng thoát khỏi nó theo đường dẫn bạn định nghĩa, chẳng hạn nút **Close** hoặc hành động `on_device_back` trong builder. Nếu bạn muốn nút back của hệ thống đóng flow lại, hãy tự xử lý hành động đó: ```kotlin class MyAdaptyUIFlowsEventsObserver : AdaptyUIFlowsEventsObserver { override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CloseAction -> mainUiScope.launch { view.dismiss() } // default behavior is AdaptyUIAction.AndroidSystemBackAction -> mainUiScope.launch { view.dismiss() } // not handled by default is AdaptyUIAction.OpenUrlAction -> AdaptyUI.openWebUrl(action.url, action.openIn) // default behavior else -> Unit } } } ``` ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục mua hàng), hãy thêm phần tử **Link** trong builder và xử lý nó theo cách tương tự như các nút với hành động **Open URL**. ::: Để thêm nút mở liên kết từ flow hoặc paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**), trong builder, hãy thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. Theo mặc định, SDK sẽ mở URL nhận được theo cách tự nhiên — trong trình duyệt ngoài hoặc trình duyệt trong ứng dụng, tùy thuộc vào `action.openIn` — nên bạn không cần viết thêm code. Chỉ cần ghi đè handler khi bạn muốn xử lý tùy chỉnh, ví dụ như hiển thị hộp thoại xác nhận trước: ```kotlin class MyAdaptyUIFlowsEventsObserver( private val uriHandler: UriHandler ) : AdaptyUIFlowsEventsObserver { override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.OpenUrlAction -> { // Show confirmation dialog before opening URL mainUiScope.launch { val selectedAction = view.showDialog( title = "Open URL?", content = action.url, primaryActionTitle = "Cancel", secondaryActionTitle = "Open" ).getOrNull() when (selectedAction) { AdaptyUIDialogActionType.PRIMARY -> { // User cancelled } AdaptyUIDialogActionType.SECONDARY -> { // User confirmed - open URL uriHandler.openUri(action.url) } else -> Unit } } } else -> Unit } } } // Set up the observer with UriHandler AdaptyUI.setFlowsEventsObserver(MyAdaptyUIFlowsEventsObserver(uriHandler)) ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng: 1. Trong builder, thêm một nút và gán cho nó hành động **Custom** với ID "login". 2. Trong code ứng dụng của bạn, triển khai một handler cho hành động tùy chỉnh đó để xác định người dùng. ```kotlin class MyAdaptyUIFlowsEventsObserver : AdaptyUIFlowsEventsObserver { override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CustomAction -> { if (action.action == "login") { // Handle login action - navigate to login screen // This depends on your app's navigation system // For example, in Compose Multiplatform: // navController.navigate("login") } } else -> Unit } } } ``` ## Xử lý các action tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các action khác: 1. Trong builder, thêm một nút, gán cho nó action **Custom**, và đặt cho nó một ID. 2. Trong code ứng dụng của bạn, triển khai một handler cho action ID mà bạn đã tạo. Ví dụ: nếu bạn có một tập hợp ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút sẽ hiển thị một flow hoặc paywall khác: ```kotlin class MyAdaptyUIFlowsEventsObserver : AdaptyUIFlowsEventsObserver { override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CustomAction -> { when (action.action) { "openNewFlow" -> { // Display another flow or paywall } } } else -> Unit } } } // Set up the observer AdaptyUI.setFlowsEventsObserver(MyAdaptyUIFlowsEventsObserver()) ``` </SDKv4> <SDKv3> :::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 cần được xử lý trong code của ứng dụng. ::: Nếu bạn đang xây dựng paywall bằng Adapty paywall builder, việc thiết lập 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 để xử lý từng hành động bạn đã gán. Hướng dẫn này mô tả 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. ## Thiết lập AdaptyUIPaywallsEventsObserver \{#set-up-the-adaptyuipaywallsevents-observer\} Để xử lý các hành động trên paywall, bạn cần triển khai interface `AdaptyUIPaywallsEventsObserver` và thiết lập nó với `AdaptyUI.setPaywallsEventsObserver()`. Bước này nên được thực hiện sớm trong vòng đời của ứng dụng, thường là trong activity chính hoặc khi khởi tạo ứng dụng. ```kotlin // In your app initialization AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` ## Đó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 của bạn, triển khai một handler cho hành động `close` để đóng paywall. :::info Trong Kotlin Multiplatform SDK, `CloseAction` và `AndroidSystemBackAction` sẽ kích hoạt việc đóng paywall theo mặc định. 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 việc mở một paywall khác. ::: ```kotlin class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { AdaptyUIAction.CloseAction, AdaptyUIAction.AndroidSystemBackAction -> view.dismiss() } } } // Set up the observer AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` Nếu bạn đang sử dụng [`createNativePaywallView`](kmp-present-paywalls#without-compose-multiplatform), việc gọi `view.dismiss()` sẽ không có tác dụng — view được nhúng vào layout của bạn, không được hiển thị qua stack KMP. Thay vào đó, hãy xóa view khỏi layout và gọi `dispose()` trên nó. ## 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 mua hàng), hãy thêm phần tử **Link** trong paywall builder và xử lý nó theo cách tương tự như các nút với hành động **Open URL**. ::: Để thêm một nút mở liên kết từ paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong paywall builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code của ứng dụng, triển khai một handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong Kotlin Multiplatform SDK, `OpenUrlAction` cung cấp URL cần mở. Bạn có thể triển khai logic tùy chỉnh để xử lý việc mở URL, chẳng hạn như hiển thị hộp thoại xác nhận hoặc sử dụng phương thức xử lý URL ưa thích của ứng dụng. ::: ```kotlin class MyAdaptyUIPaywallsEventsObserver( private val uriHandler: UriHandler ) : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.OpenUrlAction -> { // Show confirmation dialog before opening URL mainUiScope.launch { val selectedAction = view.showDialog( title = "Open URL?", content = action.url, primaryActionTitle = "Cancel", secondaryActionTitle = "Open" ).getOrNull() when (selectedAction) { AdaptyUIDialogActionType.PRIMARY -> { // User cancelled } AdaptyUIDialogActionType.SECONDARY -> { // User confirmed - open URL uriHandler.openUri(action.url) } else -> Unit } } } } } } // Set up the observer with UriHandler AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver(uriHandler)) ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng của bạn: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Custom** với ID "login". 2. Trong code ứng dụng của bạn, triển khai một handler cho hành động tùy chỉnh đó để xác định danh tính người dùng. ```kotlin class MyAdaptyUIObserver : AdaptyUIObserver { override fun paywallViewDidPerformAction(view: AdaptyUIView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CustomAction -> { if (action.action == "login") { // Handle login action - navigate to login screen // This depends on your app's navigation system // For example, in Compose Multiplatform: // navController.navigate("login") } } } } } ``` ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và đặt cho nó một ID. 2. Trong code của ứng dụng, triển khai một handler cho action ID 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 một nút để hiển thị một paywall khác: ```kotlin class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CustomAction -> { when (action.action) { "login" -> { // Handle login action - navigate to login screen // This depends on your app's navigation system // For example, in Compose Multiplatform: // navController.navigate("login") } } } } } } // Set up the observer AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` </SDKv3> --- # File: kmp-handling-events --- --- title: "Xử lý sự kiện flow & paywall - Kotlin Multiplatform" description: "Xử lý các sự kiện flow và paywall trong ứng dụng Kotlin Multiplatform của bạn." --- <SDKv4> :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, lựa chọn sản phẩm và hiển thị flow. Bạn cũng phải triển khai xử lý nút (đóng flow, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động trong flow](kmp-handle-paywall-actions) để biết thêm chi tiết. ::: Flow và paywall được cấu hình bằng [Flow Builder](adapty-flow-builder) hoặc [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, lựa chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua. Tìm hiểu cách phản hồi những sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình flow trong ứng dụng di động của bạn, hãy triển khai các phương thức của interface `AdaptyUIFlowsEventsObserver` và đăng ký observer với `AdaptyUI.setFlowsEventsObserver()`. Một số phương thức có sẵn các implementation mặc định xử lý các tình huống phổ biến một cách tự động, vì vậy chỉ cần override những phương thức bạn muốn thay đổi: ```kotlin showLineNumbers title="Kotlin" AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver { // override only the methods you want to change }) ``` :::note Đây là nơi bạn thêm logic tùy chỉnh để xử lý các sự kiện flow. Bạn có thể dùng `view.dismiss()` để đóng flow, hoặc triển khai bất kỳ hành vi tùy chỉnh nào khác bạn cần. Lưu ý rằng `dismiss()` là một suspend function — bên trong callback, hãy khởi chạy nó trên `mainUiScope` của observer: `mainUiScope.launch { view.dismiss() }`. ::: ### Sự kiện do người dùng tạo ra #### Flow xuất hiện và biến mất Khi một flow xuất hiện hoặc biến mất, các phương thức sau sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidAppear(view: AdaptyUIFlowView) { // Handle flow appearance // You can track analytics or update UI here } override fun flowViewDidDisappear(view: AdaptyUIFlowView) { // Handle flow disappearance // You can track analytics or update UI here } ``` :::note - Trên iOS, `flowViewDidAppear` cũng được gọi khi người dùng nhấn vào [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một flow, và web paywall mở trong trình duyệt trong ứng dụng. - Trên iOS, `flowViewDidDisappear` cũng được gọi khi một [web paywall](web-paywall#step-2a-add-a-web-purchase-button) được mở từ một flow trong trình duyệt trong ứng dụng biến mất khỏi màn hình. ::: <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript // Flow appeared { // No additional data } // Flow disappeared { // No additional data } ``` </Details> #### Chọn sản phẩm \{#product-selection\} Nếu người dùng chọn một sản phẩm để mua, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidSelectProduct(view: AdaptyUIFlowView, productId: String) { // Handle product selection // You can update UI or track analytics here } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "productId": "premium_monthly" } ``` </Details> #### Bắt đầu mua hàng \{#started-purchase\} Nếu người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidStartPurchase(view: AdaptyUIFlowView, product: AdaptyPaywallProduct) { // Handle purchase start // You can show loading indicators or track analytics here } ``` :::note Trong [chế độ Observer](kmp-present-flows-in-observer-mode), các giao dịch mua được bắt đầu từ một flow sẽ được chuyển đến `AdaptyUIObserverModeResolver` của bạn. ::: <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ``` </Details> #### Mua hàng thành công, bị hủy, hoặc đang chờ xử lý \{#successful-canceled-or-pending-purchase\} Phương thức này sẽ được gọi khi một giao dịch mua hoàn tất. Mặc định, nó không làm gì cả — flow vẫn mở sau khi mua hàng cho đến khi bạn tự đóng nó, vì vậy hãy tự gọi `view.dismiss()` sau khi người dùng được cấp quyền truy cập: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidFinishPurchase( view: AdaptyUIFlowView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { when (purchaseResult) { is AdaptyPurchaseResult.Success -> { // Check if user has access to premium features if (purchaseResult.profile.accessLevels["premium"]?.isActive == true) { mainUiScope.launch { view.dismiss() } } } AdaptyPurchaseResult.Pending -> { // Handle pending purchase (e.g., user will pay offline with cash) } AdaptyPurchaseResult.UserCanceled -> { // Handle user cancellation } } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "Success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // 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": "Pending" } } // User canceled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "UserCanceled" } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình flow khi mua hàng thành công. #### Mua hàng thất bại \{#failed-purchase\} Phương thức này sẽ được gọi khi một giao dịch mua hàng thất bại do lỗi. Điều này bao gồm các lỗi từ StoreKit/Google Play Billing (hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng), lỗi xác minh giao dịch và lỗi hệ thống. Lưu ý rằng khi người dùng hủy, `flowViewDidFinishPurchase` sẽ được gọi với kết quả bị hủy thay vì phương thức này, và các thanh toán đang chờ xử lý cũng không kích hoạt phương thức này. ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidFailPurchase( view: AdaptyUIFlowView, product: AdaptyPaywallProduct, error: AdaptyError ) { // Add your purchase failure handling logic here // For example: show error message, retry option, or custom error handling } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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" } } } ``` </Details> #### Bắt đầu khôi phục \{#started-restore\} Nếu người dùng khởi động quá trình khôi phục, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidStartRestore(view: AdaptyUIFlowView) { // Handle restore start // You can show loading indicators or track analytics here } ``` #### Khôi phục thành công \{#successful-restore\} Nếu việc khôi phục giao dịch mua thành công, phương thức này sẽ được gọi. Theo mặc định, nó không làm gì — flow vẫn mở sau khi khôi phục cho đến khi bạn đóng nó: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidFinishRestore(view: AdaptyUIFlowView, profile: AdaptyProfile) { // Add your successful restore handling logic here // For example: show success message, update UI, or dismiss the flow // Check if user has access to premium features if (profile.accessLevels["premium"]?.isActive == true) { mainUiScope.launch { view.dismiss() } } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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" } ] } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` yêu cầu. Tham khảo chủ đề [Trạng thái gói đăng ký](subscription-status) để tìm hiểu cách kiểm tra. #### Khôi phục thất bại \{#failed-restore\} Nếu `Adapty.restorePurchases()` thất bại, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidFailRestore(view: AdaptyUIFlowView, error: AdaptyError) { // Add your restore failure handling logic here // For example: show error message, retry option, or custom error handling } ``` <Details> <summary>Ví dụ về sự kiện (Nhấp để mở rộng)</summary> ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ``` </Details> #### Hoàn tất điều hướng thanh toán web \{#web-payment-navigation-completion\} Nếu người dùng bắt đầu quá trình mua hàng thông qua [web paywall](web-paywall), phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidFinishWebPaymentNavigation( view: AdaptyUIFlowView, product: AdaptyPaywallProduct?, error: AdaptyError? ) { if (error != null) { // Handle web payment navigation error } else { // Handle successful web payment navigation } } ``` <Details> <summary>Ví dụ về sự kiện (Nhấn để mở rộng)</summary> ```javascript // Successful web payment 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 web payment navigation { "product": null, "error": { "code": "web_payment_failed", "message": "Web payment navigation failed", "details": { "underlyingError": "Network connection error" } } } ``` </Details> ### 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 sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ server. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức sau: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidFailLoadingProducts(view: AdaptyUIFlowView, error: AdaptyError) { // Add your product loading failure handling logic here // For example: show error message, retry option, or custom error handling } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ``` </Details> #### Lỗi hiển thị và lỗi runtime \{#rendering-and-runtime-errors\} Nếu xảy ra lỗi trong quá trình hiển thị giao diện, hoặc một lỗi runtime không liên quan đến việc mua hàng, lỗi đó sẽ được báo cáo qua phương thức này. Theo mặc định, flow sẽ bị đóng khi có lỗi — ghi đè phương thức này để giữ flow mở hoặc thêm cách xử lý riêng của bạn: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidReceiveError(view: AdaptyUIFlowView, error: AdaptyError) { // Handle the error // The default implementation dismisses the flow; // once you override this method, dismissal is up to you } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render flow interface", "details": { "underlyingError": "Invalid flow configuration" } } } ``` </Details> Trong tình huống bình thường, những lỗi này không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết. #### Sự kiện analytics \{#analytics-events\} Callback `flowViewDidReceiveAnalyticEvent` được dành cho các sự kiện analytics tùy chỉnh từ một flow. Các flow chưa phát ra những sự kiện này đến code của bạn, vì vậy bạn không cần triển khai nó: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidReceiveAnalyticEvent( view: AdaptyUIFlowView, name: String, paramsJsonString: String ) { // Reserved for custom analytic events from a flow } ``` ### Điều hướng \{#navigation\} #### Nút Back hệ thống Android \{#android-system-back-button\} Mặc định, một flow không thể bị đóng bằng nút back hệ thống Android hoặc thao tác vuốt để quay lại — cài đặt mặc định của `flowViewDidPerformAction` chỉ đóng flow khi nhận `CloseAction` và bỏ qua `AndroidSystemBackAction`, do đó người dùng chỉ rời khỏi flow qua con đường bạn định nghĩa, chẳng hạn như nút **Close** hoặc action `on_device_back` trong builder. Nếu muốn nút back hệ thống có thể đóng flow, hãy tự xử lý action đó: ```kotlin showLineNumbers title="Kotlin" override fun flowViewDidPerformAction(view: AdaptyUIFlowView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CloseAction -> mainUiScope.launch { view.dismiss() } // default behavior is AdaptyUIAction.AndroidSystemBackAction -> mainUiScope.launch { view.dismiss() } // not handled by default is AdaptyUIAction.OpenUrlAction -> AdaptyUI.openWebUrl(action.url, action.openIn) // default behavior else -> Unit } } ``` Xem [hướng dẫn xử lý các hành động flow](kmp-handle-paywall-actions) để biết danh sách đầy đủ các hành động. </SDKv4> <SDKv3> Các paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm các lần nhấn nút (nút đóng, URL, lựa chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện 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**. ::: Để kiểm soát hoặc theo dõi các sự kiện xảy ra trên màn hình paywall trong ứng dụng của bạn, hãy implement các phương thức của interface `AdaptyUIPaywallsEventsObserver`. Một số phương thức có implementation mặc định xử lý các trường hợp phổ biến một cách tự động. :::note Đây là nơi bạn thêm logic tùy chỉnh để phản hồi các sự kiện trên paywall. Bạn có thể dùng `view.dismiss()` để đóng paywall, hoặc implement bất kỳ hành vi tùy chỉnh nào khác mà bạn cần. ::: ## Sự kiện do người dùng tạo ra \{#user-generated-events\} ### Giao diện paywall hiện và ẩn \{#paywall-appearance-and-disappearance\} Khi paywall hiện ra hoặc ẩn đi, các phương thức sau sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidAppear(view: AdaptyUIPaywallView) { // Handle paywall appearance // You can track analytics or update UI here } override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) { // Handle paywall disappearance // You can track analytics or update UI here } ``` :::note - Trên iOS, `paywallViewDidAppear` cũng được gọi khi người dùng nhấn vào [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall, và web paywall mở trong trình duyệt trong ứng dụng. - Trên iOS, `paywallViewDidDisappear` 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. ::: <Details> <summary>Ví dụ về sự kiện (Nhấp để mở rộng)</summary> ```javascript // Paywall appeared { // No additional data } // Paywall disappeared { // No additional data } ``` </Details> ### Chọn sản phẩm \{#product-selection\} Nếu người dùng chọn một sản phẩm để mua, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) { // Handle product selection // You can update UI or track analytics here } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "productId": "premium_monthly" } ``` </Details> ### Bắt đầu mua hàng \{#started-purchase\} Nếu người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) { // Handle purchase start // You can show loading indicators or track analytics here } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ``` </Details> ### Mua hàng thành công, bị hủy hoặc đang chờ xử lý \{#successful-canceled-or-pending-purchase\} Nếu một giao dịch mua thành công, phương thức này sẽ được gọi. Theo mặc định, nó sẽ tự động đóng paywall trừ khi giao dịch bị người dùng hủy: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFinishPurchase( view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { when (purchaseResult) { is AdaptyPurchaseResult.Success -> { // Check if user has access to premium features if (purchaseResult.profile.accessLevels["premium"]?.isActive == true) { view.dismiss() } } AdaptyPurchaseResult.Pending -> { // Handle pending purchase (e.g., user will pay offline with cash) } AdaptyPurchaseResult.UserCanceled -> { // Handle user cancellation } } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "Success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // 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": "Pending" } } // User canceled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "UserCanceled" } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình paywall khi mua hàng thành công. ### Mua hàng thất bại \{#failed-purchase\} Nếu giao dịch mua hàng thất bại do lỗi, phương thức này sẽ được gọi. Điều này bao gồm các lỗi từ StoreKit/Google Play Billing (hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng), lỗi xác minh giao dịch và lỗi hệ thống. Lưu ý rằng khi người dùng hủy giao dịch, `paywallViewDidFinishPurchase` sẽ được gọi với kết quả đã hủy, còn các khoản thanh toán đang chờ xử lý sẽ không kích hoạt phương thức này. ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailPurchase( view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, error: AdaptyError ) { // Add your purchase failure handling logic here // For example: show error message, retry option, or custom error handling } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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" } } } ``` </Details> ### Bắt đầu khôi phục \{#started-restore\} Nếu người dùng khởi động quá trình khôi phục, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidStartRestore(view: AdaptyUIPaywallView) { // Handle restore start // You can show loading indicators or track analytics here } ``` ### Khôi phục thành công \{#successful-restore\} Nếu việc khôi phục giao dịch mua thành công, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) { // Add your successful restore handling logic here // For example: show success message, update UI, or dismiss paywall // Check if user has access to premium features if (profile.accessLevels["premium"]?.isActive == true) { view.dismiss() } } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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" } ] } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` cần thiết. Tham khảo mục [Trạng thái gói đăng ký](subscription-status) để tìm hiểu cách kiểm tra. ### Khôi phục thất bại \{#failed-restore\} Nếu `Adapty.restorePurchases()` thất bại, phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your restore failure handling logic here // For example: show error message, retry option, or custom error handling } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ``` </Details> ### Hoàn tất điều hướng thanh toán web \{#web-payment-navigation-completion\} Nếu người dùng bắt đầu quá trình mua hàng thông qua [web paywall](web-paywall), phương thức này sẽ được gọi: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFinishWebPaymentNavigation( view: AdaptyUIPaywallView, product: AdaptyPaywallProduct?, error: AdaptyError? ) { if (error != null) { // Handle web payment navigation error } else { // Handle successful web payment navigation } } ``` <Details> <summary>Ví dụ về sự kiện (Nhấp để mở rộng)</summary> ```javascript // Successful web payment 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 web payment navigation { "product": null, "error": { "code": "web_payment_failed", "message": "Web payment navigation failed", "details": { "underlyingError": "Network connection error" } } } ``` </Details> ## Tải và hiển thị dữ liệu \{#data-fetching-and-rendering\} ### Lỗi tải sản phẩm \{#product-loading-errors\} Nếu bạn không truyền các sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự 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: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your product loading failure handling logic here // For example: show error message, retry option, or custom error handling } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ``` </Details> ### Lỗi hiển thị \{#rendering-errors\} Nếu có lỗi xảy ra trong quá trình hiển thị giao diện, lỗi đó sẽ được báo cáo qua phương thức này: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) { // Handle rendering error // In a normal situation, such errors should not occur // If you come across one, please let us know } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ``` </Details> Trong trường hợp bình thường, các lỗi này không nên xảy ra, vì vậy nếu bạn gặp phải, vui lòng thông báo cho chúng tôi. </SDKv3> --- # File: kmp-use-fallback-paywalls --- --- title: "Kotlin Multiplatform - 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" --- Để duy trì trải nghiệm người dùng mượt mà, điều quan trọng là phải thiết lập [paywall dự phòng](/fallback-paywalls) cho các flow, [paywall](paywalls) và [onboarding](onboardings) của bạn. Biện pháp phòng ngừa này giúp mở rộng khả năng của ứng dụng trong trường hợp mất kết nối internet một phần hoặc hoàn toàn. * **Nếu ứng dụng không thể kết nối đến máy chủ Adapty:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng, và truy cập cấu hình onboarding đã lưu cục bộ. * **Nếu ứng dụng không thể kết nối internet:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng. Onboarding chứa nội dung từ xa và cần có kết nối internet để hoạt động. :::important Trước khi thực hiện các bước trong hướng dẫn này, hãy [tải xuống](/local-fallback-paywalls) các file cấu hình dự phòng từ Adapty. ::: ## Cấu hình \{#configuration\} 1. Thêm file cấu hình dự phòng vào ứng dụng của bạn. * Nếu nền tảng mục tiêu là Android, hãy chuyển file cấu hình dự phòng vào thư mục `android/app/src/main/assets/`. * Nếu nền tảng mục tiêu là iOS, hãy thêm file JSON dự phòng vào bundle dự án của bạn. (**File** -> **Add Files to YourProjectName**) 2. Gọi phương thức `.setFallback` **trước khi** bạn tải flow, paywall, hoặc onboarding mục tiêu. 3. Đặt tham số `assetId`, tùy thuộc vào nền tảng mục tiêu của bạn. * Android: Sử dụng đường dẫn tệp tương đối so với thư mục `assets`. * iOS: Sử dụng tên tệp đầy đủ. ```kotlin showLineNumbers Adapty.setFallback(assetId = "fallback.json") .onSuccess { // Fallback paywalls loaded successfully } .onError { error -> // Handle the error } ``` :::important `setFallback` phải chạy trước khi SDK tải flow, paywall hoặc onboarding mục tiêu. ::: Tham số: | Tham số | Mô tả | | :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **assetId** | Tên tệp cấu hình dự phòng (iOS). <br /> Đường dẫn tệp cấu hình dự phòng, tính từ thư mục `assets` (Android). | :::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: kmp-localizations-and-locale-codes --- --- title: "Sử dụng localization và mã ngôn ngữ trong Kotlin Multiplatform SDK" description: "Quản lý localization và mã ngôn ngữ để tiếp cận người dùng toàn cầu trong ứng dụng Kotlin Multiplatform của bạn." --- <SDKv4> ## Tại sao điều này quan trọng \{#why-this-is-important\} Mã locale được dùng khi Adapty chọn ngôn ngữ hiển thị cho một flow, và khi bạn đọc Remote Config cho một paywall tùy chỉnh. Mã locale khá phức tạp và có thể khác nhau tùy từng nền tảng, vì vậy Adapty sử dụng một chuẩn nội bộ thống nhất trên mọi nền tảng được hỗ trợ. Hiểu rõ chuẩn đó giúp bạn dự đoán được ngôn ngữ nào người dùng sẽ nhận được. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng tiêu chuẩn [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) được chỉnh sửa nhẹ: mỗi mã bao gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Khớp mã ngôn ngữ \{#locale-code-matching\} Khi Adapty tìm kiếm bản địa hóa phù hợp với ngôn ngữ của người dùng, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Adapty tìm kiếm bản địa hóa có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy, Adapty lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` trong `pt-br`) và tìm kiếm bản địa hóa phù hợp 4. Nếu vẫn không tìm thấy, Adapty trả về bản địa hóa mặc định `en` Theo cách này, `'pt_BR'`, `pt-BR` và `pt-br` đều được ánh xạ đến cùng một bản địa hóa. ## Triển khai bản địa hóa \{#implementing-localizations\} Trong SDK v4, bạn không cần truyền mã ngôn ngữ khi lấy một flow. - **Paywall từ Flow Builder và Paywall Builder**: Adapty tự động xác định bản địa hóa dựa trên thiết bị và các bản địa hóa bạn đã cấu hình trong builder. Render flow bằng `createFlowView` — không cần mã ngôn ngữ. - **Paywall tùy chỉnh (Remote Config)**: `getFlow` trả về tất cả các bản địa hóa đã cấu hình trong `flow.remoteConfigs`. Mỗi mục là một `AdaptyRemoteConfig` với `locale` và `dataMap`. Hãy chọn mục phù hợp với người dùng, tự xử lý logic dự phòng theo ý bạn: ```kotlin showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID") .onSuccess { flow -> val config = flow.remoteConfigs.firstOrNull { it.locale == "en" } ?: flow.remoteConfigs.firstOrNull() // read your values from config?.dataMap } .onError { error -> // handle the error } ``` Các quy tắc khớp mã locale ở trên mô tả cách Adapty chuẩn hóa các mã `locale` được lưu trữ trên mỗi Remote Config. </SDKv4> <SDKv3> ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một số trường hợp mà locale code phát huy vai trò quan trọng — ví dụ, khi bạn cần lấy đúng paywall theo ngôn ngữ hiện tại của ứng dụng. Vì locale code khá phức tạp và có thể khác nhau giữa các nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ thống nhất cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng bản địa hóa, và điều gì xảy ra tiếp theo — để đảm bảo bạn luôn nhận được kết quả như kỳ vọng. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng tiêu chuẩn [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) được chỉnh sửa một chút: mỗi mã gồm các subtag viết thường, ngăn cách nhau bằng dấu gạch ngang. Ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Khớp mã ngôn ngữ \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client kèm mã ngôn ngữ và bắt đầu tìm kiếm bản dịch tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ đầu vào được chuyển về 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 bản dịch có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy, hệ thống lấy phần chuỗi trước dấu gạch ngang đầu tiên (`pt` trong `pt-br`) và tìm kiếm bản dịch khớp với chuỗi đó 4. Nếu vẫn không tìm thấy, hệ thống trả về bản dịch mặc định `en` Bằng cách này, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` đều sẽ nhận được cùng một kết quả. ## Triển khai bản địa hóa: cách được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang cân nhắc về bản địa hóa, khả năng cao là bạn đã làm việc với các tệp tài nguyên chuỗi đã được bản địa hóa trong dự án của mình. Nếu vậy, chúng tôi khuyến nghị bạn đặt một cặp key-value với mã locale Adapty tương ứng vào từng tệp tài nguyên cho mỗi bản địa hóa. Sau đó, lấy giá trị của key đó khi gọi SDK của chúng tôi, như sau: ```kotlin showLineNumbers // 1. Add the Adapty locale code to your Compose Multiplatform resources /* composeResources/values/strings.xml (default — English) */ <string name="adapty_paywalls_locale">en</string> /* composeResources/values-es/strings.xml (Spanish) */ <string name="adapty_paywalls_locale">es</string> /* composeResources/values-pt-rBR/strings.xml (Portuguese — Brazil) */ <string name="adapty_paywalls_locale">pt-br</string> // 2. Extract and use the locale code suspend fun fetchPaywall() { val locale = getString(Res.string.adapty_paywalls_locale) Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = locale ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } } ``` Bằng cách đó, bạn có thể đảm bảo rằng mình hoàn toàn kiểm soát được bản dịch nào sẽ được tải về cho từng người dùng trong ứng dụng. Nếu bạn không sử dụng Compose Multiplatform resources, ý tưởng tương tự cũng áp dụng cho bất kỳ thư viện localization nào bạn đang dùng (ví dụ: [moko-resources](https://github.com/icerockdev/moko-resources)) — hãy lưu mã ngôn ngữ Adapty dưới dạng chuỗi trong từng resource bundle của mỗi ngôn ngữ, rồi đọc nó trước khi gọi SDK. ## Triển khai bản địa hóa: cách khác \{#implementing-localizations-the-other-way\} Bạn có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống nhau) mà không cần định nghĩa tường minh mã ngôn ngữ cho từng bản địa hóa. Cách này sẽ trích xuất mã ngôn ngữ trực tiếp từ thiết bị — điều này yêu cầu khai báo `expect`/`actual`, vì không có API ngôn ngữ dùng chung trong `commonMain`: ```kotlin showLineNumbers // commonMain expect fun currentLocaleTag(): String // androidMain actual fun currentLocaleTag(): String = Locale.getDefault().toLanguageTag() // iosMain actual fun currentLocaleTag(): String = NSLocale.currentLocale.localeIdentifier // commonMain — pass the locale code to Adapty suspend fun fetchPaywall() { Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = currentLocaleTag() ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } } ``` Lưu ý rằng chúng tôi không khuyến nghị cách tiếp cận này vì một số lý do: 1. Trên iOS, ngôn ngữ ưa thích của người dùng và locale khu vực của thiết bị không giống nhau. `NSLocale.currentLocale.localeIdentifier` trả về locale khu vực, có thể khác với ngôn ngữ mà người dùng thực sự đọc ứng dụng của bạn. Các ứng dụng iOS sử dụng file chuỗi đã bản địa hóa dựa vào logic phân giải của Apple để kết hợp cả hai — điều này hoạt động tốt ngay từ đầu với cách tiếp cận được khuyến nghị ở trên. 2. Rất khó dự đoán chính xác thiết bị sẽ trả về gì và liệu nó có khớp với một bản địa hóa trong Adapty hay không. Locale của thiết bị có thể bao gồm các phần mở rộng hoặc mã vùng mà bạn chưa cấu hình trong Adapty, trong trường hợp đó SDK sẽ dự phòng về kết quả khớp với subtag đầu tiên hoặc cuối cùng là về `en`. Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. </SDKv3> --- # File: kmp-web-paywalls --- --- title: "Triển khai web paywall trong Kotlin Multiplatform SDK" description: "Thiết lập web paywall để nhận thanh toán mà không phải chịu phí và kiểm duyệt của cửa hàng." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trong dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.15 trở lên. ::: ## Mở web paywall \{#open-web-paywalls\} Nếu bạn đang làm việc với paywall do chính bạn 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 giú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 chu kỳ ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không. Nhờ đó, 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. :::note Sau khi người dùng quay lại ứng dụng, hãy làm mới giao diện để phản ánh các cập nhật hồ sơ người dùng. Adapty sẽ nhận và xử lý các sự kiện cập nhật hồ sơ người dùng. ::: ```kotlin showLineNumbers viewModelScope.launch { Adapty.openWebPaywall(product = product).onSuccess { // the web paywall was opened successfully }.onError { error -> // handle the error } } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall(product = product)` tạo URL theo paywall và thêm dữ liệu sản phẩm vào URL. 2. `openWebPaywall(paywall = paywall)` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng khi các sản phẩm trong Adapty paywall của bạn khác với các sản phẩm trong web paywall. Trong SDK v4, tham số `paywall` được thay thế bằng tham số `flowPaywall` nhận một `AdaptyFlowPaywall` — một trong các biến thể paywall trong `flow.paywalls`. Xem [hướng dẫn migration](migration-to-kmp-sdk-v4). ::: ## Mở web paywall trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} Theo mặc định, web paywall sẽ mở trong trình duyệt bên ngoài. Để mang lại trải nghiệm liền mạch cho người dùng, bạn có thể mở web paywall trong trình duyệt tích hợp ngay trong ứng dụng. Cách này hiển thị trang mua hàng trực tiếp bên trong ứng dụng của bạn, giúp người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt tham số `openIn` thành `AdaptyWebPresentation.IN_APP_BROWSER`: ```kotlin showLineNumbers viewModelScope.launch { Adapty.openWebPaywall( product = product, openIn = AdaptyWebPresentation.IN_APP_BROWSER // default – EXTERNAL_BROWSER ).onSuccess { // the web paywall was opened successfully }.onError { error -> // handle the error } } ``` --- # File: kmp-present-flows-in-observer-mode --- --- title: "Hiển thị flow trong Observer mode - Kotlin Multiplatform" description: "Hiển thị flow và paywall Paywall Builder trong Observer mode trong ứng dụng Kotlin Multiplatform của bạn trong khi xử lý giao dịch mua bằng code của riêng bạn." --- Nếu bạn đã tùy chỉnh một flow hoặc paywall bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall đó đã chứa cả nội dung cần hiển thị lẫn cách hiển thị. :::warning Phần này chỉ áp dụng cho [Observer mode](observer-vs-full-mode). Nếu bạn không làm việc ở Observer mode, hãy tham khảo chủ đề [Hiển thị flows & paywalls](kmp-present-paywalls). ::: :::info Tính năng này yêu cầu Adapty Kotlin Multiplatform SDK 4.0 (beta) trở lên — trước đây chỉ có trong SDK native iOS và Android. Xem [hướng dẫn migration](migration-to-kmp-sdk-v4) để nâng cấp. ::: <details> <summary>Trước khi bắt đầu hiển thị flow (Click to Expand)</summary> 1. Thiết lập tích hợp ban đầu của Adapty [với App Store](initial_ios) và [với Google Play](initial-android). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo gọi `withObserverMode(true)` trong configuration builder. Tham khảo [hướng dẫn cài đặt Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform#activate-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình flow hoặc paywall trong các builder](create-paywall) và gán sản phẩm cho chúng. 5. [Tạo placement và gán flow hoặc paywall vào chúng](create-placement). 6. [Lấy flow và cấu hình của chúng](kmp-get-pb-paywalls) trong code ứng dụng di động của bạn. </details> Trong chế độ Observer, SDK không thực hiện mua hàng thay cho bạn. Khi người dùng nhấn nút mua hoặc khôi phục trong flow hay paywall do Adapty hiển thị, SDK sẽ gọi `AdaptyUIObserverModeResolver` của bạn — hãy thực hiện việc mua hoặc khôi phục bằng code của chính bạn ở đó. 1. Triển khai interface `AdaptyUIObserverModeResolver`: ```kotlin showLineNumbers import com.adapty.kmp.AdaptyUIObserverModeResolver import com.adapty.kmp.models.AdaptyPaywallProduct import com.adapty.kmp.models.AdaptyUIFlowView class MyObserverModeResolver : AdaptyUIObserverModeResolver { override fun observerModeDidInitiatePurchase( view: AdaptyUIFlowView, product: AdaptyPaywallProduct, onStartPurchase: () -> Unit, onFinishPurchase: () -> Unit ) { onStartPurchase() // the view shows its loading indicator // make the purchase with your own code, // then report the transaction to Adapty and call: onFinishPurchase() // the view hides the loading indicator } override fun observerModeDidInitiateRestore( view: AdaptyUIFlowView, onStartRestore: () -> Unit, onFinishRestore: () -> Unit ) { onStartRestore() // restore purchases with your own code, then: onFinishRestore() } } ``` `observerModeDidInitiatePurchase` thông báo cho bạn biết người dùng đã khởi tạo một giao dịch mua, còn `observerModeDidInitiateRestore` — rằng người dùng đã khởi tạo một lần khôi phục. Hãy kích hoạt flow mua hàng hoặc khôi phục tùy chỉnh của bạn để phản hồi. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về quá trình mua hàng hoặc khôi phục. Điều này cần thiết để flow hoạt động đúng, chẳng hạn như hiển thị loader và nhiều thứ khác: | Callback | Mô tả | | :----------------- | :--------------------------------------------------------------------------------------------- | | onStartPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã bắt đầu. | | onFinishPurchase() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã hoàn tất. | | onStartRestore() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã bắt đầu. | | onFinishRestore() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã hoàn tất. | Flow sẽ vẫn mở trong khi code của bạn chạy — hãy tự đóng nó sau khi mua hàng thành công hoặc khôi phục thành công. 2. Đăng ký resolver trước khi hiển thị bất kỳ màn hình nào: ```kotlin showLineNumbers import com.adapty.kmp.AdaptyUI AdaptyUI.setObserverModeResolver(MyObserverModeResolver()) ``` Nếu không có resolver được đăng ký, flow view không có cách nào chuyển giao việc mua hàng sang code của bạn, và sẽ không có gì xảy ra khi người dùng nhấn nút mua hàng. 3. Tạo và hiển thị flow view như bình thường: [lấy flow và tạo view của nó](kmp-get-pb-paywalls), sau đó [hiển thị nó](kmp-present-paywalls). Không cần thêm tham số nào — một khi resolver đã được đăng ký, mọi flow hoặc paywall do Adapty render đều định tuyến các giao dịch mua và khôi phục qua nó. :::warning Đừng quên [báo cáo giao dịch và liên kết nó với paywall](report-transactions-observer-mode-kmp). Nếu không, Adapty sẽ không nhận ra giao dịch và sẽ không xác định được paywall nguồn của giao dịch mua. ::: --- # File: kmp-troubleshoot-paywall-builder --- --- title: "Khắc phục sự cố Paywall Builder trong Kotlin Multiplatform SDK" description: "Khắc phục sự cố Paywall Builder trong Kotlin Multiplatform SDK" --- Hướng dẫn này giúp bạn giải quyết các sự cố thường gặp khi sử dụng paywall được thiết kế trong Adapty Paywall Builder với Kotlin Multiplatform 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 tạo được paywall view, hoặc paywall không có cấu hình view. **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. Bạn cũng có thể kiểm tra xem paywall có cấu hình view hay không bằng cách sử dụng thuộc tính `hasViewConfiguration` trên đối tượng `AdaptyPaywall`. <img src="/assets/shared/img/show-on-device.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Số lượt xem paywall quá lớn \{#the-paywall-view-number-is-too-big\} **Sự cố**: Số lượt xem paywall hiển thị gấp đôi so với 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 nếu bạn đang dùng Paywall Builder. Với paywall được thiết kế bằng Paywall Builder, analytics được theo dõi tự động, nên 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. --- # File: kmp-implement-paywalls-manually --- --- title: "Triển khai paywall thủ công trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách triển khai paywall thủ công trong ứng dụng Kotlin Multiplatform với Adapty SDK." --- ## Xử lý thanh toán \{#accept-purchases\} Nếu bạn đang làm việc với các paywall tự triển khai, bạn có thể ủy quyền xử lý thanh toán cho Adapty thông qua phương thức `makePurchase`. Bằng cách này, chúng tôi sẽ xử lý tất cả các kịch bản người dùng, và bạn chỉ cần xử lý kết quả thanh toán. :::important `makePurchase` hoạt động với các sản phẩm được tạo trong Adapty Dashboard. Hãy đảm bảo bạn đã cấu hình sản phẩm và cách truy xuất chúng trong dashboard bằng cách làm theo [hướng dẫn nhanh](quickstart). ::: <CustomDocCardList ids={['kmp-quickstart-manual', 'fetch-paywalls-and-products-kmp', 'present-remote-config-paywalls-kmp', 'kmp-making-purchases', 'kmp-restore-purchase', 'kmp-troubleshoot-purchases']} /> ## Chế độ quan sát \{#observer-mode\} Nếu bạn muốn tự triển khai logic xử lý thanh toán từ đầu nhưng vẫn muốn tận dụng tính năng phân tích nâng cao của Adapty, bạn có thể sử dụng chế độ quan sát. :::important Xem xét các giới hạn của chế độ quan sát [tại đây](observer-vs-full-mode). ::: <CustomDocCardList ids={['implement-observer-mode-kmp', 'report-transactions-observer-mode-kmp', 'kmp-troubleshoot-purchases']} /> --- # File: kmp-quickstart-manual --- --- title: "Bật tính năng mua hàng trong paywall tùy chỉnh với Kotlin Multiplatform SDK" description: "Tích hợp Adapty SDK vào các paywall tùy chỉnh Kotlin Multiplatform của bạn để bật tính năng in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Bạn hoàn toà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ý các giao dịch mua mới và khôi phục các giao dịch trước đó. Hướng dẫn này sử dụng các API của Adapty Kotlin Multiplatform SDK v4 (beta) — nếu bạn đang dùng v3, hãy xem [hướng dẫn migration](migration-to-kmp-sdk-v4) để biết tên các phương thức tương ứng. :::important **Hướng dẫn này dành cho các developer đ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 Flow Builder](kmp-quickstart-paywalls). Với Flow Builder, bạn tạo các flow trong trình chỉnh sửa trực quan không cần code, Adapty xử lý toàn bộ logic mua hàng tự động, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để kích hoạt in-app purchase, bạn cần nắm rõ 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ấu hình xác định những sản phẩm nào sẽ được hiển thị. 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 chỉnh sửa sản phẩm, giá cả và ưu đãi mà không cần thay đổi code ứng dụng. Trong SDK v4, các biến thể paywall cho một placement được mang bởi đối tượng **flow** — bạn lấy một flow và truy vấn các sản phẩm của nó. - [**Placements**](placements) – nơi và thời điểm hiển thị paywall trong ứng dụng của bạn (ví dụ như `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trong dashboard, sau đó yêu cầu chúng theo placement ID trong code. Điều này giúp bạn dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho các người dùng khác nhau. 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 của mình. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy danh sách sản phẩm. Để hiểu những gì cần thực hiện trên dashboard, hãy làm theo hướng dẫn quickstart [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc có hoặc không có xác thực backend ở phía mình. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo cách khác nhau. Đọc [hướng dẫn nhanh về xác định người dùng](kmp-quickstart-identify) để hiểu rõ sự khác biệt và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy sản phẩm \{#step-1-get-products\} Để lấy danh sách sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền ID [placement](placements) vào phương thức `getFlow`. 2. Lấy mảng sản phẩm cho flow này bằng phương thức `getPaywallProducts`. ```kotlin showLineNumbers fun loadPaywall() { Adapty.getFlow(placementId = "YOUR_PLACEMENT_ID") .onSuccess { flow -> Adapty.getPaywallProducts(flow = flow) .onSuccess { products -> // Use products to build your custom paywall UI } .onError { error -> // Handle the error } } .onError { error -> // Handle the error } } ``` ## Bước 2. Xử lý thanh toán \{#step-2-accept-purchases\} Khi người dùng chọn một sản phẩm trên 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ý flow thanh toán và trả về hồ sơ người dùng đã được cập nhật. ```kotlin showLineNumbers fun purchaseProduct(product: AdaptyPaywallProduct) { Adapty.makePurchase(product = product) .onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // Purchase successful, profile updated } is AdaptyPurchaseResult.UserCanceled -> { // User canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Purchase is pending (e.g., user will pay offline with cash) } } } .onError { error -> // 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 có thể khôi phục giao dịch mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Thao tác này sẽ đồng bộ lịch sử giao dịch mua của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```kotlin showLineNumbers fun restorePurchases() { Adapty.restorePurchases() .onSuccess { profile -> // Restore successful, profile updated } .onError { error -> // Handle the error } } ``` ## Bước 4. Kiểm tra trạng thái gói đăng ký \{#step-4-check-the-subscription-status\} Sau khi mua hàng hoặc khôi phục, hãy kiểm tra [mức độ truy cập](access-level) của người dùng để quyết định có hiển thị paywall hay mở khóa các tính năng trả phí. Các phương thức `makePurchase` và `restorePurchases` đã trả về hồ sơ người dùng đã cập nhật; khi bạn cần trạng thái hiện tại ở bất kỳ nơi nào khác trong ứng dụng, hãy dùng phương thức `getProfile`: ```kotlin showLineNumbers fun checkPremiumAccess() { Adapty.getProfile() .onSuccess { profile -> val hasPremiumAccess = profile.accessLevels["premium"]?.isActive == true // Grant access to paid features if hasPremiumAccess is true } .onError { error -> // Handle the error } } ``` Để biết thêm các cách kiểm tra và theo dõi trạng thái gói đăng ký, bao gồm lắng nghe cập nhật theo thời gian thực, xem [Kiểm tra trạng thái gói đăng ký](kmp-check-subscription-status). ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra các giao dịch mua 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 giao dịch mua thử từ paywall. Để xem cách hoạt động trong một triển khai thực tế, hãy tham khảo [AppViewModel.kt](https://github.com/adaptyteam/AdaptySDK-KMP/blob/main/example/composeMultiplatformApp/composeApp/src/commonMain/kotlin/com/adapty/exampleapp/AppViewModel.kt) trong ứng dụng mẫu của chúng tôi, minh họa cách xử lý giao dịch mua với error handling và quản lý trạng thái phù hợp. --- # File: fetch-paywalls-and-products-kmp --- --- title: "Lấy paywall và sản phẩm cho paywall remote config trong Kotlin Multiplatform SDK" description: "Lấy paywall và sản phẩm trong Adapty Kotlin Multiplatform SDK để tăng cường monetization người dùng." --- <SDKv4> Trước khi trình bày Remote Config và các paywall tùy chỉnh, bạn cần tải thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và các paywall tùy chỉnh. Để được hướng dẫn về cách tải các flow hoặc paywall được tùy chỉnh trong **Flow Builder** hoặc **Paywall Builder**, vui lòng tham khảo [Lấy flow & paywall](kmp-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. ::: <details> <summary>Trước khi bắt đầu tải flow và sản phẩm trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow hoặc paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow hoặc paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-kotlin-multiplatform) trong ứng dụng mobile của bạn. </details> ## Lấy thông tin flow \{#fetch-flow-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các flow và paywall, cho phép bạn hiển thị chúng tại các placement cụ thể trong ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một `AdaptyFlow` từ một trong các [placement](placements) của mình bằng phương thức `getFlow`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Các flow đượ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 động—nếu hôm nay một flow trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả chúng mà không cần thay đổi code. ::: ```kotlin showLineNumbers Adapty.getFlow( placementId = "YOUR_PLACEMENT_ID", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { flow -> // the requested flow }.onError { error -> // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp sự cố kết nối internet không ổn định, hãy cân nhắc sử dụng `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet có chập chờn đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>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 cài đặt lại ứng dụng hoặc xóa thủ công.</p><p></p><p>Adapty SDK lưu trữ flow và paywall ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](kmp-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải flow và paywall nhanh hơn cùng với một server dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của flow và paywall, đồng thời duy trì độ tin cậy ngay cả khi kết nối internet kém.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ trễ hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác này có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | Đừng hardcode ID sản phẩm! Vì flow được cấu hình từ xa, các sản phẩm hiện có, số lượng sản phẩm và các ưu đãi đặc biệt (chẳng hạn như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được 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 của bạn 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 của bạn 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ố phản hồi: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` bao gồm: mã định danh flow, các biến thể paywall (`paywalls` — mỗi biến thể có mã định danh sản phẩm riêng), danh sách `remoteConfigs` (một mục cho mỗi ngôn ngữ đã cấu hình), và một số thuộc tính khác. Để lấy sản phẩm cho flow, hãy gọi `getPaywallProducts(flow)`. | :::note Trong v4, `getFlow` không có tham số `locale`. Khi bạn render một flow bằng `createFlowView`, ngôn ngữ sẽ được xác định tự động. Đối với các paywall tùy chỉnh, tất cả các ngôn ngữ có sẵn đều được trả về cùng nhau trong `flow.remoteConfigs` — hãy chọn ngôn ngữ phù hợp với thiết bị của người dùng hoặc cài đặt ứng dụng của bạn. Xem [Localizations và mã ngôn ngữ](kmp-localizations-and-locale-codes) để biết thêm chi tiết. ::: ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có flow, bạn có thể truy vấn mảng sản phẩm tương ứng với flow đó: ```kotlin showLineNumbers Adapty.getPaywallProducts(flow).onSuccess { products -> // the requested products }.onError { error -> // handle the error } ``` Tham số phản hồi: | Tham số | Mô tả | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) với: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký, và một số thuộc tính khác. | Khi triển khai thiết kế flow của riêng bạn, bạn có thể sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/). 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 chi tiết đầy đủ 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, sử dụng `product.localizedTitle`. Lưu ý rằng việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, chứ không phải ngôn ngữ của thiết bị. | | **Price** | Để hiển thị giá đã được bản địa hóa, sử dụng `product.price.localizedString`. Việc bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price.amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, sử dụng `product.price.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), sử dụng `product.subscriptionDetails?.localizedSubscriptionPeriod`. Việc bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo cách lập trình, sử dụng `product.subscriptionDetails?.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là DAY, WEEK, MONTH, YEAR, hoặc UNKNOWN). Giá trị `numberOfUnits` sẽ cho bạn biết số đơn vị chu kỳ. Ví dụ: với gói đăng ký theo quý, bạn sẽ thấy `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 cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionDetails?.introductoryOfferPhases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `paymentMode`: enum với các giá trị `FREE_TRIAL`, `PAY_AS_YOU_GO`, `PAY_UPFRONT` và `UNKNOWN`. Các gói dùng thử miễn phí sẽ có loại `FREE_TRIAL`.<br/>• `price`: Giá ưu đãi dưới dạng số. Với gói dùng thử miễn phí, giá trị này là `0`.<br/>• `localizedNumberOfPeriods`: Chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ: ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết riêng lẻ của chu kỳ ưu đãi bằng thuộc tính này. Nó hoạt động tương tự như mô tả ở phần trước đối với ưu đãi.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký được định dạng theo ngôn ngữ của người dùng. | ## Tăng tốc tải flow với flow mặc định cho đối tượng mặc định \{#speed-up-flow-fetching-with-default-audience-flow\} Thông thường, flow được tải gần như tức thì nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống đó, bạn có thể muốn hiển thị một flow mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow bằng phương thức `getFlow`, như đã mô tả chi tiết trong phần [Lấy thông tin flow](fetch-paywalls-and-products-kmp#fetch-flow-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị dùng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (legacy) hoặc chấp nhận rằng người dùng có phiên bản hiện tại (legacy) có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, có nghĩa là bạn mất khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã được mô tả [ở trên](fetch-paywalls-and-products-kmp#fetch-flow-information). ::: ```kotlin showLineNumbers Adapty.getFlowForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", fetchPolicy = AdaptyPaywallFetchPolicy.Default ).onSuccess { flow -> // the requested flow }.onError { error -> // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>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ị lựa chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn, bất kể kết nối internet của họ có chập chờn đến đâu. Cache được cập nhật thường xuyên, vì vậy hoàn toàn an toàn khi sử dụng trong suốt phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>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 thông qua việc dọn dẹp thủ công.</p> | </SDKv4> <SDKv3> Trước khi hiển thị Remote Config và paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và paywall tùy chỉnh. Để biết hướng dẫn lấy paywall cho các paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo [Lấy paywall Paywall Builder và cấu hình của chúng](kmp-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. ::: <details> <summary>Trước khi bắt đầu lấy paywall và sản phẩm trong ứng dụng của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall của bạn](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-kotlin-multiplatform) vào ứng dụng di động của bạn. </details> ## 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ừ App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của mình bằng phương thức `getPaywall`. :::important **Đừng hardcode product ID.** ID duy nhất bạn cần hardcode là placement ID. Các paywall đượ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 cứ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này phải là mã ngôn ngữ gồm một hoặc nhiều thẻ con phân cách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p> | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong bộ nhớ đệm nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp kết nối internet không ổn định, hãy cân nhắc dùng `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` để trả về dữ liệu đã lưu trong bộ nhớ đệm nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn dù kết nối internet kém. Bộ nhớ đệm được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng bộ nhớ đệm vẫn còn nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall theo hai lớp: bộ nhớ đệm được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](kmp-use-fallback-paywalls). Chúng tôi cũng dùng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không truy cập được. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã lưu trong bộ nhớ đệm hoặc fallback cục bộ sẽ được trả về.</p><p></p><p>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.</p> | Đừng hardcode product ID! Vì paywall được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những tình huống này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, app cần hiển thị đúng 2 sản phẩm đó. Nhưng nếu sau này lấy được 3 sản phẩm, app phải 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 | Đối tượng [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) bao gồm: danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy 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ó: ```kotlin showLineNumbers Adapty.getPaywallProducts(paywall).onSuccess { products -> // the requested products }.onError { error -> // handle the error } ``` Tham số phản hồi: | Parameter | Description | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký, và một số thuộc tính khác. | Khi tự thiết kế paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/). Dưới đây là các thuộc tính được sử dụng phổ biến nhất — tham khảo tài liệu được liên kết để xem đầy đủ chi tiết 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, sử dụng `product.localizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, chứ không phải ngôn ngữ của thiết bị. | | **Price** | Để hiển thị giá đã được bản địa hóa, sử dụng `product.price.localizedString`. Bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price.amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, sử dụng `product.price.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), sử dụng `product.subscriptionDetails?.localizedSubscriptionPeriod`. Bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo cách lập trình, sử dụng `product.subscriptionDetails?.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là DAY, WEEK, MONTH, YEAR hoặc UNKNOWN). Giá trị `numberOfUnits` sẽ cho bạn biết số đơn vị chu kỳ. Ví dụ: với gói đăng ký theo quý, bạn sẽ thấy `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 cho thấy gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionDetails?.introductoryOfferPhases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `paymentMode`: một enum với các giá trị `FREE_TRIAL`, `PAY_AS_YOU_GO`, `PAY_UPFRONT` và `UNKNOWN`. Giai đoạn dùng thử miễn phí sẽ có kiểu `FREE_TRIAL`.<br/>• `price`: Giá giảm dưới dạng số. Với giai đoạn dùng thử miễn phí, giá trị này là `0`.<br/>• `localizedNumberOfPeriods`: một chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ: ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết riêng lẻ của chu kỳ ưu đãi bằng thuộc tính này. Cách hoạt động tương tự như phần trước đã mô tả đối với chu kỳ gói đăng ký thông thường.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký được định dạng theo ngôn ngữ của người dùng cho phần giảm giá. | ## Tăng tốc độ tải paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywall được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng của bạn có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ 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ư đã trình bày chi tiết trong phần [Lấy thông tin Paywall](fetch-paywalls-and-products-kmp#fetch-paywall-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng có phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa (bao gồm nhắm mục tiêu theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` như đã mô tả [ở trên](fetch-paywalls-and-products-kmp#fetch-paywall-information). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều subtag được phân tách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là mã ngôn ngữ, subtag thứ hai là mã vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p> | | **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong bộ nhớ đệm nếu thất bại. Chúng tôi khuyến nghị phương án này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` để trả về dữ liệu đã lưu trong bộ nhớ đệm nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể chất lượng kết nối internet như thế nào. Bộ nhớ đệm được cập nhật thường xuyên, vì vậy hoàn toàn an toàn khi sử dụng trong suốt phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng bộ nhớ đệm vẫn còn nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.</p> | </SDKv3> --- # File: present-remote-config-paywalls-kmp --- --- title: "Hiển thị paywall được thiết kế bằng Remote Config trong Kotlin Multiplatform SDK" description: "Khám phá cách trình bày paywall Remote Config trong Adapty Kotlin Multiplatform SDK để cá nhân hóa trải nghiệm người dùng." --- <SDKv4> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code ứng dụng để người dùng có thể thấy nó. Vì Remote Config linh hoạt và hoàn toàn do bạn kiểm soát, bạn quyết định những gì có trong paywall và giao diện của nó trông như thế nào. Adapty cung cấp một phương thức để lấy cấu hình remote, giúp bạn tự do trình bày paywall tùy chỉnh của mình. ## Lấy remote config của flow và hiển thị nó \{#get-flow-remote-config-and-present-it\} Trong v4, một flow chứa một mục `AdaptyRemoteConfig` cho mỗi ngôn ngữ đã cấu hình trong danh sách `remoteConfigs`. Chọn ngôn ngữ phù hợp với tùy chọn của người dùng, sau đó đọc các giá trị bạn cần. ```kotlin showLineNumbers Adapty.getFlow("YOUR_PLACEMENT_ID") .onSuccess { flow -> val config = flow.remoteConfigs.firstOrNull { it.locale == "en" } ?: flow.remoteConfigs.firstOrNull() val headerText = config?.dataMap?.get("header_text") as? String // use the remote config values } .onError { error -> // handle the error } ``` Tại thời điểm này, khi bạn đã 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ông thật hấp dẫn. Hãy đảm bảo thiết kế phù hợp với nhiều kích thước màn hình và hướng hiển thị khác nhau của điện thoại di động, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy đảm bảo [ghi lại sự kiện xem paywall](present-remote-config-paywalls-kmp#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall, hãy 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ừ flow của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](kmp-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](kmp-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 flow và paywall. Trong khi chúng tôi tự động thu thập dữ liệu về các giao dịch mua, việc ghi lại lượt xem 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 flow. Để ghi lại sự kiện xem, chỉ cần gọi `.logShowFlow(flow)`, và nó sẽ được phản ánh trong các chỉ số của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowFlow(flow)` nếu bạn đang hiển thị các flow hoặc paywall được render bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder). Adapty tự động theo dõi lượt xem trong những trường hợp đó. ::: ```kotlin showLineNumbers Adapty.logShowFlow(flow) .onSuccess { // flow view logged successfully } .onError { error -> // handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :-------- | :------- |:-----------------------------------------------------------------| | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow`. | </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng có thể thấy nó. Vì Remote Config cho phép linh hoạt theo nhu cầu của bạn, bạn hoàn toàn kiểm soát nội dung hiển thị cũng như giao diện của paywall. Chúng tôi cung cấp một phương thức để lấy cấu hình remote, giúp bạn chủ động hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## Lấy Remote Config của paywall và hiển thị \{#get-paywall-remote-config-and-present-it\} Để lấy Remote Config của một paywall, hãy truy cập thuộc tính `remoteConfig` và lấy ra các giá trị cần thiết. ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> val headerText = paywall.remoteConfig?.dataMap?.get("header_text") as? String // use the remote config values }.onError { error -> // handle the error } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, bước tiếp theo là render và kết hợp chúng thành một trang có giao diện hấp dẫn. Hãy đảm bảo thiết kế phù hợp với nhiều loại màn hình và hướng hiển thị khác nhau trên thiết bị di động, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy đảm bảo [ghi lại sự kiện xem paywall](present-remote-config-paywalls-kmp#track-paywall-view-events-1) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy tiếp tục thiết lập luồng mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](kmp-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](kmp-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ự can thiệp của bạn vì chỉ bạn mới biết khi nào khách hàng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)`, và sự kiện này 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ị các paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```kotlin showLineNumbers Adapty.logShowPaywall(paywall = paywall) .onSuccess { // paywall view logged successfully } .onError { error -> // handle the error } ``` 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://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/). | </SDKv3> --- # File: kmp-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng với Kotlin Multiplatform SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng là bước quan trọng để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ hiển thị paywall thôi là đủ để hỗ trợ mua hàng nếu bạn dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall. Nếu không dùng Paywall Builder, bạn cần sử dụng một phương thức riêng là `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung. Đây là cầu nối giúp người dùng tương tác với paywall và tiến hành giao dịch mong muốn. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm người dùng muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng. :::warning Lưu ý rằng ưu đãi giới thiệu chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập qua Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh tính đủ đ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, đồng thời có thể tính giá đầy đủ cho những người dùng đủ điều kiện nhận ưu đãi giới thiệu. ::: Hãy đảm bảo bạn đã [hoàn thành cấu hình ban đầu](quickstart) mà không bỏ qua bước nào. Nếu thiếu, chúng tôi không thể xác thực giao dịch mua. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang dùng [Paywall Builder](adapty-paywall-builder)?** Giao dịch được xử lý tự động — bạn có thể bỏ qua bước này. **Muốn có hướng dẫn từng bước?** Xem [hướng dẫn quickstart](kmp-implement-paywalls-manually) để có hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```kotlin showLineNumbers Adapty.makePurchase(product = product).onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // Grant access to the paid features } } is AdaptyPurchaseResult.UserCanceled -> { // Handle the case where the user canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Handle deferred purchases (e.g., the user will pay offline with cash) } } }.onError { error -> // 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://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>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://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) cung cấp thông tin toàn diện về mức độ truy cập, gói đăng ký và các sản phẩm mua một lần của người dùng trong ứng dụng.</p><p>Kiểm tra trạng thái mức độ truy cập để xác định liệu người dùng có quyền truy cập cần thiết vào ứng dụng hay không.</p> | :::warning **Lưu ý:** nếu bạn vẫn đang dùng StoreKit của Apple phiên bản thấp hơn v2.0 và Adapty SDK phiên bản thấp hơn v2.9.0, bạn cần cung cấp [App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple ngừng hỗ trợ. ::: ## Thay đổi gói đăng ký khi mua hàng \{#change-subscription-when-making-a-purchase\} Khi người dùng chọn 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. Với Google Play, gói đăng ký không tự động được cập nhật. Bạn cần quản lý việc chuyển đổi trong code ứng dụng như mô tả dưới đây. Để thay thế gói đăng ký bằng gói khác trên Android, hãy gọi phương thức `.makePurchase()` với tham số bổ sung: ```kotlin showLineNumbers val subscriptionUpdateParams = AdaptyAndroidSubscriptionUpdateParameters( oldSubVendorProductId = "old_subscription_product_id", replacementMode = AdaptyAndroidSubscriptionUpdateReplacementMode.CHARGE_FULL_PRICE ) val purchaseParams = AdaptyPurchaseParameters.Builder() .setSubscriptionUpdateParams(subscriptionUpdateParams) .build() Adapty.makePurchase( product = product, parameters = purchaseParams ).onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // successful cross-grade } is AdaptyPurchaseResult.UserCanceled -> { // user canceled the purchase flow } is AdaptyPurchaseResult.Pending -> { // the purchase has not been finished yet, e.g. user will pay offline by cash } } }.onError { error -> // Handle the error } ``` Tham số yêu cầu bổ sung: | Tham số | Bắt buộc | Mô tả | |:---------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **parameters** | tùy chọn | Đối tượng [`AdaptyAndroidSubscriptionUpdateParameters`](https://kmp.adapty.io/////adapty/com.adapty.kmp.models/-adapty-android-subscription-update-parameters/) được truyền qua [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). | 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: - [Giới thiệu về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Khuyến nghị của 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ỉ dùng được khi nâng cấp gói đăng ký. Hạ cấp không được hỗ trợ. - Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Việc thay đổi gói đăng ký thực sự chỉ diễn ra khi kỳ thanh toán gói đăng ký hiện tại kết thúc. ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} <Details> <summary>Về offer code</summary> Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} :::warning Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. ::: Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác. </Details> Để hiển thị giao diện đổi mã trong ứng dụng: ```kotlin showLineNumbers Adapty.presentCodeRedemptionSheet() .onSuccess { // code redemption sheet presented successfully } .onError { error -> // handle the error } ``` :::danger Theo quan sát của chúng tôi, giao diện Offer Code Redemption trong một số ứng dụng có thể hoạt động không ổn định. Chúng tôi khuyên bạn nên chuyển hướng người dùng trực tiếp đến App Store. Để làm điều này, bạn cần mở URL theo định dạng sau: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ## 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 đang chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước. ```kotlin showLineNumbers Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withGoogleEnablePendingPrepaidPlans(true) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } ``` --- # File: kmp-restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng mobile với Kotlin Multiplatform SDK" description: "Tìm hiểu cách khôi phục giao dịch mua trong Adapty để đảm bảo trải nghiệm người dùng liền mạch." --- Khôi phục giao dịch mua là tính năng cho phép người dùng lấy lại quyền truy cập vào nội dung đã mua trước đó, chẳng hạn như các gói đăng ký hoặc in-app purchase, mà không bị tính phí lần nữa. Tính năng này đặc biệt hữu ích cho những người dùng đã gỡ cài đặt rồi cài lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập nội dung đã mua 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()`: ```kotlin showLineNumbers Adapty.restorePurchases().onSuccess { profile -> if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // successful access restore } }.onError { error -> // handle the error } ``` Tham số trả về: | Tham số | Mô tả | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>Một đối tượng [`AdaptyProfile`](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-profile/). Model này chứa thông tin về mức độ truy cập, các gói đăng ký và sản phẩm mua một lần.</p><p>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.</p> | :::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-kmp --- --- title: "Triển khai chế độ Observer trong Kotlin Multiplatform SDK" description: "Triển khai chế độ observer trong Adapty để theo dõi các sự kiện đăng ký của người dùng trong Kotlin Multiplatform 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ể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp analytics nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics. Nếu điều này phù hợp với nhu cầu của bạn, bạn chỉ cần: 1. Bật tính năng này khi cấu hình SDK bằng cách đặt tham số `observerMode` thành `true`. Làm theo hướng dẫn cài đặt cho [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform). 2. [Báo cáo giao dịch](report-transactions-observer-mode-kmp) từ hạ tầng mua hàng hiện có của bạn tới Adapty. :::tip Trong SDK v4, bạn cũng có thể hiển thị các flow và paywall do Adapty render trong Observer mode: khi người dùng nhấn nút mua hoặc khôi phục, SDK sẽ chuyển hành động đó cho code của bạn để bạn tự thực hiện việc mua hoặc khôi phục. Xem [Hiển thị flow trong Observer mode](kmp-present-flows-in-observer-mode). ::: ## Cài đặt chế độ Observer \{#observer-mode-setup\} Bật chế độ Observer nếu bạn tự xử lý các giao dịch mua và trạng thái gói đăng ký, đồng thời sử dụng Adapty để gửi sự kiện gói đăng ký và phân tích. :::important Khi chạy ở chế độ Observer, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều đó. ::: ```kotlin showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withObserverMode(true) // default false .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised in observer mode") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } ``` Các tham số: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean kiểm soát [chế độ Observer](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thêm một số bước cấu hình trong Observer mode. Đây là những việc bạn cần làm ngoài các bước trên: 1. Hiển thị paywall như bình thường đối với [remote config paywalls](present-remote-config-paywalls-kmp). 3. [Liên kết paywall](report-transactions-observer-mode-kmp) với các giao dịch mua. --- # File: report-transactions-observer-mode-kmp --- --- title: "Báo cáo giao dịch trong Observer Mode trong Kotlin Multiplatform SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trong Kotlin Multiplatform SDK." --- Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng được thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng 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 lỗi trong analytics. Sử dụng `reportTransaction` để báo cáo từng giao dịch một cách tường minh để Adapty nhận biết được. :::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. ```kotlin showLineNumbers Adapty.reportTransaction( transactionId = "your_transaction_id", variationId = paywall.variationId ).onSuccess { profile -> // Transaction reported successfully // profile contains updated user data }.onError { error -> // handle the error } ``` Các tham số: | Tham số | Bắt buộc | Mô tả | | --------------- | -------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | transactionId | bắt buộc | ID giao dịch từ giao dịch mua hàng trên cửa hàng ứng dụng của bạn. Đây thường là purchase token hoặc transaction identifier được cửa hàng trả về. | | variationId | tùy chọn | Chuỗi định danh 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://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/). | --- # File: kmp-troubleshoot-purchases --- --- title: "Khắc phục sự cố mua hàng trong Kotlin Multiplatform SDK" description: "Khắc phục sự cố mua hàng trong Kotlin Multiplatform 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 Kotlin Multiplatform 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 thành 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 bị kích hoạt nhiều lần do các vấn đề quản lý trạng thái UI 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 trong chế độ observer \{#adaptyelrorcantmakepayments-in-observer-mode\} **Vấn đề**: Bạn đang nhận được lỗi `AdaptyError.cantMakePayments` khi sử dụng `makePurchase` trong chế độ observer. **Nguyên nhân**: Trong chế độ observer, bạn cần tự xử lý giao dịch mua hàng 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` để thực hiện giao dịch, 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 hàng trong chế độ observer. Xem [Triển khai chế độ Observer](implement-observer-mode-kmp) để 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 đang 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 của 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 của 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 đang gặp vấn đề với `makePurchasesCompletionHandlers` không tìm thấy. **Nguyên nhân**: Vấn đề này thường liên quan đến các 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ề purchase completion handler liên quan đến sandbox. --- # File: kmp-user --- --- title: "Người dùng & quyền truy cập trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách làm việc với người dùng và mức độ truy cập trong ứng dụng Kotlin Multiplatform của bạn với Adapty SDK." --- Trang này tổng hợp tất cả các hướng dẫn về làm việc với người dùng và mức độ truy cập trong ứng dụng Kotlin Multiplatform của bạn. Chọn chủ đề bạn cần: - **[Xác định người dùng](kmp-identifying-users)** - Tìm hiểu cách xác định người dùng trong ứng dụng của bạn - **[Cập nhật dữ liệu người dùng](kmp-setting-user-attributes)** - Đặt thuộc tính người dùng và dữ liệu hồ sơ người dùng - **[Lắng nghe thay đổi trạng thái gói đăng ký](kmp-listen-subscription-changes)** - Theo dõi thay đổi gói đăng ký theo thời gian thực - **[Kids Mode](kids-mode-kmp)** - Triển khai Kids Mode cho ứng dụng của bạn --- # File: kmp-identifying-users --- --- title: "Xác định người dùng trong Kotlin Multiplatform SDK" description: "Xác định người dùng trong Adapty để cải thiện trải nghiệm gói đăng ký được cá nhân hóa." --- Adapty tạo một ID hồ sơ nội bộ cho mỗi người dùng. Tuy nhiên, nếu bạn có hệ thống xác thực riêng, bạn nên đặt Customer User ID của mình. Bạn có thể tìm người dùng theo Customer User ID trong phần [Profiles](profiles-crm) và sử dụng nó trong [server-side API](getting-started-with-server-side-api), thông tin này sẽ được gửi đến tất cả các tích hợp. ### Đặt customer user ID khi cấu hình \{#setting-customer-user-id-on-configuration\} Nếu bạn có user ID trong quá trình cấu hình, chỉ cần truyền nó vào tham số `customerUserId` của phương thức `.activate()`: ```kotlin showLineNumbers Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("YOUR_USER_ID") .build() ).onSuccess { // successful activation }.onError { error -> // 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ó sau bất kỳ lúc nào bằng phương thức `.identify()`. Trường hợp phổ biến nhất khi 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ừ trạng thái ẩn danh sang đã xác thực. ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID").onSuccess { // successful identify }.onError { error -> // handle the error } ``` Các 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 bất kỳ 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 nên gửi lại dữ liệu đó cho người dùng đã được xác định. Ngoài ra, hãy lưu ý rằng bạn nên yêu cầu 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 kỳ lúc nào bằng cách gọi phương thức `.logout()`: ```kotlin showLineNumbers Adapty.logout().onSuccess { // successful logout }.onError { error -> // handle the 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\} [`iosAppAccountToken`](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 bạn. StoreKit gắn token này với mọi giao dịch, để backend của bạn có thể khớp dữ liệu App Store với người dùng của bạn. Hãy sử dụng UUID ổn định được tạo cho mỗi người dùng và tái sử dụng nó cho cùng một tài khoản trên 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 App Store được liên kết chính xác. Bạn có thể đặt token theo hai cách – trong quá trình kích hoạt SDK hoặc khi xác định người dùng. :::important Bạn phải luôn truyền `iosAppAccountToken` cùng với `customerUserId`. Nếu chỉ truyền token, nó sẽ không được đưa vào giao dịch. ::: ```kotlin showLineNumbers // During configuration: Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId( id = "YOUR_USER_ID", iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN" ) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } // Or when identifying users Adapty.identify( customerUserId = "YOUR_USER_ID", iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN" ).onSuccess { // successful identify }.onError { error -> // handle the error } ``` ## Đặ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ất định 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 giao dịch mua trong khi vẫn giữ thông tin người dùng ở dạng ẩn danh, điều này đặc biệt quan trọng cho việc ngăn chặn 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 ID ẩn danh cho phép Google Play theo dõi giao dịch mua mà không tiết lộ định danh người dùng thực. :::important Bạn phải luôn truyền `androidObfuscatedAccountId` cùng với `customerUserId`. Nếu chỉ truyền obfuscated account ID, nó sẽ không được đưa vào giao dịch. ::: ```kotlin showLineNumbers // During configuration: Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId( id = "YOUR_USER_ID", androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID" ) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } // Or when identifying users Adapty.identify( customerUserId = "YOUR_USER_ID", androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID" ).onSuccess { // successful identify }.onError { error -> // handle the error } ``` ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: kmp-setting-user-attributes --- --- title: "Đặt thuộc tính người dùng trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách đặt 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ể đặt 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 của mình. 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 xem chúng trong CRM. ### Đặt thuộc tính người dùng \{#setting-user-attributes\} Để đặt thuộc tính người dùng, gọi phương thức `.updateProfile()`: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() .withEmail("email@email.com") .withPhoneNumber("+18888888888") .withFirstName("John") .withLastName("Appleseed") .withGender(AdaptyProfile.Gender.FEMALE) .withBirthday(AdaptyProfile.Date(1970, 1, 3)) Adapty.updateProfile(builder.build()) .onSuccess { // profile updated successfully } .onError { error -> // handle the error } ``` Lưu ý rằng các thuộc tính bạn đã đặt trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các key được phép \{#the-allowed-keys-list\} Các key `<Key>` được phép của `AdaptyProfileParameters.Builder` và các giá trị `<Value>` tương ứng được liệt kê bên dưới: | Key | Value | |---|-----| | <p>email</p><p>phoneNumber</p><p>firstName</p><p>lastName</p> | String | | gender | Enum, các giá trị được phép: `AdaptyProfile.Gender.FEMALE`, `AdaptyProfile.Gender.MALE`, `AdaptyProfile.Gender.OTHER` | | birthday | Date | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể đặt 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ữ, đó có thể là trình độ kiến thức của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi có mục tiêu, đồng thời 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. ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() builder.withCustomAttribute("key1", "value1") ``` Để xóa key hiện có, dùng phương thức `.withRemovedCustomAttribute()`: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() builder.withRemovedCustomAttribute("key2") ``` Đôi khi bạn cần biết những thuộc tính tùy chỉnh nào đã được cài đặt trước đó. Để làm điều này, hãy 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 mới nhất, 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, nên các thuộc tính trên server có thể đã thay đổi sau lần đồng bộ cuối. ::: ### 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 dài 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 bất kỳ ký tự nào sau đây: `_` `-` `.` - Giá trị có thể là chuỗi hoặc số thực (float) với tối đa 50 ký tự. --- # File: kmp-listen-subscription-changes --- --- title: "Kiểm tra trạng thái gói đăng ký trong Kotlin Multiplatform SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng trong ứng dụng Kotlin Multiplatform của bạn." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải chèn thủ công các 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 gói đăng ký, hãy thiết lập [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). ## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là các thuộc tính của đối tượng [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/). Chúng tôi khuyến nghị lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](android-identifying-users#setting-customer-user-id-on-configuration), rồi cập nhật nó mỗi khi có thay đổi. Bằng cách này, bạn có thể sử dụng đối tượng profile mà không cần phải yêu cầu lại nhiều lần. Để nhận thông báo khi hồ sơ người dùng được cập nhật, hãy lắng nghe các thay đổi profile như mô tả trong phần [Lắng nghe các cập nhật profile, bao gồm mức độ truy cập](android-listen-subscription-changes) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ máy chủ \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ máy chủ, sử dụng phương thức `.getProfile()`: ```kotlin showLineNumbers Adapty.getProfile().onSuccess { profile -> // check the access }.onError { error -> // handle the error } ``` Tham số phản hồi: | Tham số | Mô tả | | --------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Profile | <p>Đối tượng [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/). Thông thường, bạn chỉ cần kiểm tra trạng thái mức độ truy cập của profile để xác định xem người dùng có quyền truy cập premium vào ứng dụng hay không.</p><p></p><p>Phương thức `.getProfile` luôn cố gắng truy vấn API nên 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 lấy được thông tin từ máy chủ, dữ liệu từ bộ nhớ cache sẽ được trả về. Cần lưu ý rằng Adapty SDK cập nhật cache `AdaptyProfile` thường xuyên để đảm bảo thông tin luôn được cập nhật nhất có thể.</p> | Phương thức `.getProfile()` cung cấp cho bạn hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Ví dụ: nếu bạn có ứng dụng báo và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Nhưng trong hầu hết các trường hợp, bạn chỉ cần một mức độ truy cập — khi đó bạn có thể dùng mức độ truy cập mặc định "premium". Đây là ví dụ kiểm tra mức độ truy cập "premium" mặc định: ```kotlin showLineNumbers Adapty.getProfile().onSuccess { profile -> if (profile.accessLevels["premium"]?.isActive == true) { // grant access to premium features } }.onError { error -> // handle the error } ``` ### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận thông báo từ Adapty, bạn cần thực hiện một số cấu hình bổ sung: ```kotlin showLineNumbers Adapty.setOnProfileUpdatedListener { profile -> // handle any changes to subscription state } ``` Adapty cũng kích hoạt một sự kiện khi ứng dụng khởi động. Trong trường hợp này, trạng thái gói đăng ký được lưu trong cache sẽ được truyền vào. ### Bộ nhớ cache trạng thái gói đăng ký \{#subscription-status-cache\} Bộ nhớ 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 máy chủ không khả dụng, dữ liệu được lưu trong cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ. 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 máy chủ mỗi phút để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ người dùng. Nếu có bất kỳ thay đổi nào, chẳng hạn như giao dịch mới hoặc các cập nhật khác, chúng sẽ được đồng bộ vào dữ liệu cache để giữ cho cache luôn nhất quán với máy chủ. --- # File: kmp-deal-with-att --- --- title: "Xử lý ATT trong Kotlin Multiplatform SDK" description: "Bắt đầu với Adapty trên Kotlin Multiplatform để đơn giản hóa việc thiết lập và quản lý gói đăng ký." --- Nếu ứng dụng của bạn sử dụng framework AppTrackingTransparency và hiển thị yêu cầu xác thực 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. ```kotlin showLineNumbers val profileParameters = AdaptyProfileParameters.Builder() .withAttStatus(3) // 3 = ATTrackingManagerAuthorizationStatusAuthorized .build() Adapty.updateProfile(profileParameters) .onSuccess { // ATT status updated successfully } .onError { error -> // handle AdaptyError } ``` :::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 integration mà bạn đã cấu hình. ::: --- # File: kids-mode-kmp --- --- title: "Kids Mode trong Kotlin Multiplatform SDK" description: "Dễ dàng bật Kids Mode để tuân thủ chính sách Google. Không thu thập GAID hay dữ liệu quảng cáo trong Kotlin Multiplatform SDK." --- Nếu ứng dụng Kotlin Multiplatform của bạn dành cho trẻ em, bạn phải tuân theo các chính sách của [Google](https://support.google.com/googleplay/android-developer/answer/9893335). Nếu bạn đang sử dụng Adapty SDK, một vài bước đơn giản sẽ giúp bạn cấu hình để đáp ứng các chính sách này và vượt qua quá trình kiểm duyệt của cửa hàng. ## Yêu cầu là gì? \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách cẩn thận. User ID theo định dạng `<FirstName.LastName>` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc dùng email. Với Kids Mode, thực hành tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã hash hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ. ## Bật Kids Mode \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\} Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để thực hiện, hãy 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 \{#updates-in-your-mobile-app-code\} Để tuân thủ các chính sách, bạn cần tắt việc thu thập Android Advertising ID (AAID/GAID) và địa chỉ IP khi khởi tạo Adapty SDK: ```kotlin showLineNumbers override fun onCreate() { super.onCreate() val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") // highlight-start .withGoogleAdvertisingIdCollectionDisabled(true) // set to `true` .withIpAddressCollectionDisabled(true) // set to `true` // highlight-end .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised with privacy settings") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } } ``` --- # File: kmp-onboardings --- --- title: "Onboardings trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách làm việc với onboardings trong ứng dụng Kotlin Multiplatform của bạn với Adapty SDK." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](kmp-get-pb-paywalls) thay thế: không giống như onboardings chạy bên trong WebView, flows được render trực tiếp trên thiết bị — mang lại hiệu ứng chuyển động mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](kmp-get-pb-paywalls) và [Hiển thị flows & paywalls](kmp-present-paywalls) để bắt đầu. ::: <CustomDocCardList /> --- # File: kmp-get-onboardings --- --- title: "Lấy danh sách onboarding trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách truy xuất onboarding trong Adapty cho Kotlin Multiplatform." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong một bản phát hành tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](kmp-get-pb-paywalls) thay thế: không giống như onboardings chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](kmp-get-pb-paywalls) và [Hiển thị flows & paywalls](kmp-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) của mình bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Kotlin Multiplatform. Bước đầu tiên trong quy trình này là lấy onboarding được liên kết với placement và cấu hình view của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform) phiên bản 3.15.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). ## Tải onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng trình tạo no-code của chúng tôi, nó được lưu trữ dưới dạng một container chứa cấu hình mà ứng dụng cần tải về và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung hiển thị, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập từ biểu mẫu). 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 tính năng theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy tải cấu hình onboarding sớm để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy một onboarding, hãy sử dụng phương thức `getOnboarding`: ```kotlin showLineNumbers Adapty.getOnboarding( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | Định danh của bản địa hóa onboarding. Tham số này phải là mã ngôn ngữ gồm một hoặc hai thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên đại diện cho ngôn ngữ, thẻ con thứ hai đại diện cho vùng.<p>Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha (Brazil).</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu 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.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ có thời gian tải nhanh hơn bất kể kết nối mạng ra sao. Cache được cập nhật thường xuyên, nên việc sử dụng trong phiên làm việc để tránh các yêu cầu mạng là hoàn toàn an toàn.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ các onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và các onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập 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 các onboarding, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối mạng kém.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | ## Tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | |:----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-onboarding/) bao gồm: mã định danh và cấu hình onboarding, Remote Config, cùng một số thuộc tính khác. | ## Tăng tốc độ tải onboarding với onboarding dành cho đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và onboarding, mà người dùng lại có kết nối internet yếu, việc tải onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này tải onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là tải onboarding bằng phương thức `getOnboarding`, như đã mô tả chi tiết trong phần [Fetch Onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản app, đòi hỏi thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ 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ỏ khả năng nhắm mục tiêu dựa trên quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ tải nhanh hơn vượt trội so với những hạn chế này trong trường hợp của bạn, hãy sử dụng `getOnboardingForDefaultAudience` như hướng dẫn bên dưới. Ngược lại, hãy dùng `getOnboarding` như đã mô tả [ở trên](#fetch-onboarding). ::: ```kotlin showLineNumbers Adapty.getOnboardingForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | Mã định danh của bản dịch onboarding. Tham số này cần là mã ngôn ngữ gồm một hoặc hai thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là cho ngôn ngữ, thẻ con thứ hai là cho vùng.<br/>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil). | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>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 xảy ra lỗi. Chúng tôi khuyên dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng thường gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet có chập chờn đến đâu. Cache được cập nhật thường xuyên, vì vậy hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập phòng khi CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản onboarding mới nhất trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet khan hiếm.</p> | --- # File: kmp-present-onboardings --- --- title: "Hiển thị onboarding trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách hiển thị onboarding hiệu quả để tăng tỷ lệ chuyển đổi." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flow](kmp-get-pb-paywalls) thay thế: không giống như onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](kmp-get-pb-paywalls) và [Hiển thị flow & paywall](kmp-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh một onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng Kotlin Multiplatform để hiển thị cho người dùng. Onboarding như vậy đã chứa 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 Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform) phiên bản 3.16.1 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 Kotlin Multiplatform SDK cung cấp hai cách để hiển thị onboarding: - **Với Compose Multiplatform** - **Không dùng Compose Multiplatform** ## Với Compose Multiplatform \{#with-compose-multiplatform\} Để hiển thị một onboarding, sử dụng phương thức `view.present()` trên `view` đượ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 `view` mới. :::warning Việc tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi. ::: ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { AdaptyUI.createOnboardingView(onboarding = onboarding).onSuccess { view -> view.present() }.onError { error -> // 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 nhận giá trị `AdaptyUIIOSPresentationStyle.FULLSCREEN` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.PAGESHEET`. ```kotlin showLineNumbers viewModelScope.launch { val view = AdaptyUI.createOnboardingView(onboarding = onboarding).getOrNull() view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET) } ``` ### Tùy chỉnh cách mở liên kết trong onboarding \{#customize-how-links-open-in-onboardings\} Mặc định, các liên kết trong onboarding sẽ mở trong trình duyệt nội bộ của ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị trang web ngay bên trong ứng dụng, giúp họ xem nội dung 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, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `AdaptyWebPresentation.EXTERNAL_BROWSER`: ```kotlin showLineNumbers viewModelScope.launch { AdaptyUI.createOnboardingView( onboarding = onboarding, externalUrlsPresentation = AdaptyWebPresentation.EXTERNAL_BROWSER // default – IN_APP_BROWSER ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ## Không dùng Compose Multiplatform \{#without-compose-multiplatform\} :::note `createNativeOnboardingView` là một phần của module cốt lõi `io.adapty:adapty-kmp`. Nếu dự án của bạn không dùng Compose Multiplatform, bạn không cần dependency `io.adapty:adapty-kmp-ui`. ::: Để nhúng một onboarding mà không dùng Compose Multiplatform, hãy gọi `createNativeOnboardingView`. Hàm này trả về một `AdaptyNativeOnboardingView` mà bạn thêm vào layout của mình: <Tabs> <TabItem value="android" label="Android"> ```kotlin showLineNumbers title="Kotlin Multiplatform (Android)" val nativeView = AdaptyUI.createNativeOnboardingView( context = context, viewModelStoreOwner = activity, onboarding = onboarding, observer = myOnboardingObserver, ) // Embed in your Compose layout: AndroidView( factory = { nativeView.view }, modifier = Modifier.fillMaxSize() ) ``` </TabItem> <TabItem value="ios" label="iOS"> Vì các phương thức mặc định của interface KMP trở thành `@required` trong Swift, bạn không thể triển khai `AdaptyUIOnboardingsEventsObserver` trực tiếp từ Swift. Hãy khai báo một lớp cơ sở open trong `iosMain` trước: ```kotlin showLineNumbers title="iosMain (Kotlin)" open class BaseOnboardingObserver : AdaptyUIOnboardingsEventsObserver ``` Sau đó tạo subclass trong Swift, chỉ override những gì bạn cần: ```swift showLineNumbers title="Swift" class MyOnboardingObserver: BaseOnboardingObserver { override func onboardingViewOnCloseAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, actionId: String ) { // remove nativeView from your view hierarchy } } let nativeView = AdaptyUI.shared.createNativeOnboardingView( onboarding: onboarding, observer: MyOnboardingObserver() ) // nativeView.viewController is a UIViewController. // Add it to your SwiftUI view or UIKit hierarchy. ``` </TabItem> </Tabs> ### Hủy view \{#dispose-the-view\} Gọi `dispose()` khi xóa view khỏi layout. Thao tác này sẽ hủy đăng ký event listener và giải phóng các tài nguyên nội bộ. ```kotlin showLineNumbers title="Kotlin Multiplatform" nativeView.dispose() ``` --- # File: kmp-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong Kotlin Multiplatform SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong Kotlin Multiplatform bằng Adapty." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](kmp-get-pb-paywalls) thay thế: không giống như onboarding chạy bên trong WebView, flows được render trực tiếp trên thiết bị — mang lại hiệu ứng mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView. Xem [Lấy flows & paywalls](kmp-get-pb-paywalls) và [Hiển thị flows & paywalls](kmp-present-paywalls) để bắt đầu. ::: Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform) phiên bản 3.15.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể phản hồi. Tìm hiểu cách xử lý các sự kiện này bên dưới. ## Thiết lập observer sự kiện onboarding \{#set-up-the-onboarding-event-observer\} Để xử lý các sự kiện onboarding, bạn cần triển khai interface `AdaptyUIOnboardingsEventsObserver` và thiết lập nó với `AdaptyUI.setOnboardingsEventsObserver()`. Việc này nên được thực hiện sớm trong vòng đời ứng dụng, thường là trong activity chính hoặc khi khởi tạo ứng dụng. ```kotlin // In your app initialization AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` ## Hành động tùy chỉnh \{#custom-actions\} Trong builder, bạn có thể thêm hành động **custom** vào một nút và gán cho nó một ID. Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. <img src={require('./img/ios-events-1.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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 `onCustomAction` sẽ được kích hoạt với action ID từ builder. Bạn có thể tự tạo ID theo ý muốn, ví dụ như "allowNotifications". ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnCustomAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, actionId: String ) { when (actionId) { "openPaywall" -> { // Display paywall from onboarding // You would typically fetch and present a new paywall here mainUiScope.launch { // Example: Get paywall by placement ID // val paywallResult = Adapty.getPaywall("your_placement_id") // paywallResult.onSuccess { paywall -> // val paywallViewResult = AdaptyUI.createPaywallView(paywall) // paywallViewResult.onSuccess { paywallView -> // paywallView.present() // } // } } } "allowNotifications" -> { // Handle notification permissions } else -> { // Handle other custom actions } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ``` </Details> ## Đóng onboarding \{#closing-onboarding\} Onboarding được coi là đã đóng khi người dùng nhấn vào nút được gán hành động **Close**. Bạn cần quản lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ: :::important Bạn cần quản lý những gì xảy ra khi người dùng đóng onboarding. Chẳng hạn, bạn cần dừng hiển thị chính onboarding đó. ::: Nếu bạn đang sử dụng [`createNativeOnboardingView`](kmp-present-onboardings#without-compose-multiplatform), `view.isStandaloneView` là `false` — cài đặt mặc định sẽ không gọi `view.dismiss()`. Thay vào đó, hãy xóa view khỏi layout và gọi `dispose()` trên nó trong callback này. ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnCloseAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, actionId: String ) { // Dismiss the onboarding screen mainUiScope.launch { view.dismiss() } // Additional cleanup or navigation logic can be added here // For example, navigate back or show main app content } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ``` </Details> ## Mở một 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 nó được đóng lại, có một cách đơn giản hơn – xử lý [`onboardingViewOnCloseAction`](#closing-onboarding) và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall. Theo cách này, bạn có thể dùng placement ID để lấy và mở paywall ngay lập tức: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnPaywallAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, actionId: String ) { // Get the paywall using the placement ID from the action mainUiScope.launch { val paywallResult = Adapty.getPaywall(placementId = actionId) paywallResult.onSuccess { paywall -> val paywallViewResult = AdaptyUI.createPaywallView(paywall) paywallViewResult.onSuccess { paywallView -> paywallView.present() }.onError { error -> // handle the error } }.onError { error -> // handle the error } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` <Details> <summary>Ví dụ về sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ``` </Details> ## Hoàn tất tải onboarding \{#finishing-loading-onboarding\} Khi một onboarding hoàn tất việc tải, phương thức này sẽ được gọi: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewDidFinishLoading( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta ) { // Handle loading completion // You can add any initialization logic here } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ``` </Details> ## Sự kiện điều hướng \{#navigation-events\} Phương thức `onboardingViewOnAnalyticsEvent` được gọi khi các sự kiện analytics khác nhau xảy ra trong quá trình onboarding flow. Đối tượng `event` có thể là một trong các kiểu sau: |Kiểu | Mô tả | |------------|-------------| | `AdaptyOnboardingsAnalyticsEventOnboardingStarted` | Khi onboarding đã được tải | | `AdaptyOnboardingsAnalyticsEventScreenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `AdaptyOnboardingsAnalyticsEventScreenCompleted` | Khi một màn hình hoàn tất. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn tất) 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. | | `AdaptyOnboardingsAnalyticsEventSecondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `AdaptyOnboardingsAnalyticsEventUserEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `AdaptyOnboardingsAnalyticsEventOnboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy gán ID `final` cho màn hình cuối cùng. | | `AdaptyOnboardingsAnalyticsEventUnknown` | Dành cho bất kỳ kiểu sự kiện không được nhận dạng nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (siêu dữ liệu bổ sung) | Mỗi sự kiện đều 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 | Dưới đây là ví dụ về cách sử dụng các sự kiện analytics để theo dõi: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnAnalyticsEvent( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, event: AdaptyOnboardingsAnalyticsEvent ) { when (event) { is AdaptyOnboardingsAnalyticsEventOnboardingStarted -> { // Track onboarding start trackEvent("onboarding_started", event.meta) } is AdaptyOnboardingsAnalyticsEventScreenPresented -> { // Track screen presentation trackEvent("screen_presented", event.meta) } is AdaptyOnboardingsAnalyticsEventScreenCompleted -> { // Track screen completion with user response trackEvent("screen_completed", event.meta, event.elementId, event.reply) } is AdaptyOnboardingsAnalyticsEventOnboardingCompleted -> { // Track successful onboarding completion trackEvent("onboarding_completed", event.meta) } is AdaptyOnboardingsAnalyticsEventUnknown -> { // Handle unknown events trackEvent(event.name, event.meta) } // Handle other cases as needed } } private fun trackEvent(eventName: String, meta: AdaptyUIOnboardingMeta, elementId: String? = null, reply: String? = null) { // Implement your analytics tracking here // For example, send to your analytics service } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```javascript // OnboardingStarted { "meta": { "onboardingId": "onboarding_123", "screenClientId": "welcome_screen", "screenIndex": 0, "screensTotal": 4 } } // ScreenPresented { "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 4 } } // ScreenCompleted { "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 1, "screensTotal": 4 }, "elementId": "profile_form", "reply": "success" } // SecondScreenPresented { "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 1, "screensTotal": 4 } } // UserEmailCollected { "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 1, "screensTotal": 4 } } // OnboardingCompleted { "meta": { "onboardingId": "onboarding_123", "screenClientId": "final_screen", "screenIndex": 3, "screensTotal": 4 } } ``` </Details> --- # File: kmp-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong Kotlin Multiplatform SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng Kotlin Multiplatform của bạn với Adapty SDK." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong các phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](kmp-get-pb-paywalls) thay thế: khác với onboarding chạy trong WebView, flows render trực tiếp trên thiết bị — mang lại hiệu ứng animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](kmp-get-pb-paywalls) và [Hiển thị flows & paywalls](kmp-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời câu hỏi trong bài quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `onboardingViewOnStateUpdatedAction` 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ụ: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnStateUpdatedAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, elementId: String, params: AdaptyOnboardingsStateUpdatedParams ) { // Store user preferences or responses when (params) { is AdaptyOnboardingsSelectParams -> { // Handle single selection val id = params.id val value = params.value val label = params.label AppLogger.d("Selected option: $label (id: $id, value: $value)") } is AdaptyOnboardingsMultiSelectParams -> { // Handle multiple selections } is AdaptyOnboardingsInputParams -> { // Handle text input } is AdaptyOnboardingsDatePickerParams -> { // Handle date selection } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` <Details> <summary>Ví dụ dữ liệu đã lưu (định dạng có thể khác trong phần triển khai của bạn)</summary> ```javascript // Example of a saved select action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "preferences_screen", "screen_index": 1, "total_screens": 3 }, "action": { "element_id": "preference_selector", "element_type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 3 }, "action": { "element_id": "interests_selector", "element_type": "multi_select", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 0, "total_screens": 3 }, "action": { "element_id": "name_input", "element_type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 0, "total_screens": 3 }, "action": { "element_id": "birthday_picker", "element_type": "date_picker", "value": { "day": 15, "month": 6, "year": 1990 } } } ``` </Details> ## Các trường hợp sử dụng \{#use-cases\} ### Làm phong phú hồ sơ người dùng với dữ liệu \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ hai lần cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](kmp-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à bạn muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code của ứng dụng, nó có thể trông như thế này: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnStateUpdatedAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, elementId: String, params: AdaptyOnboardingsStateUpdatedParams ) { // Store user preferences or responses when (params) { is AdaptyOnboardingsInputParams -> { // Handle text input val builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field when (elementId) { "name" -> { when (val input = params.input) { is AdaptyOnboardingsTextInput -> { builder.withFirstName(input.value) } } } "email" -> { when (val input = params.input) { is AdaptyOnboardingsEmailInput -> { builder.withEmail(input.value) } } } } // Update profile asynchronously mainUiScope.launch { val profileParams = builder.build() val result = Adapty.updateProfile(profileParams) result.onSuccess { profile -> // Profile updated successfully AppLogger.d("Profile updated: ${profile.email}") }.onError { error -> // Handle the error AppLogger.e("Failed to update profile: ${error.message}") } } } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Bằng cách sử dụng các bài quiz 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 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 một bài quiz](onboarding-quizzes) trong trình tạo onboarding và gán ID có ý nghĩa cho các tùy chọn của nó. 2. Xử lý các phản hồi bài quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](kmp-setting-user-attributes) cho người dùng. ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnStateUpdatedAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, elementId: String, params: AdaptyOnboardingsStateUpdatedParams ) { // Handle quiz responses and set custom attributes when (params) { is AdaptyOnboardingsSelectParams -> { // Handle quiz selection val builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes when (elementId) { "experience" -> { // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.withCustomAttribute("experience", params.value) } } // Update profile asynchronously mainUiScope.launch { val profileParams = builder.build() val result = Adapty.updateProfile(profileParams) result.onSuccess { profile -> // Profile updated successfully AppLogger.d("Custom attribute 'experience' set to: ${params.value}") }.onError { error -> // Handle the error AppLogger.e("Failed to update profile: ${error.message}") } } } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` 3. [Tạo phân khúc](segments) cho mỗi 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](kmp-paywalls) cho placement trong code của ứng dụng. Nếu onboarding của bạn có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho hành động của nút này](kmp-handling-onboarding-events#opening-a-paywall). --- # File: kmp-best-practices --- --- title: "Thực hành tốt nhất với SDK Kotlin Multiplatform" description: "Các mẫu tham chiếu để tích hợp Adapty SDK trên Kotlin Multiplatform — thứ tự gọi, xử lý lỗi và các quy tắc đảm bảo sẵn sàng cho môi trường production." --- <CustomDocCardList /> --- # File: kmp-sdk-call-order --- --- title: "Thứ tự gọi trong Kotlin Multiplatform SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution, và lỗi kích hoạt ngắt quãng 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 Adapty SDK nào khác. Cho đến khi nó hoàn tất, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` đều sẽ thất bại với lỗi kích hoạt. Xem [Xử lý lỗi trong Kotlin Multiplatform SDK](kmp-handle-errors). Nếu ứng dụng của bạn xác thực người dùng và bạn thu thập customer user ID sau khi khởi chạy, hãy gọi `Adapty.identify()` ngay tại thời điểm đó. Đừng gọi các phương thức liên quan đến hành động người dùng cho đến khi `identify` hoàn tất. Các lệnh gọi chạy đồng thời với nó có thể trả về lỗi, hoặc sẽ tác động lên hồ sơ người dùng ẩn danh được tạo khi kích hoạt. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác thực. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo 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ẽ được 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. Để biết chi tiết về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự thực hiện đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai yếu tố: thời điểm bạn biết customer user ID, và liệu bạn có sử dụng MMP hoặc analytics SDK hay không. - **Bước 2 và 5**: Bắt buộc với mọi ứng dụng. Kích hoạt SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ cần thiết nếu bạn tích hợp MMP hoặc analytics SDK (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ cần thiết nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn đã có customer user ID ngay khi khởi động ứng dụng, hãy truyền nó vào `AdaptyConfig.Builder` trước khi gọi `activate()` (bước 2a). Cách này sẽ không bao giờ tạo hồ sơ người dùng ẩn danh, do đó bước 4 là không cần thiết. | Bước | Lệnh gọi | Khi nào | Ghi chú | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo MMP hoặc analytics SDK (AppsFlyer, Adjust, PostHog, Branch) | Khi khởi động ứng dụng, thực hiện đầu tiên | Chờ callback UID từ MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty.activate(configuration = AdaptyConfig.Builder("KEY").withCustomerUserId(...).build())` | Khi khởi động ứng dụng, sau bước 1, nếu bạn đã có customer user ID | Khuyến nghị. Sẽ không tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty.activate(configuration = AdaptyConfig.Builder("KEY").build())` không có `withCustomerUserId` | Khi khởi động ứng dụng, sau bước 1, nếu bạn chưa có customer user ID (hoặc không thu thập) | Adapty sẽ tạo hồ sơ người dùng ẩn danh. | | 3 | `Adapty.setIntegrationIdentifier("appsflyer_id", uid)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi nào liên quan đến người dùng | Bắt buộc để MMP ID được gắn vào đúng hồ sơ người dùng. | | 4 | `Adapty.identify("YOUR_USER_ID").onSuccess { ... }.onError { ... }` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ áp dụng cho luồng 2b có xác thực | Chờ `onSuccess` trước bất kỳ lệnh gọi nào liên quan đến người dùng. Các lệnh gọi đồng thời trong lúc `identify` có thể gắn vào hồ sơ người dùng ẩn danh. | | 5 | `getPaywall` (`getFlow` trong SDK v4), `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; ngược lại sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này sẽ khiến người dùng cũ mất quyền truy cập premium, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall trả về sai đối tượng. ::: ## Cài đặt 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 đầu tiên `activate()` trên thiết bị sẽ tạo một hồ sơ người dùng ẩn danh mới. Hồ sơ người dùng này không được liên kết với hồ sơ người dùng 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 `AdaptyConfig.Builder`. Nếu không, lần mua hàng trên web sẽ không hiển thị trên thiết bị cho đến khi bạn gọi `identify("YOUR_USER_ID")` và sau đó gọi `restorePurchases`. Để biết metadata cần gửi với mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: kmp-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong Kotlin Multiplatform SDK" description: "Tải paywall Adapty một cách đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho Kotlin Multiplatform." --- Một lần tải paywall đáng tin cậy trên Kotlin Multiplatform cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng mục tiêu, và xử lý dự phòng mượt mà khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó. :::tip Các quy tắc sau đây giả định `Adapty.activate()` và `Adapty.identify()` đã được thực thi xong. Xem [Thứ tự gọi trong Kotlin Multiplatform SDK](kmp-sdk-call-order). ::: Các hướng dẫn dưới đây sử dụng tên phương thức của v3. Trong SDK v4, `getPaywall` được đổi tên thành `getFlow` (xem [hướng dẫn migration](migration-to-kmp-sdk-v4)) — mọi quy tắc đều áp dụng không thay đổi. ## Quy tắc và lưu ý \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Fetch placement mà bạn sắp hiển thị. | Prefetch tất cả các placement đồng thời khi khởi chạy. | Bulk prefetch chặn main thread và gây ra màn hình đen trong suốt quá trình tải. | | Fetch `getPaywall` sau khi attribution có thời gian để resolve — ví dụ: 1–2 giây sau khi `activate` hoặc sau khi `setOnProfileUpdatedListener` được kích hoạt. | Gọi `getPaywall` khi khởi chạy ứng dụng. | Attribution chưa được xử lý xong. Paywall sẽ resolve 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 với kết nối kém sẽ thấy màn hình trắng cho đến khi mạng phục hồi — hoặc họ sẽ đóng ứng dụng. | Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-kmp) để tham khảo các tham số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn placement phù hợp. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối thường xuyên kém (vùng nông thôn, trên phương tiện giao thông, các khu vực 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 sử dụng paywall dự phòng khi hết thời gian chờ. - Đừng để hiển thị paywall phụ thuộc vào `Adapty.getProfile()`. Gọi `getPaywall` độc lập để một profile chậm không chặn giao diện người dùng. --- # File: kmp-test --- --- title: "Test & release in Kotlin Multiplatform SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Kotlin Multiplatform của bạn với Adapty." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng Kotlin Multiplatform, 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 không. Việc này bao gồm kiểm tra cả phần tích hợp SDK lẫn luồng mua hàng thực tế trong môi trường sandbox. ## 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 từng 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 máy chủ đã được cấu hình - Các giao dịch mua hoàn tất và được ghi nhận vào 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à kiểm duyệt đã được đáp ứng --- # File: kmp-reference --- --- title: "Tham chiếu cho Kotlin Multiplatform SDK" description: "Tài liệu tham chiếu cho Adapty Kotlin Multiplatform SDK." --- Trang này chứa tài liệu tham chiếu cho Adapty Kotlin Multiplatform SDK. Chọn chủ đề bạn cần: - **[Mô hình SDK](https://kmp.adapty.io/adapty/)** - Các mô hình dữ liệu và cấu trúc được SDK sử dụng - **[Xử lý lỗi](kmp-handle-errors)** - Xử lý lỗi và khắc phục sự cố --- # File: kmp-handle-errors --- --- title: "Xử lý lỗi trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách xử lý lỗi trong ứng dụng Kotlin Multiplatform của bạn với Adapty." --- Trang này hướng dẫn cách xử lý lỗi trong Adapty Kotlin Multiplatform SDK. ## Cơ bản về xử lý lỗi \{#error-handling-basics\} Tất cả các phương thức của Adapty SDK đều trả về kết quả có thể là thành công hoặc lỗi. Luôn xử lý cả hai trường hợp: <Tabs groupId="current-os" queryString> <TabItem value="kotlin" label="Kotlin" default> ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // Handle success } is AdaptyResult.Error -> { val error = result.error // Handle error Log.e("Adapty", "Error: ${error.message}") } } } ``` </TabItem> <TabItem value="java" label="Java" default> ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success<AdaptyProfile>) result).getValue(); // Handle success } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle error Log.e("Adapty", "Error: " + error.getMessage()); } }); ``` </TabItem> </Tabs> ## Các mã lỗi thường gặp \{#common-error-codes\} | Mã lỗi | Mô tả | Giải pháp | |------------|-------------|----------| | 1000 | Không tìm thấy ID sản phẩm | Kiểm tra cấu hình sản phẩm trong dashboard | | 1001 | Lỗi mạng | Kiểm tra kết nối internet | | 1002 | SDK key không hợp lệ | Xác minh lại SDK key của bạn | | 1003 | Không thể thực hiện thanh toán | Thiết bị không hỗ trợ thanh toán | | 1004 | Sản phẩm không khả dụng | Sản phẩm chưa được cấu hình trong cửa hàng | ## Xử lý các lỗi cụ thể \{#handle-specific-errors\} ### Lỗi mạng \{#network-errors\} <Tabs groupId="current-os" queryString> <TabItem value="kotlin" label="Kotlin" default> ```kotlin showLineNumbers Adapty.getPaywall("main") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // Use paywall } is AdaptyResult.Error -> { val error = result.error when (error.code) { 1001 -> { // Network error - show offline message showOfflineMessage() } else -> { // Other errors showErrorMessage(error.message) } } } } } ``` </TabItem> <TabItem value="java" label="Java" default> ```java showLineNumbers Adapty.getPaywall("main", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success<AdaptyPaywall>) result).getValue(); // Use paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); switch (error.getCode()) { case 1001: // Network error - show offline message showOfflineMessage(); break; default: // Other errors showErrorMessage(error.getMessage()); break; } } }); ``` </TabItem> </Tabs> ### Lỗi mua hàng \{#purchase-errors\} <Tabs groupId="current-os" queryString> <TabItem value="kotlin" label="Kotlin" default> ```kotlin showLineNumbers product.makePurchase { result -> when (result) { is AdaptyResult.Success -> { val purchase = result.value // Purchase successful showSuccessMessage() } is AdaptyResult.Error -> { val error = result.error when (error.code) { 1003 -> { // Can't make payments showPaymentNotAvailableMessage() } 1004 -> { // Product not available showProductNotAvailableMessage() } else -> { // Other purchase errors showPurchaseErrorMessage(error.message) } } } } } ``` </TabItem> <TabItem value="java" label="Java" default> ```java showLineNumbers product.makePurchase(result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchase purchase = ((AdaptyResult.Success<AdaptyPurchase>) result).getValue(); // Purchase successful showSuccessMessage(); } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); switch (error.getCode()) { case 1003: // Can't make payments showPaymentNotAvailableMessage(); break; case 1004: // Product not available showProductNotAvailableMessage(); break; default: // Other purchase errors showPurchaseErrorMessage(error.getMessage()); break; } } }); ``` </TabItem> </Tabs> ## Chiến lược khôi phục sau lỗi \{#error-recovery-strategies\} ### Thử lại khi gặp lỗi mạng \{#retry-on-network-errors\} <Tabs groupId="current-os" queryString> <TabItem value="kotlin" label="Kotlin" default> ```kotlin showLineNumbers fun getPaywallWithRetry(placementId: String, maxRetries: Int = 3) { var retryCount = 0 fun attemptGetPaywall() { Adapty.getPaywall(placementId) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // Use paywall } is AdaptyResult.Error -> { val error = result.error if (error.code == 1001 && retryCount < maxRetries) { // Network error - retry retryCount++ Handler(Looper.getMainLooper()).postDelayed({ attemptGetPaywall() }, 1000 * retryCount) // Exponential backoff } else { // Max retries reached or other error showErrorMessage(error.message) } } } } } attemptGetPaywall() } ``` </TabItem> <TabItem value="java" label="Java" default> ```java showLineNumbers public void getPaywallWithRetry(String placementId, int maxRetries) { AtomicInteger retryCount = new AtomicInteger(0); Runnable attemptGetPaywall = new Runnable() { @Override public void run() { Adapty.getPaywall(placementId, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success<AdaptyPaywall>) result).getValue(); // Use paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); if (error.getCode() == 1001 && retryCount.get() < maxRetries) { // Network error - retry retryCount.incrementAndGet(); new Handler(Looper.getMainLooper()).postDelayed(this, 1000 * retryCount.get()); } else { // Max retries reached or other error showErrorMessage(error.getMessage()); } } }); } }; attemptGetPaywall.run(); } ``` </TabItem> </Tabs> ### Dùng dữ liệu đã lưu cache khi có lỗi \{#fallback-to-cached-data\} <Tabs groupId="current-os" queryString> <TabItem value="kotlin" label="Kotlin" default> ```kotlin showLineNumbers class PaywallManager { private var cachedPaywall: AdaptyPaywall? = null fun getPaywall(placementId: String) { Adapty.getPaywall(placementId) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value cachedPaywall = paywall showPaywall(paywall) } is AdaptyResult.Error -> { val error = result.error if (error.code == 1001 && cachedPaywall != null) { // Network error - use cached paywall showPaywall(cachedPaywall!!) showOfflineIndicator() } else { // No cache available or other error showErrorMessage(error.message) } } } } } } ``` </TabItem> <TabItem value="java" label="Java" default> ```java showLineNumbers public class PaywallManager { private AdaptyPaywall cachedPaywall; public void getPaywall(String placementId) { Adapty.getPaywall(placementId, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success<AdaptyPaywall>) result).getValue(); cachedPaywall = paywall; showPaywall(paywall); } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); if (error.getCode() == 1001 && cachedPaywall != null) { // Network error - use cached paywall showPaywall(cachedPaywall); showOfflineIndicator(); } else { // No cache available or other error showErrorMessage(error.getMessage()); } } }); } } ``` </TabItem> </Tabs> ## Các bước tiếp theo \{#next-steps\} - [Sửa lỗi Code-1000 noProductIDsFound](InvalidProductIdentifiers-kmp) - [Sửa lỗi Code-1003 cantMakePayments](cantMakePayments-kmp) - [Tài liệu tham khảo API đầy đủ](https://android.adapty.io) - Tài liệu SDK hoàn chỉnh --- # File: InvalidProductIdentifiers-kmp --- --- title: "Sửa lỗi Code-1000 noProductIDsFound trong Kotlin Multiplatform SDK" description: "Khắc phục lỗi mã nhận dạng 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, dù chúng đã được liệt kê ở đó. Đôi khi lỗi này xuất hiện kèm theo 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 đang gặp lỗi `noProductIDsFound`, hãy làm theo các bước sau để khắc phục: ## Bước 1. Kiểm tra Bundle ID \{#step-2-check-bundle-id\} 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <Zoom> <img src="/docs/img/afd5012-bundle_id_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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**. <Zoom> <img src="/docs/img/2d64163-bundle_id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào tên nhóm gói đăng ký. Bạn sẽ thấy danh sách sản phẩm trong mục **Subscriptions**. 3. Đảm bảo sản phẩm bạn đang kiểm tra được đánh dấu là **Ready to Submit**. Nếu chưa, hãy làm theo hướng dẫn trên trang [Product in App Store](app-store-products). <img src="/assets/shared/img/ready-to-submit.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. So sánh ID sản phẩm trong bảng với ID trong tab [**Products**](https://app.adapty.io/products) trên Adapty Dashboard. Nếu các ID không khớp, hãy sao chép ID sản phẩm từ bảng và [tạo sản phẩm](create-product) với ID đó trong Adapty Dashboard. <img src="/assets/shared/img/product-id-copy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 3. Kiểm tra tình trạng khả dụng của sản phẩm \{#step-4-check-product-availability\} 1. Quay lại **App Store Connect** và mở mục **Subscriptions** tương tự. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào tên nhóm gói đăng ký để xem danh sách sản phẩm. 3. Chọn sản phẩm bạn đang kiểm tra. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn đến mục **Availability** và kiểm tra rằng tất cả các quốc gia và khu vực cần thiết đều được liệt kê. <img src="/assets/shared/img/product-availability.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 4. Kiểm tra giá sản phẩm \{#step-5-check-product-prices\} 1. Một lần nữa, truy cập mục **Monetization** → **Subscriptions** trong **App Store Connect**. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống **Subscription Pricing** và mở rộng mục **Current Pricing for New Subscribers**. <img src="/assets/shared/img/check-prices.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Đảm bảo tất cả các mức giá cần thiết đều được liệt kê. <img src="/assets/shared/img/product-pricing.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 5. Kiểm tra trạng thái thanh toán của ứng dụng, tài khoản ngân hàng và biểu mẫu thuế \{#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**. <img src="/assets/shared/img/business.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Chọn tên công ty của bạn. <img src="/assets/shared/img/business-name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Cuộn xuống và kiểm tra rằng **Paid Apps Agreement**, **Bank Account** và **Tax forms** của bạn đều hiển thị trạng thái **Active**. <img src="/assets/shared/img/appstore-connect-status.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bằng cách làm theo các bước trên, bạn sẽ có thể khắc phục cảnh báo `InvalidProductIdentifiers` và đưa sản phẩm 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ề lỗi `1000 noProductIDsFound`. Trong trường hợp đó, sản phẩm có thể đang bị kẹt trong registry của Apple. Registry sản phẩm của Apple đôi khi rơi vào trạng thái mà sản phẩm tồn tại trong giao diện App Store Connect nhưng không xuất hiện trong đườ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 product ID. Sau khi tạo lại, hãy đợi tối đa 24 giờ để dữ liệu được đồng bộ. --- # File: cantMakePayments-kmp --- --- title: "Cách sửa lỗi Code-1003 cantMakePayment trong Kotlin Multiplatform SDK" description: "Giải quyết lỗi không thể thực hiện 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: kmp-sdk-migration-guides --- --- title: "Hướng dẫn Migration Kotlin Multiplatform SDK" description: "Hướng dẫn migration cho các phiên bản Adapty Kotlin Multiplatform SDK." --- Trang này chứa tất cả các hướng dẫn migration cho Adapty Kotlin Multiplatform SDK. Chọn phiên bản bạn muốn migrate để xem hướng dẫn chi tiết: - **[Migrate lên v4.0 (beta)](migration-to-kmp-sdk-v4)** - **[Migrate lên v3.15](migration-to-kmp-315)** --- # File: migration-to-kmp-sdk-v4 --- --- title: "Migrate Adapty Kotlin Multiplatform SDK sang v. 4.0" description: "Migrate sang Adapty Kotlin Multiplatform SDK v4.0 (beta) bằng cách thay thế các API paywall bằng API flow, tương thích với cả Flow Builder và Paywall Builder." --- Adapty Kotlin Multiplatform SDK 4.0 (beta) giới thiệu các flow và đổi tên các API paywall tương ứng. Các API mới hoạt động với cả Flow Builder mới lẫn Paywall Builder hiện có — không cần thay đổi cấu hình nào trên Adapty Dashboard. ## Tài liệu tham khảo nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `Adapty.getPaywall(placementId, locale)` | `Adapty.getFlow(placementId)` | | `Adapty.getPaywallForDefaultAudience(placementId, locale)` | `Adapty.getFlowForDefaultAudience(placementId)` | | `Adapty.getPaywallProducts(paywall)` | `Adapty.getPaywallProducts(flow)` | | `Adapty.logShowPaywall(paywall)` | `Adapty.logShowFlow(flow)` | | `AdaptyPaywall` | `AdaptyFlow` | | `AdaptyUI.createPaywallView(paywall, ...)` | `AdaptyUI.createFlowView(flow, ...)` | | `AdaptyUI.createNativePaywallView(...)` → `AdaptyNativePaywallView` | `AdaptyUI.createNativeFlowView(...)` → `AdaptyNativeFlowView` | | `AdaptyUIPaywallView` | `AdaptyUIFlowView` | | `AdaptyUI.presentPaywallView(view)` / `dismissPaywallView(view)` | `AdaptyUI.presentFlowView(view)` / `dismissFlowView(view)` | | `AdaptyUI.setPaywallsEventsObserver(observer)` | `AdaptyUI.setFlowsEventsObserver(observer)` | | `AdaptyUI.registerPaywallEventsListener` / `unregisterPaywallEventsListener` | `AdaptyUI.registerFlowEventsListener` / `unregisterFlowEventsListener` | | `AdaptyUIPaywallsEventsObserver` | `AdaptyUIFlowsEventsObserver` | | `AdaptyUIPaywallPlatformView(paywall, ...)` | `AdaptyUIFlowPlatformView(flow, ...)` | | `paywallViewDidPerformAction`, `paywallViewDidAppear`, và các callback `paywallView...` khác | `flowViewDidPerformAction`, `flowViewDidAppear`, và các callback `flowView...` khác | | `paywallViewDidFailRendering` | `flowViewDidReceiveError` | `AdaptyPaywallProduct` giữ nguyên tên — sản phẩm vẫn thuộc về một flow, và `getPaywallProducts` cũng giữ nguyên tên, giờ nhận vào một `AdaptyFlow`. Các phương thức `getFlow` và `getFlowForDefaultAudience` không còn nhận tham số `locale` nữa. Các API mua hàng và hồ sơ người dùng (`makePurchase`, `restorePurchases`, `getProfile`, `identify`, `updateProfile`) cùng với fallback qua `setFallback` vẫn không thay đổi. Các phương thức onboarding vẫn hoạt động nhưng đã bị deprecated — xem [Deprecation của Onboarding API](#onboarding-api-deprecation). Một số hành vi mặc định đã thay đổi — xem [Thay đổi hành vi mặc định](#default-behavior-changes). ## Cài đặt \{#installation\} v4.0 là bản pre-release, vì vậy hãy ghim đúng phiên bản — Gradle không tự chọn các phiên bản pre-release thông qua dynamic ranges: ```toml showLineNumbers title="libs.versions.toml" [versions] adapty-kmp = "4.0.0-beta.1" [libraries] adapty-kmp = { module = "io.adapty:adapty-kmp", version.ref = "adapty-kmp" } adapty-kmp-ui = { module = "io.adapty:adapty-kmp-ui", version.ref = "adapty-kmp" } ``` Module `adapty-kmp-ui` chỉ cần thiết nếu bạn render flow và paywall bằng layer Compose Multiplatform (`view.present()`). Xem [Cài đặt Adapty SDK](sdk-installation-kotlin-multiplatform) để biết hướng dẫn thiết lập đầy đủ. Các native Adapty SDK bên dưới đã được nâng lên phiên bản 4.x trên cả hai nền tảng và được resolve tự động — không cần thay đổi cấu hình build. iOS deployment target vẫn là **15.0**, không thay đổi trong bản phát hành này. ## Tải flows \{#fetching-flows\} ### getPaywall → getFlow Kiểu trả về thay đổi từ `AdaptyPaywall` thành `AdaptyFlow`, và tham số `locale` bị loại bỏ — khi bạn render một flow, locale sẽ được tự động xác định; đối với paywall tùy chỉnh, tất cả các locale được trả về trong `flow.remoteConfigs`: ```diff showLineNumbers - Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en") - .onSuccess { paywall -> - // use the paywall + Adapty.getFlow("YOUR_PLACEMENT_ID") + .onSuccess { flow -> + // use the flow } .onError { error -> // handle the error } ``` `getPaywallForDefaultAudience` được đổi tên tương tự: ```diff showLineNumbers - Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", locale = "en") + Adapty.getFlowForDefaultAudience("YOUR_PLACEMENT_ID") ``` ### getPaywallProducts(paywall) → getPaywallProducts(flow) `getPaywallProducts` giữ nguyên tên nhưng bây giờ nhận vào một `AdaptyFlow`: ```diff showLineNumbers - Adapty.getPaywallProducts(paywall) + Adapty.getPaywallProducts(flow) .onSuccess { products -> // use the products } ``` ## Mô hình dữ liệu \{#data-model\} `getFlow` trả về một `AdaptyFlow` thay vì `AdaptyPaywall`, và cấu trúc object đã thay đổi: | Thuộc tính `AdaptyPaywall` v3 | Thuộc tính `AdaptyFlow` v4 | Hành động | |---|---|---| | `remoteConfig: AdaptyRemoteConfig?` (đơn lẻ) | `remoteConfigs: List<AdaptyRemoteConfig>` | Một flow mang một Remote Config cho mỗi ngôn ngữ được cấu hình. Đọc cái phù hợp với người dùng: `flow.remoteConfigs.firstOrNull { it.locale == "en" }`. | | _(mới)_ | `paywalls: List<AdaptyFlowPaywall>` | Mỗi mục là một biến thể paywall trong flow, với `name`, `variationId`, và `productIdentifiers` riêng. Các phương thức web paywall nhận một `AdaptyFlowPaywall` — xem [Phương thức web paywall](#web-paywall-methods). | | `productIdentifiers` | đã chuyển | Định danh sản phẩm giờ nằm trên mỗi biến thể: `flow.paywalls[i].productIdentifiers`. Để lấy sản phẩm, tiếp tục gọi `getPaywallProducts(flow)`. | | `hasViewConfiguration` | đã xóa | Xóa mọi kiểm tra `hasViewConfiguration` khỏi code của bạn — `createFlowView` sẽ trả về lỗi thay thế (xem [Hiển thị flow](#displaying-flows)). | `hasViewConfiguration` vẫn còn trên `AdaptyOnboarding` — chỉ có flow model là bỏ nó đi. ## Phương thức Web paywall \{#web-paywall-methods\} `openWebPaywall` và `createWebPaywallUrl` giữ nguyên tên, nhưng tham số `paywall` được thay thế bằng tham số `flowPaywall` nhận một `AdaptyFlowPaywall` — một trong các biến thể trong `flow.paywalls`. Bạn vẫn có thể truyền vào một `AdaptyPaywallProduct` thay thế: ```diff showLineNumbers - Adapty.openWebPaywall(paywall = paywall) + flow.paywalls.firstOrNull()?.let { flowPaywall -> + Adapty.openWebPaywall(flowPaywall = flowPaywall) + } ``` ## Theo dõi lượt xem flow \{#tracking-flow-views\} ### logShowPaywall → logShowFlow `logShowPaywall` được đổi tên thành `logShowFlow` và nhận vào một `AdaptyFlow`. Sự kiện vẫn được ghi nhận theo cùng một biến thể, vì vậy các chỉ số funnel và A/B test hiện có vẫn hoạt động bình thường mà không cần thay đổi gì trên dashboard. ```diff showLineNumbers - Adapty.logShowPaywall(paywall) + Adapty.logShowFlow(flow) ``` Giống như trong v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được dựng bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi các lượt xem đó. ## Hiển thị flows \{#displaying-flows\} ### createPaywallView → createFlowView Đổi tên phương thức factory và truyền vào `AdaptyFlow`. Kiểu view trả về được đổi tên từ `AdaptyUIPaywallView` thành `AdaptyUIFlowView`, nhưng các phương thức (`present`, `dismiss`) và các tham số tùy chọn (`loadTimeout`, `preloadProducts`, `customTags`, `customTimers`, `customAssets`, `productPurchaseParams`) vẫn không thay đổi: ```diff showLineNumbers - AdaptyUI.createPaywallView(paywall) + AdaptyUI.createFlowView(flow) .onSuccess { view -> view.present() } .onError { error -> // handle the error } ``` Nếu bạn không sử dụng Compose Multiplatform, phương thức factory native cũng được đổi tên tương tự: ```diff showLineNumbers - AdaptyUI.createNativePaywallView(paywall) + AdaptyUI.createNativeFlowView(flow) ``` `createFlowView` trả về `AdaptyResult.Error` nếu flow không có view được cấu hình — điều này thay thế cho kiểm tra `hasViewConfiguration` ở v3: ```diff showLineNumbers - if (paywall.hasViewConfiguration) { - AdaptyUI.createPaywallView(paywall) - .onSuccess { view -> view.present() } - } + AdaptyUI.createFlowView(flow) + .onSuccess { view -> view.present() } + .onError { error -> + // the flow has no view configured, or view creation failed + } ``` :::note Flow view chỉ dùng được một lần: sau khi gọi `dismiss()`, view sẽ bị huỷ, vì vậy hãy gọi lại `createFlowView` nếu muốn hiển thị flow thêm lần nữa. ::: ## Xử lý sự kiện \{#handling-events\} Observer sự kiện được đổi tên từ `AdaptyUIPaywallsEventsObserver` thành `AdaptyUIFlowsEventsObserver`, và các callback của nó thay đổi tiền tố `paywallView` thành `flowView`. Nội dung các handler hiện có không cần thay đổi code — chỉ cần đổi tên kiểu và các override: ```diff showLineNumbers - AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver { - override fun paywallViewDidFinishPurchase( - view: AdaptyUIPaywallView, + AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver { + override fun flowViewDidFinishPurchase( + view: AdaptyUIFlowView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { // custom logic after purchase } }) ``` Một callback cũng được đổi tên: `paywallViewDidFailRendering` thành `flowViewDidReceiveError`. Callback này kích hoạt cho các lỗi render như trước, cộng thêm các lỗi runtime không liên quan đến mua hàng: ```diff showLineNumbers - override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) {} + override fun flowViewDidReceiveError(view: AdaptyUIFlowView, error: AdaptyError) {} ``` Xem [Xử lý sự kiện flow & paywall](kmp-handling-events) để biết danh sách đầy đủ các callback. ### Compose platform view Nếu bạn nhúng view bằng composable Compose Multiplatform, `AdaptyUIPaywallPlatformView(paywall, ...)` được đổi tên thành `AdaptyUIFlowPlatformView(flow, ...)`. Các callback sự kiện vẫn giữ tên `onDid...`, ngoại trừ `onDidFailRendering` được đổi thành `onDidReceiveError`: ```diff showLineNumbers - AdaptyUIPaywallPlatformView( - paywall = paywall, + AdaptyUIFlowPlatformView( + flow = flow, onDidFinishPurchase = { view, product, result -> /* ... */ }, ) ``` Giống như trong v3, các callback bạn truyền vào đây (và bất kỳ observer nào được đăng ký qua `registerFlowEventsListener`) chạy **bổ sung vào** global observer, không thay thế nó — callback của bạn quan sát một sự kiện; nó không thay thế hành vi mặc định toàn cục. Hãy lưu ý các [thay đổi mặc định](#default-behavior-changes): ví dụ, hành vi mặc định toàn cục không còn tự động đóng view sau khi mua hàng nữa. ### API Mới \{#new-apis\} - `AdaptyUI.setObserverModeResolver(...)` với `AdaptyUIObserverModeResolver` — điều khiển các giao dịch mua và khôi phục được khởi tạo từ flow khi SDK chạy ở [chế độ Observer](implement-observer-mode-kmp). Trước đây, tính năng này chỉ có trong các SDK native iOS và Android. Xem [Hiển thị flow trong chế độ Observer](kmp-present-flows-in-observer-mode). - `AdaptyUI.setSystemRequestsHandler(...)` với `AdaptyUISystemRequestsHandler` — dành riêng cho các yêu cầu hệ thống từ flow (lời nhắc cấp quyền OS và yêu cầu đánh giá ứng dụng). Hiện tại flow chưa kích hoạt các yêu cầu này, nên bạn chưa cần đăng ký handler. - Callback tùy chọn mới `flowViewDidReceiveAnalyticEvent` được dành riêng cho các sự kiện phân tích tùy chỉnh từ flow. Hiện tại flow chưa phát ra các sự kiện này cho code của bạn, nên bạn chưa cần triển khai. - `AdaptyUI.openWebUrl(url, openIn)` và `AdaptyUI.requestAppReview()` — hỗ trợ xử lý mặc định `OpenUrlAction` và `handleAppReviewRequest` mặc định, giúp xử lý URL và lời nhắc đánh giá ứng dụng ngay lập tức mà không cần cấu hình thêm. Chỉ gọi trực tiếp khi bạn ghi đè các giá trị mặc định đó. - `AdaptyConfig.ServerCluster.CN` — tùy chọn cụm máy chủ mới bên cạnh `DEFAULT` và `EU`, dùng để kết nối ứng dụng với [máy chủ China của Adapty](china-cluster). ## Thay đổi hành vi mặc định \{#default-behavior-changes\} Những thay đổi này không gây ra lỗi biên dịch, vì vậy hãy kiểm tra chúng ở runtime: - **Hoàn tất mua hàng**: Trong v3, `paywallViewDidFinishPurchase` mặc định sẽ đóng view sau mọi kết quả mua hàng ngoại trừ `AdaptyPurchaseResult.UserCanceled`. Trong v4, `flowViewDidFinishPurchase` mặc định là no-op, tức là **flow vẫn mở sau khi mua hàng cho đến khi bạn tự đóng nó** — giống hành vi trên iOS. Nếu bạn phụ thuộc vào tính năng tự đóng đó, hãy tự gọi `view.dismiss()` sau khi mua hàng xong. - **Nút back hệ thống Android**: Trong v3, `paywallViewDidPerformAction` mặc định sẽ đóng view với cả `CloseAction` lẫn `AndroidSystemBackAction`. Trong v4, hành vi mặc định chỉ xử lý `CloseAction` — **nút back hệ thống không còn tự đóng flow nữa**, giống iOS, nơi flow không thể bị đóng bằng thao tác hệ thống. Hãy cung cấp cho người dùng cách thoát rõ ràng (nút **Close** hoặc hành động `on_device_back`), hoặc tự đóng view trong `flowViewDidPerformAction`. - **Lỗi view**: Trong v3, `paywallViewDidFailRendering` mặc định không làm gì. Trong v4, `flowViewDidReceiveError` mặc định sẽ **đóng view** — hãy override nó nếu bạn muốn giữ view mở hoặc xử lý lỗi theo cách khác. - **View chỉ dùng một lần**: Sau khi gọi `dismiss()`, view sẽ bị hủy. Gọi lại `createFlowView` để hiển thị flow thêm một lần nữa. ## Ngừng hỗ trợ Onboarding API \{#onboarding-api-deprecation\} Onboarding API cũ đã bị ngừng hỗ trợ trong v4.0, thay thế bằng [Flow Builder](adapty-flow-builder). API này vẫn hoạt động, nhưng sẽ bị xóa trong phiên bản tương lai, vì vậy hãy lên kế hoạch migration các onboarding của bạn sang Flow Builder. Các ký hiệu đã ngừng hỗ trợ: `getOnboarding`, `getOnboardingForDefaultAudience`, `AdaptyUI.createOnboardingView`, `AdaptyUI.createNativeOnboardingView`, và `AdaptyUIOnboardingsEventsObserver`. --- # File: migration-to-kmp-315 --- --- title: "Hướng dẫn migration lên Adapty Kotlin Multiplatform SDK 3.15.0" description: "Các bước migration cho Adapty Kotlin Multiplatform SDK 3.15.0" --- Adapty Kotlin Multiplatform SDK 3.15.0 là một bản phát hành lớn mang đến các tính năng và cải tiến mới, 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 và method của observer. 2. Cập nhật tên method của fallback paywalls. 3. Cập nhật tên class view trong các method xử lý sự kiện. ## Cập nhật tên class và method của observer \{#update-observer-class-and-method-names\} Tên class observer và method đăng ký của nó đã được đổi tên: ```diff - import com.adapty.kmp.AdaptyUIObserver + import com.adapty.kmp.AdaptyUIPaywallsEventsObserver - import com.adapty.kmp.models.AdaptyUIView + import com.adapty.kmp.models.AdaptyUIPaywallView - class MyAdaptyUIObserver : AdaptyUIObserver { - override fun paywallViewDidPerformAction(view: AdaptyUIView, action: AdaptyUIAction) { + class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver { + override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { // handle actions } } // Set up the observer - AdaptyUI.setObserver(MyAdaptyUIObserver()) + AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` ## Cập nhật tên method của fallback paywalls \{#update-fallback-paywalls-method-name\} Tên method dùng để thiết lập fallback paywalls đã được thay đổi: ```diff showLineNumbers - Adapty.setFallbackPaywalls(assetId = "fallback.json") + Adapty.setFallback(assetId = "fallback.json") .onSuccess { // Fallback paywalls loaded successfully } .onError { error -> // Handle the error } ``` ## Cập nhật tên class view trong các method xử lý sự kiện \{#update-view-class-name-in-event-handling-methods\} Tất cả các method xử lý sự kiện hiện sử dụng class `AdaptyUIPaywallView` mới thay vì `AdaptyUIView`: ```diff - override fun paywallViewDidAppear(view: AdaptyUIView) { + override fun paywallViewDidAppear(view: AdaptyUIPaywallView) { // Handle paywall appearance } - override fun paywallViewDidDisappear(view: AdaptyUIView) { + override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) { // Handle paywall disappearance } - override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) { + override fun paywallViewDidSelectProduct(view: AdaptyUIView, productId: String) { // Handle product selection } - override fun paywallViewDidStartPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct) { + override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) { // Handle purchase start } - override fun paywallViewDidFinishPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) { + override fun paywallViewDidFinishPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) { // Handle purchase result } - override fun paywallViewDidFailPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, error: AdaptyError) { + override fun paywallViewDidFailPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, error: AdaptyError) { // Add your purchase failure handling logic here } - override fun paywallViewDidFinishRestore(view: AdaptyUIView, profile: AdaptyProfile) { + override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) { // Add your successful restore handling logic here } - override fun paywallViewDidFailRestore(view: AdaptyUIView, error: AdaptyError) { + override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your restore failure handling logic here } - override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIView, product: AdaptyPaywallProduct?, error: AdaptyError?) { + override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct?, error: AdaptyError?) { // Handle web payment navigation result } - override fun paywallViewDidFailLoadingProducts(view: AdaptyUIView, error: AdaptyError) { + override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your product loading failure handling logic here } - override fun paywallViewDidFailRendering(view: AdaptyUIView, error: AdaptyError) { + override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) { // Handle rendering error } ``` --- # End of Documentation _Generated on: 2026-07-24T13:01:43.499Z_ _Successfully processed: 48/48 files_ # REACT-NATIVE - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.502Z Total files: 45 --- # File: sdk-installation-react-native-expo --- --- title: "Install & configure Adapty React Native SDK in an Expo project" description: "Step-by-step guide on installing Adapty React Native SDK in an Expo project for subscription-based apps." --- :::important Hướng dẫn này đề cập đến việc cài đặt và cấu hình Adapty React Native SDK **trong một dự án Expo**. Nếu bạn đang sử dụng **React Native thuần (không có Expo)**, hãy làm theo [hướng dẫn cài đặt React Native](sdk-installation-react-native-pure) thay thế. ::: Adapty SDK bao gồm hai module chính để tích hợp liền mạch vào ứng dụng React Native của bạn: - **Core Adapty**: Module này bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), công cụ no-code thân thiện giúp tạo paywall đa nền tảng dễ dàng. AdaptyUI được kích hoạt tự động cùng với module core. Nếu bạn muốn xem hướng dẫn đầy đủ về cách triển khai IAP trong ứng dụng React Native, hãy xem [tại đây](https://adapty.io/blog/react-native-in-app-purchases-tutorial/). :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng Expo? Hãy xem các ứng dụng mẫu của chúng tôi: - [Expo dev build sample](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo) — đầy đủ chức năng bao gồm mua hàng thực tế và Paywall Builder - [Expo Go & Web sample](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/ExpoGoWebMock) — kiểm thử với chế độ mock ::: Để xem hướng dẫn triển khai đầy đủ, bạn có thể xem video sau: <div style={{ textAlign: 'center' }}> <iframe width="560" height="315" src="https://www.youtube.com/embed/TtCJswpt2ms?si=FlFJGvpj-U33yoNK" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> </div> ## Yêu cầu \{#requirements\} Adapty React Native SDK yêu cầu iOS 15.0+. Để build cho iOS cần có **Swift 6.0** trở lên. [Kids Mode](kids-mode-react-native) yêu cầu **Swift 6.1** trở lên. :::info Từ SDK v3.17, Adapty SDK sử dụng Google Play Billing Library v8.0.0 theo mặc định. ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK \{#install-adapty-sdk\} :::important Kể từ v4, Adapty React Native SDK không còn hỗ trợ cài đặt các thư viện native phụ thuộc qua CocoaPods. Nếu bạn cần v4 trở lên (dành cho [Flow Builder](adapty-flow-builder)), hãy làm theo hướng dẫn [Adapty SDK 4.0: enable Swift Package Manager](#adapty-sdk-40-enable-swift-package-manager) bên dưới. ::: [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-React-Native.svg?style=flat&logo=react)](https://github.com/adaptyteam/AdaptySDK-React-Native/releases) :::important [Expo Dev Client](https://docs.expo.dev/versions/latest/sdk/dev-client/) (một bản build phát triển tùy chỉnh) là bắt buộc để sử dụng Adapty trong dự án Expo. Expo Go không hỗ trợ các native module tùy chỉnh, vì vậy bạn chỉ có thể dùng nó với [**chế độ mock**](#set-up-mock-mode-for-expo-go--expo-web) để phát triển UI/logic (không có giao dịch mua thật và không có AdaptyUI/Paywall Builder). ::: 1. Cài đặt Adapty SDK (thao tác này cũng tự động cài `@adapty/core`): ```sh npx expo install react-native-adapty npx expo prebuild ``` 2. Build ứng dụng cho môi trường phát triển bằng EAS hoặc local build: <Tabs> <TabItem value="eas" label="EAS build" default> ```sh # For iOS eas build --profile development --platform ios # For Android eas build --profile development --platform android ``` </TabItem> <TabItem value="local" label="Local build"> ```sh # For iOS npx expo run:ios # For Android npx expo run:android ``` </TabItem> </Tabs> 3. Khởi động dev server: ```sh npx expo start --dev-client ``` ### Adapty SDK 4.0: bật Swift Package Manager \{#adapty-sdk-40-enable-swift-package-manager\} React Native SDK 4.0 — bổ sung hỗ trợ [Flow Builder](adapty-flow-builder) — yêu cầu **React Native 0.75 trở lên**. Cài đặt SDK: ```sh npx expo install react-native-adapty@^4.0.0 ``` v4 kéo các iOS SDK gốc (`Adapty`, `AdaptyUI`, `AdaptyPlugin`) thông qua Swift Package Manager thay vì CocoaPods sub-dependencies ([Repo spec của CocoaPods sẽ chuyển sang chế độ chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/)). SPM yêu cầu dynamic frameworks, mà trong Expo bạn bật bằng plugin [`expo-build-properties`](https://docs.expo.dev/versions/latest/sdk/build-properties/). Thêm nó vào `app.json` (hoặc `app.config.js`): ```json showLineNumbers title="app.json" { "expo": { "plugins": [ [ "expo-build-properties", { "ios": { "useFrameworks": "dynamic" } } ] ] } } ``` Sau đó cài đặt plugin và tạo lại native project: ```sh npx expo install expo-build-properties npx expo prebuild --clean ``` Xem [Hướng dẫn migration Adapty React Native SDK lên v4](migration-to-react-native-sdk-v4) để biết đầy đủ các bước migration. ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Để 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. Sao chép đoạn code sau vào `App.tsx` để kích hoạt Adapty: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` :::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 React Native SDK](react-native-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), hãy xem [hướng dẫn nhanh về Paywall Builder](react-native-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](react-native-quickstart-manual). :::tip Để tránh lỗi kích hoạt trong môi trường phát triển, hãy xem các [mẹo](#development-environment-tips). ::: ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder), bạn cần module AdaptyUI. Module này sẽ được kích hoạt tự động khi bạn kích hoạt module chính; bạn không cần làm gì thêm. ## Cài đặt 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 chuyện gì đang xảy ra. Các mức độ log hiện có như sau: | Level | Description | | ---------- | ------------------------------------------------------------ | | `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à nhiều thông báo thông tin khác | | `verbose` | Ghi lại mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như 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 hoặc trong quá trình cấu hình Adapty: ```typescript showLineNumbers title="App.tsx" // Set log level before activation // 'verbose' is recommended for development and the first production release adapty.setLogLevel('verbose'); // Or set it during configuration adapty.activate('YOUR_PUBLIC_SDK_KEY', { logLevel: 'verbose', }); ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi lên, nhưng bạn có thể triển khai 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 từng 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 tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư cho người dùng, tuân thủ các quy định bảo vệ dữ liệu khu vực (như GDPR hoặc CCPA), hoặc giảm thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không được yêu cầu trong ứng dụng của bạn. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ipAddressCollectionDisabled: true, }); ``` #### Vô hiệu hóa việc thu thập và chia sẻ advertising ID \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ios.idfaCollectionDisabled` (iOS) hoặc `android.adIdCollectionDisabled` (Android) thành `true` để tắt tính năng thu thập advertising identifier. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách của App Store/Play Store, tránh kích hoạt hộp thoại 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 advertising ID. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { idfaCollectionDisabled: true, }, android: { adIdCollectionDisabled: true, }, }); ``` #### Cấu hình bộ nhớ đệm media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Theo mặc định, AdaptyUI lưu vào bộ nhớ đệm các tệp media (như hình ảnh và video) để cải thiện hiệu suất và giảm mức sử dụng mạng. Bạn có thể tùy chỉnh cài đặt bộ nhớ đệm bằng cách cung cấp cấu hình tùy chỉnh. Sử dụng `mediaCache` để ghi đè các cài đặt bộ nhớ đệm mặc định: ```typescript adapty.activate('YOUR_PUBLIC_SDK_KEY', { mediaCache: { memoryStorageTotalCostLimit: 200 * 1024 * 1024, // Optional: memory cache size in bytes memoryStorageCountLimit: 2147483647, // Optional: max number of items in memory diskStorageSizeLimit: 200 * 1024 * 1024, // Optional: disk cache size in bytes }, }); ``` | 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 theo giá trị riêng của từng nền tảng. | | memoryStorageCountLimit | tùy chọn | Giới hạn số lượng item trong bộ nhớ cache. Mặc định theo giá trị riêng của từng nền tảng. | | 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 theo giá trị riêng của từng nền tảng. | ### Bật mức độ truy cập cục bộ (Android) \{#enable-local-access-levels-android\} Theo mặc định, [mức độ truy cập cục bộ](local-access-levels) được bật trên iOS và tắt trên Android. Để bật tính năng này trên Android, hãy đặt `localAccessLevelAllowed` thành `true`: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { android: { localAccessLevelAllowed: true, }, }); ``` ### Xóa dữ liệu khi khôi phục từ backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ bản 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, thông tin sản phẩm và paywall. Sau đó SDK sẽ khởi tạo lại với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ có cache cục bộ của SDK bị xóa. Lịch sử giao dịch với Apple và dữ liệu người dùng trên máy chủ Adapty vẫn không thay đổi. ::: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { clearDataOnBackup: true }, }); ``` ## Mẹo cho môi trường phát triển \{#development-environment-tips\} #### Thiết lập chế độ mock cho Expo Go / Expo Web \{#set-up-mock-mode-for-expo-go--expo-web\} Expo Go và Expo Web không có quyền truy cập vào các native module của Adapty. Để tránh lỗi runtime trong khi vẫn có thể build và kiểm tra UI cũng như logic paywall của ứng dụng, Adapty cung cấp **chế độ mock**. ::::important Chế độ mock **không** phải là công cụ để kiểm tra giao dịch mua thực tế: - Nó **không mở** luồng mua hàng của App Store / Google Play và **không tạo** giao dịch thực. - Nó **không hiển thị** paywall/onboarding được tạo bằng **Adapty Paywall Builder (AdaptyUI)**. - Các native module của Adapty bị **bỏ qua hoàn toàn**—ngay cả khi thiếu file native SDK trong Xcode/Android build hoặc API key không hợp lệ cũng sẽ không gây ra lỗi. Để kiểm tra mua hàng thực và paywall của Paywall Builder, hãy sử dụng Expo Dev Client / production build—ở đó chế độ mock sẽ tự động bị tắt. :::: **Theo mặc định**, SDK tự động phát hiện môi trường Expo Go và web, rồi bật chế độ mock. Bạn không cần cấu hình gì thêm trừ khi muốn tùy chỉnh dữ liệu mock. Khi chế độ mock đang hoạt động: - Tất cả các phương thức Adapty đều trả về dữ liệu mock mà không thực hiện bất kỳ yêu cầu mạng nào đến máy chủ của Adapty. - Theo mặc định, hồ sơ người dùng mock ban đầu không có gói đăng ký nào đang hoạt động. - Theo mặc định, `makePurchase(...)` mô phỏng một giao dịch mua thành công và cấp quyền truy cập premium. Bạn có thể tùy chỉnh dữ liệu mock bằng `mockConfig` trong quá trình kích hoạt. Xem định dạng cấu hình và các tham số được hỗ trợ [tại đây](https://react-native.adapty.io/interfaces/adaptymockconfig). ```typescript showLineNumbers title="App.tsx" try { await adapty.activate('YOUR_PUBLIC_SDK_KEY', { mockConfig: { // Customize the initial mock profile (optional) }, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); } ``` Nếu bạn cần gọi các phương thức SDK trước khi kích hoạt (chẳng hạn như `isActivated()` hoặc `setLogLevel()`), hãy sử dụng `enableMock()` trước `activate()`. Nếu bridge đã được khởi tạo, phương thức này sẽ không làm gì cả. ```typescript showLineNumbers title="App.tsx" adapty.enableMock(); // Optional: pass mockConfig to customize mock data // Now you can call methods before activation await adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` #### Trì hoãn kích hoạt SDK cho mục đích phát triển \{#delay-sdk-activation-for-development-purposes\} Adapty tải trước tất cả dữ liệu người dùng cần thiết khi khởi động SDK, giúp truy cập dữ liệu mới nhanh hơn. Tuy nhiên, điều này có thể gây ra vấn đề trên iOS Simulator, vì simulator thường xuyên yêu cầu xác thực trong quá trình phát triển. Mặc dù Adapty không thể kiểm soát luồng xác thực StoreKit, nhưng có thể trì hoãn các yêu cầu mà SDK thực hiện để lấy dữ liệu người dùng mới. Bằng cách bật thuộc tính `__debugDeferActivation`, lệnh gọi activate sẽ được giữ lại cho đến khi bạn thực hiện lệnh gọi Adapty SDK tiếp theo. Điều này giúp tránh các lần nhắc xác thực không cần thiết nếu không cần dùng đến. Cần lưu ý rằng **tính năng này chỉ dành cho mục đích phát triển**, vì nó không bao gồm tất cả các tình huống người dùng có thể xảy ra. Trong môi trường production, không nên trì hoãn việc kích hoạt, vì các thiết bị thực thường ghi nhớ dữ liệu xác thực và không liên tục yêu cầu nhập lại thông tin đăng nhập. Dưới đây là cách sử dụng được khuyến nghị: ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __debugDeferActivation: isSimulator(), // 'isSimulator' from any 3rd party library }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` #### Khắc phục lỗi kích hoạt SDK trên Fast Refresh của React Native \{#troubleshoot-sdk-activation-errors-on-react-natives-fast-refresh\} Khi phát triển với Adapty SDK trong React Native, bạn có thể gặp lỗi: `Adapty can only be activated once. Ensure that the SDK activation call is not made more than once.` Điều này xảy ra vì tính năng fast refresh của React Native kích hoạt nhiều lần gọi activation trong quá trình phát triển. Để tránh điều này, hãy sử dụng tùy chọn `__ignoreActivationOnFastRefresh` với giá trị `__DEV__` (cờ chế độ phát triển của React Native). ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __ignoreActivationOnFastRefresh: __DEV__, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` ## Khắc phục sự cố \{#troubleshooting\} #### Lỗi phiên bản iOS tối thiểu \{#minimum-ios-version-error\} Khi build cho iOS, bạn có thể gặp lỗi về **phiên bản iOS tối thiểu** hoặc deployment target. Adapty yêu cầu **iOS 15.0+**. Vì Expo tạo ra dự án iOS (bao gồm cả `Podfile`) trong quá trình `expo prebuild`, **bạn không nên chỉnh sửa `Podfile` trực tiếp**. Thay vào đó, hãy cấu hình deployment target thông qua config plugin `expo-build-properties`. 1. Cài đặt plugin: ```sh npx expo install expo-build-properties ``` 2. Cập nhật cấu hình Expo (`app.json` hoặc `app.config.js`) để đặt iOS deployment target: ``` { "expo": { // ...other Expo config... "plugins": [ [ "expo-build-properties", { "ios": { // Adapty requires iOS 15.0+. "deploymentTarget": "15.0" } } ], ] } } ``` 3. Tạo lại native iOS project và build lại: ``` npx expo prebuild --clean npx expo run:ios # or `eas build -p ios` on your CI ``` #### Xung đột Android Auto Backup manifest \{#android-auto-backup-manifest-conflict\} Khi sử dụng Expo với nhiều SDK cùng cấu hình Android Auto Backup (chẳng hạn như Adapty, AppsFlyer, hoặc expo-secure-store), bạn có thể gặp lỗi xung đột khi merge manifest. Lỗi thường gặp trông như sau: `Manifest merger failed : Attribute application@fullBackupContent value=(@xml/secure_store_backup_rules) from AndroidManifest.xml:24:248-306 is also present at [io.adapty:android-sdk:3.12.0] AndroidManifest.xml:9:18-70 value=(@xml/adapty_backup_rules).` Để giải quyết xung đột này, bạn cần để plugin Adapty quản lý cấu hình Android backup. Nếu dự án của bạn cũng sử dụng `expo-secure-store`, hãy tắt thiết lập backup của nó để tránh bị trùng lặp. Đây là cách cấu hình `app.json` của bạn: ```json title="app.json" { "expo": { "plugins": [ ["react-native-adapty", { "replaceAndroidBackupConfig": true }], ["expo-secure-store", { "configureAndroidBackup": false }] ] } } ``` Tùy chọn `replaceAndroidBackupConfig` mặc định là `false`. Khi được bật, nó cho phép plugin Adapty kiểm soát các quy tắc sao lưu Android. Thêm `"configureAndroidBackup": false` nếu bạn sử dụng `expo-secure-store` để tránh cảnh báo, vì cấu hình sao lưu của SecureStore sẽ được Adapty xử lý. :::important Cài đặt này chỉ áp dụng các yêu cầu sao lưu cho Adapty, AppsFlyer và expo-secure-store. Nếu các thư viện khác trong dự án của bạn có quy tắc sao lưu tùy chỉnh, bạn sẽ cần tự cấu hình thủ công. ::: --- # File: sdk-installation-react-native-pure --- --- title: "Install & configure Adapty SDK in a pure React Native project" description: "Step-by-step guide on installing Adapty SDK on React Native for subscription-based apps." --- :::important Hướng dẫn này chỉ áp dụng cho **dự án React Native thuần (không dùng Expo)**. Nếu bạn đang dùng **Expo**, hãy xem [hướng dẫn cài đặt Expo](sdk-installation-react-native-expo) thay thế. ::: SDK Adapty bao gồm hai module chính để tích hợp liền mạch vào ứng dụng React Native của bạn: - **Core Adapty**: Module này là bắt buộc để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), công cụ không cần code, thân thiện với người dùng để dễ dàng tạo paywall đa nền tảng. AdaptyUI sẽ tự động được kích hoạt cùng với module core. :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Yêu cầu \{#requirements\} Adapty React Native SDK yêu cầu iOS 15.0 trở lên. Để build cho iOS cần có **Swift 6.0** hoặc cao hơn. [Kids Mode](kids-mode-react-native) yêu cầu **Swift 6.1** hoặc cao hơn. :::info Từ SDK v3.17 trở đi, Adapty SDK sử dụng Google Play Billing Library v8.0.0 theo mặc định. ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK \{#install-adapty-sdk\} :::important Kể từ v4, Adapty React Native SDK không còn hỗ trợ cài đặt các native dependency qua CocoaPods. Nếu bạn cần v4 trở lên (dành cho [Flow Builder](adapty-flow-builder)), hãy làm theo hướng dẫn [Adapty SDK 4.0: enable Swift Package Manager](#adapty-sdk-40-enable-swift-package-manager) bên dưới. ::: [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-React-Native.svg?style=flat&logo=react)](https://github.com/adaptyteam/AdaptySDK-React-Native/releases) 1. Cài đặt Adapty SDK (thao tác này cũng tự động cài đặt `@adapty/core`): ```sh showLineNumbers title="Shell" # using npm npm install react-native-adapty # or using yarn yarn add react-native-adapty ``` 2. Với iOS, cài đặt pods: ```sh showLineNumbers title="Shell" cd ios && pod install ``` <details> <summary>Với Android, nếu phiên bản React Native của bạn cũ hơn 0.73.0 (nhấn để mở rộng)</summary> Cập nhật file `/android/build.gradle`. Đảm bảo rằng có dependency `kotlin-gradle-plugin:1.8.0` hoặc phiên bản mới hơn: ```groovy showLineNumbers title="/android/build.gradle" ... buildscript { ... dependencies { ... classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0" } } ... ``` </details> ### Adapty SDK 4.0: kích hoạt Swift Package Manager React Native SDK 4.0 — hỗ trợ [Flow Builder](adapty-flow-builder) — yêu cầu **React Native 0.75 trở lên**. Cài đặt SDK: ```sh showLineNumbers title="Shell" npm install react-native-adapty@^4.0.0 # or using yarn yarn add react-native-adapty@^4.0.0 ``` v4 tải các native iOS SDK (`Adapty`, `AdaptyUI`, `AdaptyPlugin`) thông qua Swift Package Manager thay vì CocoaPods sub-dependencies ([repo spec của CocoaPods sẽ chuyển sang chế độ chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/)). SPM yêu cầu dynamic frameworks — thêm đoạn sau vào target trong `ios/Podfile` của bạn, sau đó cài lại pods: ```ruby showLineNumbers title="ios/Podfile" use_frameworks! :linkage => :dynamic ``` ```sh showLineNumbers title="Shell" cd ios && pod install --repo-update ``` Nếu trước đây bạn đã thêm `Adapty`, `AdaptyUI`, hoặc `AdaptyPlugin` dưới dạng sub-dependencies của CocoaPods, hãy xóa các dòng `pod 'Adapty'`, `pod 'AdaptyUI'`, hoặc `pod 'AdaptyPlugin'` khỏi `Podfile` trước. :::warning Chuyển từ liên kết tĩnh (static linkage) mặc định sang dynamic frameworks có thể xung đột với các thư viện chưa hỗ trợ modular headers, và không tương thích với Flipper. Xem [Migrate Adapty React Native SDK to v4](migration-to-react-native-sdk-v4) để biết thêm chi tiết. ::: ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Để 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. Sao chép đoạn code sau vào `App.tsx` để kích hoạt Adapty: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` :::important Hãy đợi `activate` hoàn thành trước khi gọi bất kỳ phương thức nào khác của Adapty SDK. Xem [Thứ tự gọi trong React Native SDK](react-native-sdk-call-order) để biết toàn bộ trình tự. ::: Bây giờ hãy thiết lập các paywall trong ứng dụng của bạn: - Nếu bạn dùng [Adapty Paywall Builder](adapty-paywall-builder), hãy xem [hướng dẫn nhanh về Paywall Builder](react-native-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, hãy xem [hướng dẫn nhanh cho custom paywall](react-native-quickstart-manual). :::tip Để tránh lỗi kích hoạt trong môi trường phát triển, hãy tham khảo [các mẹo](#development-environment-tips). ::: ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder), bạn cần module AdaptyUI. Module này được kích hoạt tự động khi bạn kích hoạt module lõi; bạn không cần làm gì thêm. ## Cài đặt tùy chọn \{#optional-setup\} ### Ghi log \{#logging\} #### Thiết lập hệ thống ghi log \{#set-up-the-logging-system\} Adapty ghi lại các lỗi và thông tin quan trọng khác để giúp bạn hiểu chuyện gì đang xảy ra. Các cấp độ ghi log hiện có như sau: | Level | Description | | ---------- | ------------------------------------------------------------ | | `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à nhiều thông báo thông tin khác | | `verbose` | Ghi lại mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như các lần gọi hàm, truy vấn API, v.v. | Bạn có thể thiết lập mức độ log trong ứng dụng trước hoặc trong quá trình cấu hình Adapty: ```typescript showLineNumbers title="App.tsx" // Set log level before activation // 'verbose' is recommended for development and the first production release adapty.setLogLevel('verbose'); // Or set it during configuration adapty.activate('YOUR_PUBLIC_SDK_KEY', { logLevel: 'verbose', }); ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi, 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 từng 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 tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư cho người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm thiểu việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không được yêu cầu trong ứng dụng của bạn. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ipAddressCollectionDisabled: true, }); ``` #### Tắt tính năng thu thập và chia sẻ advertising ID \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ios.idfaCollectionDisabled` (iOS) hoặc `android.adIdCollectionDisabled` (Android) thành `true` để tắt thu thập advertising identifier. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách của App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc khi ứng dụng của bạn không cần attribution quảng cáo hay analytics dựa trên advertising ID. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { idfaCollectionDisabled: true, }, android: { adIdCollectionDisabled: true, }, }); ``` #### Thiết lập cấu hình bộ nhớ đệm media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Theo mặc định, AdaptyUI lưu vào bộ nhớ đệm các file media (như hình ảnh và video) để cải thiện hiệu suất và giảm lưu lượng mạng. Bạn có thể tùy chỉnh các cài đặt bộ nhớ đệm bằng cách cung cấp cấu hình tùy chỉnh. Sử dụng `mediaCache` để ghi đè các cài đặt bộ nhớ đệm mặc định: ```typescript adapty.activate('YOUR_PUBLIC_SDK_KEY', { mediaCache: { memoryStorageTotalCostLimit: 200 * 1024 * 1024, // Optional: memory cache size in bytes memoryStorageCountLimit: 2147483647, // Optional: max number of items in memory diskStorageSizeLimit: 200 * 1024 * 1024, // Optional: disk cache size in bytes }, }); ``` | 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 theo giá trị riêng của từng nền tảng. | | memoryStorageCountLimit | tùy chọn | Giới hạn số lượng mục trong bộ nhớ cache. Mặc định theo giá trị riêng của từng nền tảng. | | diskStorageSizeLimit | tùy chọn | Giới hạn kích thước tệp trên đĩa tính bằng byte. Mặc định theo giá trị riêng của từng nền tảng. | ### Bật mức độ truy cập cục bộ (Android) \{#enable-local-access-levels-android\} Theo mặc định, [mức độ truy cập cục bộ](local-access-levels) được bật trên iOS và tắt trên Android. Để bật tính năng này trên Android, hãy đặt `localAccessLevelAllowed` thành `true`: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { android: { localAccessLevelAllowed: true, }, }); ``` ### Xóa dữ liệu khi khôi phục từ backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ bản backup iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng đã cache, chi tiết sản phẩm và các paywall. Sau đó SDK sẽ khởi tạo lại với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ có 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. ::: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { clearDataOnBackup: true }, }); ``` ## Mẹo cho môi trường phát triển \{#development-environment-tips\} #### Trì hoãn kích hoạt SDK cho mục đích phát triển \{#delay-sdk-activation-for-development-purposes\} Adapty tải trước tất cả dữ liệu người dùng cần thiết ngay khi SDK được kích hoạt, giúp truy cập dữ liệu mới nhanh hơn. Tuy nhiên, điều này có thể gây ra vấn đề trong iOS simulator, nơi thường xuyên yêu cầu xác thực trong quá trình phát triển. Mặc dù Adapty không thể kiểm soát luồng xác thực StoreKit, nhưng có thể trì hoãn các yêu cầu mà SDK thực hiện để lấy dữ liệu người dùng mới. Bằng cách bật thuộc tính `__debugDeferActivation`, lệnh gọi activate sẽ được giữ lại cho đến khi bạn thực hiện lệnh gọi SDK tiếp theo. Điều này giúp tránh các thông báo yêu cầu dữ liệu xác thực không cần thiết khi không cần thiết. Cần lưu ý rằng **tính năng này chỉ dành cho mục đích phát triển**, vì nó không bao phủ tất cả các tình huống người dùng tiềm năng. Trong môi trường production, không nên trì hoãn việc kích hoạt, vì các thiết bị thực thường ghi nhớ dữ liệu xác thực và không liên tục yêu cầu nhập thông tin đăng nhập. Dưới đây là cách tiếp cận được khuyến nghị khi sử dụng: ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __debugDeferActivation: isSimulator(), // 'isSimulator' từ thư viện bên thứ ba bất kỳ }); } catch (error) { console.error('Không thể kích hoạt Adapty SDK:', error); // Xử lý lỗi phù hợp với ứng dụng của bạn } ``` #### Khắc phục lỗi kích hoạt SDK khi sử dụng Fast Refresh của React Native \{#troubleshoot-sdk-activation-errors-on-react-natives-fast-refresh\} Khi phát triển với Adapty SDK trong React Native, bạn có thể gặp lỗi: `Adapty can only be activated once. Ensure that the SDK activation call is not made more than once.` Điều này xảy ra vì tính năng fast refresh của React Native kích hoạt nhiều lần gọi activation trong quá trình phát triển. Để ngăn điều này, hãy sử dụng tùy chọn `__ignoreActivationOnFastRefresh` được đặt thành `__DEV__` (cờ chế độ phát triển của React Native). ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __ignoreActivationOnFastRefresh: __DEV__, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` #### Thiết lập chế độ mock để kiểm tra cục bộ \{#set-up-mock-mode-for-local-testing\} Để phát triển và kiểm thử cục bộ, bạn có thể bật chế độ mock để không cần tài khoản sandbox App Store/Google Play và tăng tốc độ vòng lặp phát triển. Chế độ mock hoàn toàn bỏ qua các module native của Adapty và trả về dữ liệu giả lập. :::important Chế độ mock **không** phải là công cụ để kiểm thử giao dịch mua thực tế: - Nó **không mở** luồng mua hàng App Store / Google Play và **không tạo** giao dịch thực. - Nó **không render** paywall/onboarding được tạo bằng **Adapty Paywall Builder (AdaptyUI)**. - Các native module của Adapty bị **bỏ qua hoàn toàn**—ngay cả khi thiếu file native SDK trong bản build Xcode/Android hoặc API key không hợp lệ cũng sẽ không phát sinh lỗi. - Không có dữ liệu nào được gửi đến server của Adapty. Để kiểm tra mua hàng thực và paywall từ Paywall Builder, hãy tắt mock mode và sử dụng tài khoản sandbox. ::: Để bật mock mode, đặt `enableMock` thành `true`: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { enableMock: true, }); ``` Khi chế độ mock đang hoạt động: - Tất cả các phương thức của Adapty trả về dữ liệu mock mà không thực hiện bất kỳ yêu cầu mạng nào đến máy chủ của Adapty. - Theo mặc định, hồ sơ người dùng mock ban đầu không có gói đăng ký nào đang hoạt động. - Theo mặc định, `makePurchase(...)` mô phỏng một giao dịch mua thành công và cấp quyền truy cập premium. Bạn có thể tùy chỉnh dữ liệu mock bằng `mockConfig` trong quá trình kích hoạt. Xem định dạng cấu hình và các tham số được hỗ trợ [tại đây](https://react-native.adapty.io/interfaces/adaptymockconfig). ```typescript showLineNumbers title="App.tsx" try { await adapty.activate('YOUR_PUBLIC_SDK_KEY', { mockConfig: { // Customize the initial mock profile (optional) }, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); } ``` Nếu bạn cần gọi các phương thức SDK trước khi kích hoạt (chẳng hạn như `isActivated()` hoặc `setLogLevel()`), hãy sử dụng `enableMock()` trước `activate()`. Nếu bridge đã được khởi tạo, phương thức này sẽ không làm gì cả. ```typescript showLineNumbers title="App.tsx" adapty.enableMock(); // Optional: pass mockConfig to customize mock data // Now you can call methods before activation await adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` ## Xử lý sự cố \{#troubleshooting\} #### Lỗi phiên bản iOS tối thiểu \{#minimum-ios-version-error\} Nếu bạn gặp lỗi phiên bản iOS tối thiểu, hãy cập nhật Podfile: ```diff -platform :ios, min_ios_version_supported +platform :ios, '15.0' ``` #### Xung đột manifest Android Auto Backup \{#android-auto-backup-manifest-conflict\} 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 `<manifest>` có chứa tools: ```xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app"> ... </manifest> ``` #### 2. Ghi đè các thuộc tính backup trong `<application>` \{#2-override-backup-attributes-in-application\} Trong cùng file `AndroidManifest.xml`, cập nhật thẻ `<application>` để ứ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 <application android:name=".App" android:allowBackup="true" android:fullBackupContent="@xml/sample_backup_rules" android:dataExtractionRules="@xml/sample_data_extraction_rules" tools:replace="android:fullBackupContent,android:dataExtractionRules"> ... </application> ``` 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" <?xml version="1.0" encoding="utf-8"?> <data-extraction-rules> <cloud-backup> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </cloud-backup> <device-transfer> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </device-transfer> </data-extraction-rules> ``` **Dành cho Android 11 trở xuống** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" <?xml version="1.0" encoding="utf-8"?> <full-backup-content> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> #### Giao dịch mua thất bại sau khi quay lại từ ứng dụng khác trên Android \{#purchases-fail-after-returning-from-another-app-in-android\} Nếu Activity khởi động 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 nó không đúng cách khi người dùng quay lại từ Google Play, ứng dụng ngân hàng hoặc trình duyệt. Điều này có thể khiến kết quả giao dịch bị mất hoặc bị xử lý như đã hủy. Để đảm bảo quá trình mua hàng hoạt động đúng, hãy chỉ sử dụng chế độ khởi chạy `standard` hoặc `singleTop` cho Activity khởi động flow mua hàng, và tránh các chế độ khác. Trong file `AndroidManifest.xml`, đảm bảo Activity khởi động flow mua hàng được đặt thành `standard` hoặc `singleTop`: ```xml <activity android:name=".MainActivity" android:launchMode="standard" /> ``` #### 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 React Native cho iOS, bạn có thể gặp 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` không khớp 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 của bạn vẫn có thể dùng Swift 5 — chỉ các pod target của Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`, `AdaptyLogger`, `AdaptyPlugin`) mới cần build bằng 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 sửa**: Loại trừ các pod target của Adapty khỏi phần ghi đè: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| next if %w[Adapty AdaptyUI AdaptyUIBuilder AdaptyLogger AdaptyPlugin].include?(target.name) target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` Sau đó chạy `pod install` từ thư mục `ios/` và build lại. Để xác nhận, mở `ios/Pods/Pods.xcodeproj`, chọn target pod `Adapty` → **Build Settings** → **Swift Language Version**. Giá trị phải là **Swift 6**. --- # File: react-native-quickstart-paywalls --- --- title: "Bật tính năng mua hàng với Flow Builder trong React Native SDK" description: "Hướng dẫn nhanh để bật in-app purchase với Adapty Flow Builder." --- Để bật tính năng in-app purchase, bạn cần nắm ba khái niệm quan trọng: - [**Sản phẩm**](product) – những thứ người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flow**](adapty-flow-builder) – chuỗi màn hình hiển thị sản phẩm cho người dùng, được xây dựng trong Flow Builder không cần code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện trong code của mình, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](react-native-quickstart-manual). - [**Placement**](placements) – nơi và thời điểm bạn hiển thị flow trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trong dashboard, sau đó yêu cầu chúng theo placement ID trong code. Cách này giúp dễ dàng chạy A/B test và hiển thị các flow khác nhau cho từng nhóm người dùng. Adapty cung cấp cho bạn ba cách để tích hợp tính năng mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Triển khai | Độ phức tạp | Khi nào dùng | |---|---|---| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng mua hàng trong trình tạo không cần code](quickstart-paywalls). Adapty tự động render và xử lý toàn bộ flow mua hàng, xác thực receipt và quản lý gói đăng ký. | | Paywall tự tạo | 🟡 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 flow từ Adapty để duy trì tính linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](react-native-quickstart-manual). | | Observer mode | 🔴 Khó | Bạn đã có 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 observer mode có một số hạn chế trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước bên dưới hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, hãy xem [Triển khai paywall thủ công](react-native-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code của ứng dụng, bạn chỉ cần: 1. **Lấy flow**: Lấy nó từ Adapty. 2. **Hiển thị và Adapty sẽ xử lý việc mua hàng cho bạn**: Hiển thị view trong ứng dụng. 3. **Xử lý các hành động nút**: Liên kết tương tác của người dùng với phản hồi tương ứng trong ứng dụng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn các 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 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 Adapty SDK](sdk-installation-reactnative) trong code ứng dụng. :::tip Cách nhanh nhất để hoàn thành các bước này là làm theo [hướng dẫn quickstart](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy flow \{#1-get-the-flow\} Các flow của bạn được liên kết với các placement được cấu hình trên dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy một flow đã tạo trong Adapty Flow Builder, hãy lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow`. Flow chứa các phần tử UI và style cần thiết để hiển thị nó. ```typescript showLineNumbers title="React Native" try { const flow = await adapty.getFlow('YOUR_PLACEMENT_ID'); // the requested flow } catch (error) { // handle the error } ``` ## 2. Hiển thị flow \{#2-display-the-flow\} Bây giờ, khi bạn đã có flow, chỉ cần thêm vài dòng code là có thể hiển thị nó. <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Để nhúng flow vào cây component hiện có của bạn, hãy sử dụng component `AdaptyFlowView` trực tiếp trong cấu trúc component React Native của bạn: ```typescript showLineNumbers title="React Native (TSX)" function MyFlow({ flow }) { const onPurchaseCompleted = useCallback<FlowEventHandlers['onPurchaseCompleted']>( (result, product) => result.type !== 'user_cancelled', [], ); return ( <AdaptyFlowView flow={flow} style={{ flex: 1 }} onPurchaseCompleted={onPurchaseCompleted} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Để hiển thị flow dưới dạng màn hình độc lập, hãy tạo một `view` bằng phương thức `createFlowView`, thiết lập các trình xử lý sự kiện, rồi gọi `view.present()`. Mỗi `view` chỉ có thể dùng một lần. Nếu bạn cần hiển thị flow lại, hãy gọi `createFlowView` thêm một lần nữa để tạo instance `view` mới. ```typescript showLineNumbers title="React Native" try { const view = await createFlowView(flow); view.setEventHandlers({ onPurchaseCompleted(result, product) { return result.type !== 'user_cancelled'; }, }); await view.present(); } catch (error) { // handle the error } ``` </TabItem> </Tabs> :::tip Để biết thêm chi tiết về cách hiển thị một flow, xem [hướng dẫn](react-native-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động của nút \{#handle-button-actions\} Khi người dùng nhấn các nút trong flow, React Native SDK tự động xử lý việc mua hàng, khôi phục, đóng flow và mở URL. Tuy nhiên, một số nút có ID tùy chỉnh hoặc được định nghĩa sẵn và yêu cầu bạn xử lý hành động trong code. Hoặc bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ, đây là hành vi mặc định của nút đóng. Bạn không cần thêm đoạn code này, nhưng ở đây bạn có thể thấy cách thực hiện nếu cần. <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> For the React component, handle actions directly in the `AdaptyFlowView` component: ```typescript showLineNumbers title="React Native (TSX)" function MyFlow({ flow }) { const onCloseButtonPress = useCallback<FlowEventHandlers['onCloseButtonPress']>( () => true, // allow the flow to close [], ); const onCustomAction = useCallback<FlowEventHandlers['onCustomAction']>( (actionId) => false, [], ); return ( <AdaptyFlowView flow={flow} style={{ flex: 1 }} onCloseButtonPress={onCloseButtonPress} onCustomAction={onCustomAction} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Đối với modal presentation, hãy triển khai các trình xử lý sự kiện bằng `setEventHandlers`: ```typescript showLineNumbers title="React Native" const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; // allow the flow to close }, }); ``` </TabItem> </Tabs> :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [actions](react-native-handle-paywall-actions) và [events](react-native-handling-events-1) của nút. ::: ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra mua hàng trong [App Store sandbox](test-purchases-in-sandbox) hoặc trong [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử từ paywall. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](react-native-check-subscription-status) để đảm bảo bạn hiển thị paywall hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Dưới đây là cách tích hợp tất cả các bước trong hướng dẫn này vào ứng dụng của bạn. <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> ```javascript showLineNumbers title="React Native (TSX)" export default function FlowScreen() { const [flow, setFlow] = useState(null); const loadFlow = async () => { try { const flowData = await adapty.getFlow('YOUR_PLACEMENT_ID'); setFlow(flowData); } catch (error) { console.warn('Error loading flow:', error); } }; const onCloseButtonPress = useCallback<FlowEventHandlers['onCloseButtonPress']>( () => true, [], ); const onPurchaseCompleted = useCallback<FlowEventHandlers['onPurchaseCompleted']>( (result, product) => result.type !== 'user_cancelled', [], ); useEffect(() => { loadFlow(); }, []); return ( <View style={{ flex: 1 }}> {flow ? ( <AdaptyFlowView flow={flow} style={{ flex: 1 }} onCloseButtonPress={onCloseButtonPress} onPurchaseCompleted={onPurchaseCompleted} /> ) : ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Button title="Load Flow" onPress={loadFlow} /> </View> )} </View> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> ```javascript showLineNumbers title="React Native" export default function FlowScreen() { const showFlow = async () => { try { const flow = await adapty.getFlow('YOUR_PLACEMENT_ID'); const view = await createFlowView(flow); view.setEventHandlers({ onCloseButtonPress() { return true; }, onPurchaseCompleted(result, product) { return result.type !== 'user_cancelled'; }, }); await view.present(); } catch (error) { // handle any error that may occur during the process console.warn('Error showing flow:', error); } }; // you can add a button to manually trigger the flow for testing purposes return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Button title="Show Flow" onPress={showFlow} /> </View> ); } ``` </TabItem> </Tabs> --- # File: react-native-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong React Native SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng React Native 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 hay không, 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 đọc trạng thái hồ sơ người dùng để quyết định nên hiển thị gì — 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ọ. Bạn có hai lựa chọn: - Gọi `getProfile` nếu cần dữ liệu hồ sơ mới nhất ngay lập tức (ví dụ khi khởi chạy ứng dụng) hoặc muốn ép 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`: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); } catch (error) { // handle the error } ``` ### Lắng nghe cập nhật gói đăng ký \{#listen-to-subscription-updates\} Để tự động nhận cập nhật hồ sơ trong ứng dụng: 1. Dùng `adapty.addEventListener('onLatestProfileLoad')` để 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 các yêu cầu mạng. ```javascript class SubscriptionManager { private currentProfile: any = null; constructor() { // Listen for profile updates adapty.addEventListener('onLatestProfileLoad', (profile) => { this.currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() hasAccess(): boolean { return this.currentProfile?.accessLevels?.['premium']?.isActive ?? false; } } ``` :::note Adapty tự động gọi event listener `onLatestProfileLoad` khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký đã được cache ngay cả khi thiết bị offline. ::: ## 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 vào tính năng trả phí, bạn có thể kiểm tra hồ sơ người dùng trực tiếp. Cách tiếp cận này hữu ích cho các tình huống như khi khởi chạy ứng dụng, khi vào các phần premium, hoặc trước khi hiển thị nội dung cụ thể. ```javascript const checkAccessLevel = async () => { try { const profile = await adapty.getProfile(); return profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive === true; } catch (error) { console.warn('Error checking access level:', error); return false; // Show paywall if access check fails } }; const initializePaywall = async () => { try { await loadPaywall(); const hasAccess = await checkAccessLevel(); if (!hasAccess) { // Show paywall if no access } } catch (error) { console.warn('Error initializing paywall:', error); } }; ``` ## Bước tiếp theo \{#next-steps\} Bây giờ khi bạn đã biết cách theo dõi trạng thái gói đăng ký, hãy tìm hiểu cách [làm việc với hồ sơ người dùng](react-native-quickstart-identify) để đảm bảo họ có thể truy cập những gì đã thanh toán. --- # File: react-native-quickstart-identify --- --- title: "Xác định người dùng trong React Native SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký in-app trong React Native." --- :::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ó phù hợp với hệ thống xác thực hiện có của bạn. ::: Cách bạn quản lý 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 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ó (hoặc sẽ có) xác thực backend, xem [phần về người dùng đã xác định](#identified-users). **Các khái niệm chính**: - **Hồ sơ người dùng** là các thực thể cần thiết để SDK hoạt động. Adapty tự động tạo chúng. - Chúng có thể là ẩn danh **(không có customer user ID)** hoặc đã xác định **(có customer user ID)**. - Bạn cung cấp **customer user ID** để đối chiếu hồ sơ người dùng trong Adapty với hệ thống xác thực nội bộ của bạn. Dưới đây là sự khác biệt giữa người dùng ẩn danh và người dùng đã xác định: | | Người dùng ẩn danh | Người dùng đã xác định | |-------------------------|------------------------------------------------------|-------------------------------------------------------------------------------| | **Quản lý 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 các thiết bị thông qua customer user ID | | **Quản lý hồ sơ người dùng** | Hồ sơ mới sau mỗi lần cài đặt lại | Cùng một hồ sơ người dùng trên các 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 khởi chạy ứng dụng, Adapty **tạo một hồ sơ người dùng mới**. 2. Khi người dùng mua bất cứ thứ gì trong ứng dụng, giao dịch mua này được **liên kết với hồ sơ Adapty và tài khoản cửa hàng của họ**. 3. Khi người dùng **cài đặt lại** ứng dụng hoặc cài đặt từ **thiết bị mới**, Adapty **tạo một hồ sơ ẩn danh mới khi kích hoạt**. 4. Nếu người dùng đã thực hiện giao dịch mua trước đó trong ứng dụng của bạn, theo mặc định, các giao dịch mua của họ sẽ tự động được đồng bộ từ App Store khi SDK 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 phần analytics của Adapty, bạn có thể [cấu hình những gì sẽ được tính là lần cài đặt mới](general#4-installs-definition-for-analytics). :::note Khôi phục từ bản sao lưu hoạt động khác với cài đặt lại. Theo mặc định, khi người dùng khôi phục từ bản sao lưu, SDK giữ nguyên dữ liệu đã cache và không tạo hồ sơ mới. Bạn có thể cấu hình hành vi này bằng cài đặt `clearDataOnBackup`. [Tìm hiểu thêm](sdk-installation-react-native-pure#clear-data-on-backup-restore). ::: Đối với người dùng ẩn danh, bạn cần đếm lượt 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 một thiết bị được tính là một lượt 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 một giao dịch mua từ một Customer User ID hiện đang được liên kết với Customer User ID khác, mức độ truy cập sẽ được chia sẻ, vì vậy cả hai hồ sơ đều có quyền truy cập trả phí. Bạn có thể cấu hình cài đặt này để chuyển quyền truy cập trả phí từ hồ sơ này sang hồ sơ khác hoặc vô hiệu hóa 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. ::: <img src="/assets/shared/img/identify-diagram.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Trong quá trình đăng nhập/đăng ký \{#during-loginsignup\} Nếu bạn đang xác định người dùng sau khi ứng dụng khởi chạy (ví dụ: sau khi họ đăng nhập vào ứng dụng hoặc đăng ký), hãy sử dụng phương thức `identify` để đặt customer user ID của họ. - Nếu bạn **chưa sử dụng customer user ID này trước đây**, Adapty sẽ tự động liên kết nó với hồ sơ hiện tại. - Nếu bạn **đã sử dụng customer user ID này để xác định người dùng trước đây**, Adapty sẽ chuyển sang làm việc với hồ sơ được liên kết với customer user ID này. :::important Customer user ID phải là duy nhất cho mỗi người dùng. Nếu bạn hardcode giá trị tham số, tất cả người dùng sẽ được coi là một người. ::: Luôn `await` `identify` trước khi gọi các phương thức SDK khác. Các lần gọi đồng thời sẽ tạo ra `#3006 profileWasChanged` hoặc rơi vào hồ sơ ẩn danh. Xem [Thứ tự gọi trong React Native SDK](react-native-sdk-call-order). ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // successfully identified } catch (error) { // handle the error } ``` ### Trong quá trình kích hoạt SDK \{#during-the-sdk-activation\} Nếu bạn đã biết customer user ID khi kích hoạt SDK, bạn có thể gửi nó trong phương thức `activate` thay vì gọi `identify` riêng biệt. Nếu bạn biết customer user ID nhưng chỉ đặt nó sau khi kích hoạt, điều đó có nghĩa là khi kích hoạt, Adapty sẽ tạo một hồ sơ ẩn danh mới và chỉ chuyển sang hồ sơ hiện có sau khi bạn gọi `identify`. Bạn có thể truyền customer user ID hiện có (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ì lượt cài đặt được đếm dựa trên device ID. Device ID đại diện cho một lần cài đặt ứng dụng từ cửa hàng trên một thiết bị và chỉ được tạo lại sau khi ứng dụng được cài đặt lại. Nó không phụ thuộc vào việc đây là lần cài đặt đầu tiên hay lần cài đặt lại, hoặc liệu có sử dụng customer user ID hiện có hay không. Việc tạo hồ sơ (khi kích hoạt SDK hoặc đăng xuất), đăng nhập, hoặc nâng cấp ứng dụng mà không cài đặt lại không tạo ra các sự kiện cài đặt bổ sung. Nếu bạn muốn đếm lượt 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). ::: ```typescript showLineNumbers adapty.activate("PUBLIC_SDK_KEY", { customerUserId: "YOUR_USER_ID" // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. }); ``` ### Đă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 đó. ::: ```typescript showLineNumbers try { await adapty.logout(); // successful logout } catch (error) { // handle the 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 không cần đăng nhập \{#allow-purchases-without-login\} Nếu người dùng của bạn có thể mua hàng 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ẽ 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 gắn 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, 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 gán customer user ID cho hồ sơ hiện tại, vì vậy toàn bộ lịch sử giao dịch mua được duy trì. - Nếu đây là customer user ID đã tồn tại (customer user ID đã được liên kết với một hồ sơ), bạn cần lấy mức độ truy cập thực tế sau khi chuyển đổi hồ sơ. Bạn có thể gọi [`getProfile`](react-native-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe các cập nhật hồ sơ](react-native-check-subscription-status) để dữ liệu tự động đồng bộ. ## Bước tiếp theo \{#next-steps\} Xin chúc mừng! Bạn đã triển khai logic thanh toán in-app trong ứng dụng của mình! Chúc bạn thành công với việc kiếm tiền từ ứng dụng! Để khai thác Adapty tốt hơn nữa, 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**](react-native-onboardings): Thu hút người dùng với onboarding và thúc đẩy giữ chân người dùng - [**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 - [**Đặt thuộc tính hồ sơ tùy chỉnh**](react-native-setting-user-attributes): Thêm thuộc tính tùy chỉnh vào hồ sơ người dùng và tạo phân khúc, để bạn có thể chạy A/B test hoặc hiển thị các paywall khác nhau cho các nhóm người dùng khác nhau --- # File: adapty-sdk-integration-skill-react-native --- --- title: "Tích hợp Adapty vào ứng dụng React Native 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 React Native của bạn từ đầu đến cuối với công cụ AI coding." --- :::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-react-native) — hướng dẫn này sẽ dẫn dắt công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn form phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt xong, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-react-native --- --- title: "Tích hợp Adapty vào ứng dụng React Native 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 React Native 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ẽ dẫn bạn qua từng bước tích hợp Adapty vào ứng dụng React Native của bạn với sự hỗ trợ của công cụ AI — bạn chỉ cần cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập trên 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ỳ mã SDK nào. Bạn có thể thực hiện điều này thông qua một skill LLM tương tác, hoặc thủ công qua Dashboard. ### Cách dùng Skill (khuyến nghị) \{#skill-approach-recommended\} Skill Adapty CLI cho phép LLM của bạn thiết lập ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng của mình](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm skill, chạy `/adapty-cli` trong agent của bạn. Nó sẽ hướng dẫn bạn qua từng bước — kể cả khi nào cần mở Dashboard để kết nối 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ứ, dưới đây là những gì bạn cần trước khi viết bất kỳ mã nào. LLM của bạn không thể tra cứu các giá trị từ dashboard — bạn sẽ phải cung cấp chúng. 1. **Kết nối cửa hàng ứng dụng của bạn**: Trong Adapty Dashboard, vào **App settings → General**. Kết nối cả App Store và Google Play nếu ứng dụng của bạn nhắm đến cả hai nền tảng. Đây là điều kiện bắt buộc để mua hàng hoạt động. [Kết nối cửa hàng ứng dụng](integrate-payments) 2. **Sao chép Public SDK key của bạn**: Trong Adapty Dashboard, vào **App settings → General**, sau đó tìm phần **API keys**. Trong mã, đây là chuỗi bạn truyền vào `adapty.activate("YOUR_PUBLIC_SDK_KEY")`. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu sản phẩm trực tiếp trong mã — Adapty phân phối 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ó cho một placement trên trang **Placements**. Trong mã, placement ID là chuỗi bạn truyền vào `adapty.getPaywall("YOUR_PLACEMENT_ID")`. [Tạo paywall](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình theo từng sản phẩm trên trang **Products**. Trong mã, đây là 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 truy cập 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 mã. :::tip Khi bạn có đủ năm mục trên, bạn đã sẵn sàng viết mã. Hãy cho LLM của bạn biết: "Public SDK key của tôi là X, placement ID của tôi là Y" để nó có thể tạo mã 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 mã, 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 mã. [A/B test](ab-tests) - **Thêm paywall và placement**: Thêm nhiều 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 tùy theo từng 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 cho phép LLM của bạ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 sẽ tự động lấy đúng tài liệu dựa trên câu hỏi của bạn — 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 sẽ 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 [repository GitHub của Context7](https://github.com/upstash/context7). Sau khi cấu hình xong, tham chiếu thư viện Adapty trong prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the React Native SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn rất 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 văn bản thuần \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng Markdown thuần. Thêm `.md` vào cuối URL của nó, hoặc nhấp **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor-react-native.md](https://adapty.io/docs/vi/adapty-cursor-react-native.md). Mỗi giai đoạn trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới đều có một khối "Gửi cho LLM của bạn" với các link `.md` để dán vào. Để có thêm tài liệu cùng lúc, xem [file index và các tập con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này sẽ đi qua việc tích hợp Adapty theo đúng thứ tự triển khai. Mỗi giai đoạn bao gồm tài liệu cần gửi cho LLM, kết quả mong đợi khi hoàn thành và các vấn đề thường gặp. ### Lập kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết mã, hãy yêu cầu LLM của bạn phân tích dự á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 của bạn lẫn tài liệu Adapty trước khi viết bất kỳ mã nào. Hãy cho LLM biết cách bạn xử lý mua hàng — điều này ảnh hưởng đến các hướng dẫn nó cần theo: - [**Adapty Paywall Builder**](adapty-paywall-builder): Bạn tạo paywall trong trình tạo no-code của Adapty, và SDK sẽ hiển thị chúng tự động. - [**Paywall tự tạo**](react-native-making-purchases): Bạn tự xây dựng giao diện paywall trong mã nhưng vẫn dùng Adapty để lấy sản phẩm và xử lý mua hàng. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên cơ sở hạ tầng mua hàng hiện có và chỉ dùng Adapty cho analytics và tích hợp. Chưa chắc nên chọn cái nào? Đọc [bảng so sánh trong quickstart](react-native-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm dependency Adapty SDK bằng npm (hoặc yarn) và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — mọi thứ khác đều không hoạt động nếu thiếu bước này. Chúng tôi có hướng dẫn cài đặt riêng cho dự án Expo và bare React Native — hãy chọn cái phù hợp với thiết lập của bạn. **Hướng dẫn:** - [Cài đặt với Expo](sdk-installation-react-native-expo) - [Cài đặt với bare React Native](sdk-installation-react-native-pure) Gửi cho LLM của bạn (chọn cái phù hợp với thiết lập của bạn, hoặc gửi cả hai): ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-react-native-expo.md - https://adapty.io/docs/vi/sdk-installation-react-native-pure.md ``` :::tip[Kiểm tra] - **Kết quả mong đợi:** Ứng dụng build và chạy được trên cả iOS và Android. Log của Metro bundler 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ật từ App settings chưa. ::: ### Hiển thị paywall và xử lý mua hàng \{#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ý mua hàng. Hãy kiểm tra từng giao dịch trong sandbox khi thực hiện — đừng đợi đến cuối. Xem [Kiểm tra mua hàng trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. <Tabs groupId="paywall-approach"> <TabItem value="builder" label="Paywall Builder" default> **Hướng dẫn:** - [Bật mua hàng bằng paywall (quickstart)](react-native-quickstart-paywalls) - [Lấy paywall từ Paywall Builder và cấu hình của chúng](react-native-get-pb-paywalls) - [Hiển thị paywall](react-native-present-paywalls) - [Xử lý sự kiện paywall](react-native-handling-events-1) - [Phản hồi các thao tác nút](react-native-handle-paywall-actions) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/react-native-quickstart-paywalls.md - https://adapty.io/docs/vi/react-native-get-pb-paywalls.md - https://adapty.io/docs/vi/react-native-present-paywalls.md - https://adapty.io/docs/vi/react-native-handling-events-1.md - https://adapty.io/docs/vi/react-native-handle-paywall-actions.md ``` :::tip[Kiểm tra] - **Kết quả mong đợi:** Paywall xuất hiện với các sản phẩm bạn đã cấu hình. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Paywall trống hoặc lỗi `getPaywall` → xác minh placement ID khớp chính xác với dashboard và placement có đối tượng được gán. ::: </TabItem> <TabItem value="manual" label="Paywall tự tạo"> **Hướng dẫn:** - [Bật mua hàng trong paywall tùy chỉnh của bạn (quickstart)](react-native-quickstart-manual) - [Lấy paywall và sản phẩm](fetch-paywalls-and-products-react-native) - [Hiển thị paywall được thiết kế bằng remote config](present-remote-config-paywalls-react-native) - [Thực hiện mua hàng](react-native-making-purchases) - [Khôi phục mua hàng](react-native-restore-purchase) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/react-native-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products-react-native.md - https://adapty.io/docs/vi/present-remote-config-paywalls-react-native.md - https://adapty.io/docs/vi/react-native-making-purchases.md - https://adapty.io/docs/vi/react-native-restore-purchase.md ``` :::tip[Kiểm tra] - **Kết quả mong đợi:** Paywall tùy chỉnh của bạn hiển thị các sản phẩm lấy từ Adapty. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Mảng sản phẩm trống → xác minh paywall đã có sản phẩm được gán trong dashboard và placement có đối tượng. ::: </TabItem> <TabItem value="observer" label="Observer mode"> **Hướng dẫn:** - [Tổng quan về Observer mode](observer-vs-full-mode) - [Triển khai Observer mode](implement-observer-mode-react-native) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode-react-native) Gửi 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-react-native.md - https://adapty.io/docs/vi/report-transactions-observer-mode-react-native.md ``` :::tip[Kiểm tra] - **Kết quả mong đợi:** Sau khi mua hàng trong sandbox bằng flow mua hàng hiện có của bạn, giao dịch sẽ xuất hiện trong **Event Feed** trên Adapty dashboard. - **Lưu ý:** Không có sự kiện → xác minh bạn đang báo cáo giao dịch cho Adapty và thông báo server đã được cấu hình cho cả hai cửa hàng. ::: </TabItem> </Tabs> ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua hàng, kiểm tra hồ sơ người dùng để xem có mức độ truy cập đang hoạt động hay không nhằm giới hạn nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](react-native-check-subscription-status) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/react-native-check-subscription-status.md ``` :::tip[Kiểm tra] - **Kết quả mong đợi:** Sau khi mua hàng trong sandbox, `profile.accessLevels['premium']?.isActive` trả về `true`. - **Lưu ý:** `accessLevels` trống sau khi mua → kiểm tra sản phẩm đã được gán mức độ truy cập trong dashboard chưa. ::: ### Xác định người dùng \{#identify-users\} Liên kết tài khoản người dùng trong ứng dụng của bạn với hồ sơ Adapty để mua hàng được duy trì trên nhiều thiết bị. :::important Bỏ qua bước này nếu ứng dụng của bạn không có xác thực. ::: **Hướng dẫn:** [Xác định người dùng](react-native-quickstart-identify) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/react-native-quickstart-identify.md ``` :::tip[Kiểm tra] - **Kết quả mong đợi:** Sau khi gọi `adapty.identify("your-user-id")`, phần **Profiles** trong dashboard sẽ hiển thị ID người dùng tùy chỉnh của bạn. - **Lưu ý:** Gọi `identify` sau khi kích hoạt nhưng trước khi lấy paywall để tránh attribution hồ sơ ẩn danh. ::: ### Chuẩn bị phát hành \{#prepare-for-release\} Sau khi tích hợp hoạt động trong sandbox, hãy xem qua danh sách kiểm tra phát hành để đảm bảo mọi thứ sẵn sàng cho môi trường production. **Hướng dẫn:** [Danh sách kiểm tra phát hành](release-checklist) Gửi cho LLM của bạn: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/vi/release-checklist.md ``` :::tip[Kiểm tra] - **Kết quả mong đợi:** Tất cả các mục trong danh sách đã được xác nhận: kết nối cửa hàng, thông báo server, flow mua hàng, kiểm tra mức độ truy cập và yêu cầu về quyền riêng tư. - **Lưu ý:** Thiếu 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**. ::: ## File index tài liệu dạng văn bản thuần \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM ngữ cảnh rộng hơn ngoài các trang riêng lẻ, chúng tôi có các file index liệt kê hoặc kết hợp toàn bộ tài liệu Adapty: - [`llms.txt`](https://adapty.io/docs/vi/llms.txt): Liệt kê tất cả các trang với các link `.md`. Đây là [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho các trang web có thể truy cập được bởi LLM. Lưu ý rằng với một số AI agent (ví dụ: ChatGPT), bạn sẽ cần tải `llms.txt` xuống 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 kết hợp thành một file duy nhất. Rất lớn — chỉ dùng khi bạn cần toàn bộ bức tranh. - Các file dành riêng cho React Native [`react-native-llms.txt`](https://adapty.io/docs/vi/react-native-llms.txt) và [`react-native-llms-full.txt`](https://adapty.io/docs/vi/react-native-llms-full.txt): Các tập con theo nền tảng giúp tiết kiệm token so với toàn bộ trang. --- # File: react-native-get-pb-paywalls --- --- title: "Lấy flows & paywalls - React Native" description: "Lấy flows và paywalls từ Adapty trong ứng dụng React Native của bạn." --- <SDKv4> <MethodPromo method="getFlow" /> Sau khi [bạn đã thiết kế flow hoặc paywall bằng Paywall Builder](adapty-paywall-builder), bạn có thể hiển thị chúng trong ứng dụng di động của mình. Bước đầu tiên là lấy flow hoặc paywall được liên kết với placement cùng cấu hình hiển thị của nó như mô tả bên dưới. Lưu ý rằng chủ đề này đề cập đến flow và các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall theo cách thủ công, vui lòng tham khảo chủ đề [Lấy paywall và sản phẩm cho remote config paywall trong ứng dụng di động của bạn](fetch-paywalls-and-products-react-native). :::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. ::: <details> <summary>Trước khi bắt đầu hiển thị flow và paywall trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow/paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-reactnative) trong ứng dụng mobile của bạn. </details> ## Lấy flow/paywall \{#fetch-flowpaywall\} Nếu bạn đã thiết kế một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới trình bày nó trong ứng dụng. Tải flow hoặc paywall và tạo [view](react-native-get-pb-paywalls#fetch-the-view-configuration) của nó càng sớm càng tốt — lý tưởng là trước khi hiển thị. Phương thức `createFlowView` tải cấu hình view và bắt đầu tải xuống, cache hình ảnh ở chế độ nền. Bạn gọi nó càng sớm thì các hình ảnh đó càng có nhiều thời gian để hoàn tất. Đến khi hiển thị flow hoặc paywall, cấu hình và hình ảnh đã được cache sẵn sàng để hiển thị. Để lấy flow hoặc paywall, dùng phương thức `getFlow`: ```typescript showLineNumbers try { const placementId = 'YOUR_PLACEMENT_ID'; const flow = await adapty.getFlow(placementId); // flow/paywall được yêu cầu } catch (error) { // xử lý lỗi } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |-------------------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng thường gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối mạng của họ ra sao. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi dùng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>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 cài đặt lại ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không 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 paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối mạng kém.</p> | | **loadTimeoutMs** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p><p>Đố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` được lấy từ `import com.adapty.utils.seconds`), hoặc `TimeInterval.seconds(5)`. Để không giới hạn thời gian chờ, hãy dùng `TimeInterval.INFINITE`.</p> | ## Tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Flow | Một đối tượng `AdaptyFlow` chứa các mã định danh của flow (`id`, `variationId`), tên, placement, các biến thể paywall (`paywalls`) và bất kỳ Remote Config nào (`remoteConfigs`). | ## Lấy cấu hình view \{#fetch-the-view-configuration\} :::important Hãy đảm bảo bật toggle **Show on device** trong builder. Nếu tùy chọn này chưa được bật, cấu hình view sẽ không thể lấy được. ::: Nếu placement được thiết kế trong **Flow Builder** hoặc **Paywall Builder**, Adapty sẽ tự động render giao diện cho bạn. Tạo view bằng `createFlowView`, sau đó [hiển thị flow hoặc paywall](react-native-present-paywalls). Nếu placement là một paywall tùy chỉnh không có giao diện Builder, hãy [xử lý nó như một paywall Remote Config](present-remote-config-paywalls-react-native). Trong React Native SDK, hãy gọi `createFlowView` trực tiếp — bạn không cần fetch cấu hình view trước. :::warning Kết quả của phương thức `createFlowView` chỉ có thể được sử dụng một lần. Nếu cần dùng lại, hãy gọi lại phương thức `createFlowView` từ đầu. Gọi hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```typescript showLineNumbers try { const view = await createFlowView(flow); } catch (error) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :------------------- | :------- | :----------------------------------------------------------- | | **flow** | bắt buộc | Đối tượng `AdaptyFlow` để lấy controller cho flow/paywall mong muốn. | | **customTags** | tùy chọn | Định nghĩa một dictionary các custom tag và giá trị tương ứng. Custom tag đóng vai trò là placeholder trong nội dung, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong flow/paywall. Tham khảo chủ đề Custom tags in paywall builder để biết thêm chi tiết. | | **prefetchProducts** | tùy chọn | Bật để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Khi đặt là `true`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. Mặc định: `false`. | | **android.enableSafeArea** | tùy chọn | Chỉ dành cho Android (bỏ qua trên iOS). Truyền dưới dạng đối tượng lồng nhau: `android: { enableSafeArea: true }`. Khi đặt là `true`, flow view sẽ áp dụng padding safe-area. Mặc định là `true` cho kiểu trình bày modal (`createFlowView` + `present()`) và `false` cho component `AdaptyFlowView` nhúng. Giá trị mặc định phù hợp với hầu hết các trường hợp. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa flow](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã ngôn ngữ đúng cách [tại đây](react-native-localizations-and-locale-codes). ::: Sau khi có view, hãy [hiển thị flow/paywall](react-native-present-paywalls). ## Lấy flow hoặc paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, flow và paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow hoặc paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị flow hoặc paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow hoặc paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow hoặc paywall bằng phương thức `getFlow`, như đã mô tả chi tiết trong phần [Lấy flow/paywall](#fetch-flowpaywall) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng nhắm mục tiêu cá nhân hóa (bao gồm 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 để đổi lấy tốc độ tải flow hoặc paywall nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` như đã mô tả [ở trên](#fetch-flowpaywall). ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const flow = await adapty.getFlowForDefaultAudience(id); // the requested flow/paywall } catch (error) { // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn, bất kể kết nối mạng có chập chờn đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong suốt phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong flow/paywall của bạn, hãy triển khai custom assets. Hero image và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác nhau cho một số người dùng. - Hiển thị hình ảnh 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 phát video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty React Native SDK lên phiên bản 3.8.0 trở lên. ::: Dưới đây là ví dụ về cách cung cấp custom assets thông qua một dictionary đơn giản: ```javascript const customAssets: Record<string, AdaptyCustomAsset> = { 'custom_image': { type: 'image', relativeAssetPath: 'custom_image.png' }, 'hero_video': { type: 'video', fileLocation: { ios: { fileName: 'custom_video.mp4' }, android: { relativeAssetPath: 'videos/custom_video.mp4' } } } }; view = await createFlowView(flow, { customAssets }) ``` :::note Nếu không tìm thấy asset, flow/paywall sẽ quay về giao diện mặc định của nó. ::: </SDKv4> <SDKv3> 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 của mình. Bước đầu tiên trong quá trình này là lấy paywall được liên kết với placement và cấu hình view của nó như mô tả bên dưới. :::warning Paywall Builder mới hoạt động với React Native SDK phiên bản 3.0 trở lên. ::: Vui lòng lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall theo cách thủ công, hãy tham khảo chủ đề [Lấy paywall và sản phẩm cho Remote Config paywall trong ứng dụng mobile của bạn](fetch-paywalls-and-products-react-native). :::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. ::: <details> <summary>Trước khi bắt đầu hiển thị paywall trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-reactnative) vào ứng dụng di động của bạn. </details> ## 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. Loại paywall này chứa đầy đủ cả nội dung hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới hiển thị nó trong ứng dụng. Để đảm bảo hiệu suất tối ưu, điều quan trọng là phải lấy paywall và [cấu hình view](react-native-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ằm dành đủ thời gian để tải ảnh trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `getPaywall`: ```typescript showLineNumbers try { const placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywall(placementId, locale); // paywall được yêu cầu } catch (error) { // xử lý lỗi } ``` 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này phải là mã ngôn ngữ gồm một hoặc hai subtag được phân tách bằng ký tự gạch ngang (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet có kém đến đâu. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một server 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 paywall, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối internet không ổn định.</p> | | **loadTimeoutMs** | mặc định: 5 giây | <p>Giá trị này giới hạn thời gian timeout cho phương thức này. Nếu đạt đến giới hạn timeout, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p><p>Đối với Android: Bạn có thể tạo `TimeInterval` bằng các extension function (như `5.seconds`, trong đó `.seconds` đến từ `import com.adapty.utils.seconds`), hoặc `TimeInterval.seconds(5)`. Để không giới hạn, hãy dùng `TimeInterval.INFINITE`.</p> | ## Tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | Đối tượng [`AdaptyPaywall`](https://react-native.adapty.io/interfaces/adaptypaywall) chứa danh sách ID sản phẩm, mã định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình hiển thị của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bật toggle **Show on device** trong Paywall Builder. Nếu tùy chọn này chưa được bật, cấu hình hiển thị sẽ không thể lấy được. ::: Sau khi lấy paywall, hãy kiểm tra xem nó có chứa `ViewConfiguration` hay không — đây là dấu hiệu cho biết paywall được tạo bằng Paywall Builder. Điều này sẽ giúp bạn xác định cách hiển thị paywall. Nếu có `ViewConfiguration`, hãy xử lý nó như một Paywall Builder paywall; nếu không, [xử lý nó như một Remote Config paywall](present-remote-config-paywalls-react-native). Trong React Native SDK, hãy gọi trực tiếp phương thức `createPaywallView` mà không cần tự lấy view configuration trước. :::warning Kết quả của phương thức `createPaywallView` chỉ có thể được sử dụng một lần. Nếu bạn cần sử dụng lại, hãy gọi lại phương thức `createPaywallView` từ đầu. Việc gọi hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```typescript showLineNumbers // for the Adapty SDK < 3.14 – import {createPaywallView} from 'react-native-adapty/dist/ui'; if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { //use your custom logic } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :------------------- | :------- | :----------------------------------------------------------- | | **paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **customTags** | tùy chọn | Định nghĩa một dictionary gồm các custom tag và giá trị tương ứng. Custom tag đóng vai trò là placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Xem thêm trong phần Custom tags in paywall builder để biết thêm chi tiết. | | **prefetchProducts** | tùy chọn | Bật tùy chọn này để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Khi đặt là `true`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. Mặc định: `false`. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](react-native-localizations-and-locale-codes). ::: Sau khi có view, hãy [hiển thị paywall](react-native-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng của bạn có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong đợi. 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 cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement đã chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị vẫn là lấy paywall bằng phương thức `getPaywall`, như đã trình bày chi tiết trong phần [Lấy thông tin Paywall](#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ể: - **Các vấn đề về khả năng 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ế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng đang dùng phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không hiển thị được. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế đó để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` được mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder). ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywallForDefaultAudience(id, locale); // the requested paywall } catch (error) { // handle the error } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ React Native SDK phiên bản 2.11.2 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này phải là mã ngôn ngữ gồm một hoặc nhiều thẻ phụ được phân tách bằng dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Bản địa hóa và mã ngôn ngữ](react-native-localizations-and-locale-codes) để biết thêm thông tin về mã ngôn ngữ và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng kết nối không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu 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 mạng không ổn định. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets. Hình ảnh và video hero có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ remote đang tải. - Hiển thị hình ảnh xem trước trước khi chạy video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty React Native SDK lên phiên bản 3.8.0 trở lên. ::: Dưới đây là ví dụ về cách cung cấp custom assets thông qua một dictionary đơn giản: ```javascript const customAssets: Record<string, AdaptyCustomAsset> = { 'custom_image': { type: 'image', relativeAssetPath: 'custom_image.png' }, 'hero_video': { type: 'video', fileLocation: { ios: { fileName: 'custom_video.mp4' }, android: { relativeAssetPath: 'videos/custom_video.mp4' } } } }; view = await createPaywallView(paywall, { customAssets }) ``` :::note Nếu không tìm thấy asset, paywall sẽ trở về giao diện mặc định của nó. ::: </SDKv3> --- # File: react-native-present-paywalls --- --- title: "Hiển thị flows & paywalls - React Native" description: "Hiển thị flows và paywalls cho người dùng trong ứng dụng React Native của bạn với Adapty." --- <SDKv4> <MethodPromo method="getFlow" label="Hiển thị flows và paywalls" /> Nếu bạn đã tạo một flow hoặc paywall trong Flow Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow đó đã bao gồm cả nội dung 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 đã [tạo một flow hoặc paywall](create-paywall). 2. Bạn đã thêm nó vào một [placement](placements). 3. Bạn đã [tải flow và chuẩn bị view](react-native-get-pb-paywalls). :::warning Hướng dẫn này chỉ dành cho **flow và paywall sử dụng Paywall Builder**, yêu cầu SDK v4.0 trở lên. Quy trình hiển thị flow khác với paywall remote config. - Để hiển thị **paywall remote config**, xem [Hiển thị paywall thiết kế bằng remote config](present-remote-config-paywalls). ::: Adapty React Native SDK cung cấp hai cách để hiển thị flow và paywall: - **React component**: Component nhúng cho phép tích hợp vào kiến trúc và hệ thống điều hướng của ứng dụng. - **Modal presentation** ## Component React \{#react-component\} Để nhúng một flow vào cây component hiện có của bạn, hãy sử dụng component `AdaptyFlowView` trực tiếp trong phân cấp component React Native. Component nhúng này cho phép bạn tích hợp nó vào kiến trúc và hệ thống điều hướng của ứng dụng. :::tip Component `AdaptyFlowView` tạo view khi render, tức là khi cấu hình và hình ảnh được tải xuống. Để tải trước, hãy gọi [`createFlowView`](react-native-get-pb-paywalls#fetch-the-view-configuration) cho cùng một flow ở phần trước của ứng dụng. Component sẽ tái sử dụng dữ liệu đã cache và render mà không cần chờ tải xuống. ::: ```typescript showLineNumbers title="React Native (TSX)" function MyFlow({ flow }) { const flowParams = useMemo(() => ({ loadTimeoutMs: 3000, }), []); const onCloseButtonPress = useCallback<FlowEventHandlers['onCloseButtonPress']>(() => {}, []); const onProductSelected = useCallback<FlowEventHandlers['onProductSelected']>((productId) => {}, []); const onPurchaseStarted = useCallback<FlowEventHandlers['onPurchaseStarted']>((product) => {}, []); const onPurchaseCompleted = useCallback<FlowEventHandlers['onPurchaseCompleted']>((purchaseResult, product) => {}, []); const onPurchaseFailed = useCallback<FlowEventHandlers['onPurchaseFailed']>((error, product) => {}, []); const onRestoreStarted = useCallback<FlowEventHandlers['onRestoreStarted']>(() => {}, []); const onRestoreCompleted = useCallback<FlowEventHandlers['onRestoreCompleted']>((profile) => {}, []); const onRestoreFailed = useCallback<FlowEventHandlers['onRestoreFailed']>((error) => {}, []); const onAppeared = useCallback<FlowEventHandlers['onAppeared']>(() => {}, []); const onError = useCallback<FlowEventHandlers['onError']>((error) => {}, []); const onLoadingProductsFailed = useCallback<FlowEventHandlers['onLoadingProductsFailed']>((error) => {}, []); const onUrlPress = useCallback<FlowEventHandlers['onUrlPress']>((url) => {}, []); const onCustomAction = useCallback<FlowEventHandlers['onCustomAction']>((actionId) => {}, []); const onWebPaymentNavigationFinished = useCallback<FlowEventHandlers['onWebPaymentNavigationFinished']>(() => {}, []); return ( <AdaptyFlowView flow={flow} params={flowParams} style={styles.flow} onCloseButtonPress={onCloseButtonPress} onProductSelected={onProductSelected} onPurchaseStarted={onPurchaseStarted} onPurchaseCompleted={onPurchaseCompleted} onPurchaseFailed={onPurchaseFailed} onRestoreStarted={onRestoreStarted} onRestoreCompleted={onRestoreCompleted} onRestoreFailed={onRestoreFailed} onAppeared={onAppeared} onError={onError} onLoadingProductsFailed={onLoadingProductsFailed} onCustomAction={onCustomAction} onUrlPress={onUrlPress} onWebPaymentNavigationFinished={onWebPaymentNavigationFinished} /> ); } ``` ## Trình bày dạng modal \{#modal-presentation\} Để hiển thị một flow dưới dạng màn hình độc lập, hãy dùng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createFlowView`](react-native-get-pb-paywalls#fetch-the-view-configuration). Mỗi `view` chỉ có thể dùng một lần. Nếu bạn cần hiển thị flow lại, hãy gọi `createFlowView` thêm một lần nữa để tạo một `view` mới. :::warning Không được tái sử dụng cùng một `view` mà không tạo lại. Điều này sẽ dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```typescript showLineNumbers title="React Native (TSX)" const view = await createFlowView(flow); // Optional: handle flow events (close, purchase, restore, etc) // view.setEventHandlers({ ... }); try { await view.present(); } catch (error) { // handle the error } ``` :::important Gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã thiết lập trước đó cho những sự kiện cụ thể đó. ::: ### Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\} Cấu hình cách flow được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. ```typescript showLineNumbers try { await view.present({ iosPresentationStyle: 'page_sheet' }); } catch (error) { // handle the error } ``` ## Sử dụng timer do developer tự định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer do developer tự định nghĩa trong ứng dụng mobile, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer mà bạn đã thiết lập trong Adapty dashboard. Điều này đảm bảo ứng dụng của bạn tự động cập nhật timer với giá trị chính xác — ví dụ như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, chẳng hạn như Ngày đầu năm mới, trừ đi thời gian hiện tại). <Tabs> <TabItem value="component" label="React component"> ```typescript showLineNumbers title="React Native (TSX)" const flowParams = { customTimers: { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } }; <AdaptyFlowView flow={flow} params={flowParams} // ... your event handlers /> ``` </TabItem> <TabItem value="modal" label="Modal presentation"> ```typescript showLineNumbers title="React Native (TSX)" const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) }; const view = await createFlowView(flow, { customTimers }); ``` </TabItem> </Tabs> Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của bộ đếm thời gian do developer đị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 bộ đếm với giá trị chính xác—chẳng hạn như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của bộ đếm, ví dụ như Năm Mới, trừ đi thời gian hiện tại). ## Hiển thị hộp thoại \{#show-dialog\} Sử dụng phương thức này thay cho các hộp thoại alert mặc định khi một flow view đang được hiển thị trên Android. Trên Android, các alert thông thường của RN sẽ xuất hiện phía sau flow view, khiến người dùng không nhìn thấy. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng vị trí, phía trên flow, trên tất cả các nền tảng. ```typescript showLineNumbers title="React Native (TSX)" try { const action = await view.showDialog({ title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', }); if (action === 'secondary') { // User confirmed - close the flow await view.dismiss(); } // If primary - do nothing, user stays } catch (error) { // handle error } ``` ## Thay thế một gói đăng ký bằng gói khác \{#replace-one-subscription-with-another\} Khi người dùng cố mua một gói đăng ký mới trong khi đã có một gói đăng ký đang hoạt động trên Android, bạn có thể kiểm soát cách xử lý giao dịch mua mới bằng cách truyền tham số cập nhật gói đăng ký khi tạo flow view. Để thay thế gói đăng ký hiện tại bằng gói mới, hãy sử dụng `productPurchaseParams` trong `createFlowView` với các tham số `oldSubVendorProductId` và `prorationMode`. ```typescript showLineNumbers title="React Native (TSX)" const productPurchaseParams = flow.paywalls .flatMap((variation) => variation.productIdentifiers) .map((productId) => { let params = {}; if (Platform.OS === 'android') { params.android = { subscriptionUpdateParams: { oldSubVendorProductId: 'PRODUCT_ID_OF_THE_CURRENT_ACTIVE_SUBSCRIPTION', prorationMode: 'with_time_proration', }, }; } return { productId, params }; }); const view = await createFlowView(flow, { productPurchaseParams }); ``` </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung hiển thị lẫn cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã [tạo một paywall](create-paywall). 2. Bạn đã thêm paywall vào một [placement](placements). 3. Bạn đã [lấy paywall và chuẩn bị view](react-native-get-pb-paywalls). :::warning Hướng dẫn này chỉ dành cho **paywall mới trong Paywall Builder**, yêu cầu SDK v3.0 trở lên. Quy trình hiển thị paywall khác nhau tùy theo phiên bản Paywall Builder được sử dụng và paywall sử dụng Remote Config. - Để hiển thị **paywall sử dụng Remote Config**, xem [Hiển thị paywall được thiết kế bằng Remote Config](present-remote-config-paywalls). ::: Adapty React Native SDK cung cấp hai cách để hiển thị paywall: - **React component**: Component được nhúng vào cho phép bạn tích hợp nó vào kiến trúc và hệ thống điều hướng của ứng dụng. - **Modal presentation** ## React component \{#react-component\} :::note Cách tiếp cận **React component** yêu cầu SDK phiên bản 3.14.0 trở lên. ::: Để nhúng một paywall vào cây component hiện có của bạn, hãy sử dụng component `AdaptyPaywallView` trực tiếp trong hệ thống phân cấp component React Native. Component nhúng này cho phép bạn tích hợp nó vào kiến trúc và hệ thống điều hướng của ứng dụng. :::note Trên Android, nếu paywall không kéo dài ra phía sau thanh trạng thái, một lớp phủ trực quan có thể xuất hiện ở phía trên. Chúng tôi khuyến nghị bạn tắt tính năng này cho các paywall của mình. Xem [Lớp phủ trực quan ở đầu paywall (Android)](#visual-overlay-at-the-top-of-the-paywall-android). ::: ```typescript showLineNumbers title="React Native (TSX)" function MyPaywall({ paywall }) { const paywallParams = useMemo(() => ({ loadTimeoutMs: 3000, }), []); const onCloseButtonPress = useCallback<EventHandlers['onCloseButtonPress']>(() => {}, []); const onProductSelected = useCallback<EventHandlers['onProductSelected']>((productId) => {}, []); const onPurchaseStarted = useCallback<EventHandlers['onPurchaseStarted']>((product) => {}, []); const onPurchaseCompleted = useCallback<EventHandlers['onPurchaseCompleted']>((purchaseResult, product) => {}, []); const onPurchaseFailed = useCallback<EventHandlers['onPurchaseFailed']>((error, product) => {}, []); const onRestoreStarted = useCallback<EventHandlers['onRestoreStarted']>(() => {}, []); const onRestoreCompleted = useCallback<EventHandlers['onRestoreCompleted']>((profile) => {}, []); const onRestoreFailed = useCallback<EventHandlers['onRestoreFailed']>((error) => {}, []); const onPaywallShown = useCallback<EventHandlers['onPaywallShown']>(() => {}, []); const onRenderingFailed = useCallback<EventHandlers['onRenderingFailed']>((error) => {}, []); const onLoadingProductsFailed = useCallback<EventHandlers['onLoadingProductsFailed']>((error) => {}, []); const onUrlPress = useCallback<EventHandlers['onUrlPress']>((url) => {}, []); const onCustomAction = useCallback<EventHandlers['onCustomAction']>((actionId) => {}, []); const onWebPaymentNavigationFinished = useCallback<EventHandlers['onWebPaymentNavigationFinished']>(() => {}, []); return ( <AdaptyPaywallView paywall={paywall} params={paywallParams} style={styles.paywall} onCloseButtonPress={onCloseButtonPress} onProductSelected={onProductSelected} onPurchaseStarted={onPurchaseStarted} onPurchaseCompleted={onPurchaseCompleted} onPurchaseFailed={onPurchaseFailed} onRestoreStarted={onRestoreStarted} onRestoreCompleted={onRestoreCompleted} onRestoreFailed={onRestoreFailed} onPaywallShown={onPaywallShown} onRenderingFailed={onRenderingFailed} onLoadingProductsFailed={onLoadingProductsFailed} onCustomAction={onCustomAction} onUrlPress={onUrlPress} onWebPaymentNavigationFinished={onWebPaymentNavigationFinished} /> ); } ``` ## Trình bày dạng modal \{#modal-presentation\} Để hiển thị paywall dưới dạng màn hình độc lập, hãy sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createPaywallView`](react-native-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo một `view` mới. :::warning Không được phép tái sử dụng cùng một `view` mà không tạo lại. Việc này sẽ dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```typescript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); // Optional: handle paywall events (close, purchase, restore, etc) // view.setEventHandlers({ ... }); try { await view.present(); } catch (error) { // handle the error } ``` :::important Gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã được thiết lập trước đó cho những sự kiện cụ thể đó. ::: ### Cấu hình kiểu hiển thị 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 các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. ```typescript showLineNumbers try { await view.present({ iosPresentationStyle: 'page_sheet' }); } catch (error) { // handle the error } ``` ## Sử dụng timer do lập trình viên định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer do lập trình viên định nghĩa trong ứng dụng di động, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer do lập trình viên định nghĩa mà bạn đã thiết lập trên Adapty dashboard. Điều này đảm bảo ứng dụng cập nhật động giá trị của timer với giá trị chính xác — chẳng hạn `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, ví dụ như ngày đầu năm mới, trừ đi thời gian hiện tại). <Tabs> <TabItem value="component" label="React component"> ```typescript showLineNumbers title="React Native (TSX)" const paywallParams = { customTimers: { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } }; <AdaptyPaywallView paywall={paywall} params={paywallParams} // ... your event handlers /> ``` </TabItem> <TabItem value="modal" label="Modal presentation"> ```typescript showLineNumbers title="React Native (TSX)" const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) }; const view = await createPaywallView(paywall, { customTimers }); ``` </TabItem> </Tabs> Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của timer do developer định nghĩa mà bạn đã cài đặt trong Adapty dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động timer với giá trị chính xác—chẳng hạn như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, như Ngày Đầu Năm Mới, trừ đi thời gian hiện tại). ## Hiển thị dialog \{#show-dialog\} Sử dụng phương thức này thay cho các hộp thoại alert thông thường khi một paywall view đang hiển thị trên Android. Trên Android, các alert RN thông thường sẽ xuất hiện phía sau paywall view, khiến người dùng không thể nhìn thấy chúng. Phương thức này đảm bảo dialog được hiển thị đúng cách, phía trên paywall trên mọi nền tảng. ```typescript showLineNumbers title="React Native (TSX)" try { const action = await view.showDialog({ title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', }); if (action === 'secondary') { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (error) { // handle error } ``` ## Thay thế một gói đăng ký bằng gói khác \{#replace-one-subscription-with-another\} Khi người dùng cố mua một gói đăng ký mới trong khi đang có gói đăng ký khác đang hoạt động trên Android, bạn có thể kiểm soát cách xử lý giao dịch mua mới bằng cách truyền các tham số cập nhật gói đăng ký khi tạo paywall view. Để thay thế gói đăng ký hiện tại bằng gói mới, hãy sử dụng `productPurchaseParams` trong `createPaywallView` với các tham số `oldSubVendorProductId` và `prorationMode`. ```typescript showLineNumbers title="React Native (TSX)" const productPurchaseParams = paywall.productIdentifiers.map((productId) => { let params = {}; if (Platform.OS === 'android') { params.android = { subscriptionUpdateParams: { oldSubVendorProductId: 'PRODUCT_ID_OF_THE_CURRENT_ACTIVE_SUBSCRIPTION', prorationMode: 'with_time_proration', }, }; } return { productId, params }; }); const view = await createPaywallView(paywall, { productPurchaseParams }); ``` ## Khắc phục sự cố \{#troubleshooting\} ### Lớp phủ hình ảnh ở đầu paywall (Android) \{#visual-overlay-at-the-top-of-the-paywall-android\} :::note Cài đặt này được hỗ trợ từ React Native SDK 3.15.5 trở lên và chỉ khả dụng trong các dự án React Native thuần (bare). Nếu bạn đang sử dụng Expo managed workflow, bạn không thể thêm trực tiếp Android resource này. Để áp dụng cài đặt này, bạn cần tạo một Expo config plugin tùy chỉnh để thêm Android resource tương ứng và đăng ký nó trong `app.config.js`. Điều này là bắt buộc vì Expo quản lý native Android project thay cho bạn. ::: Nếu `AdaptyPaywallView` không mở rộng ra phía sau thanh trạng thái, một lớp phủ trực quan vẫn có thể xuất hiện ở phía trên. Để xóa nó, hãy thêm tài nguyên boolean sau vào ứng dụng của bạn: 1. Truy cập vào `android/app/src/main/res/values`. Nếu chưa có file `bools.xml`, hãy tạo mới. 2. Thêm tài nguyên sau: ```xml <resources> <bool name="adapty_paywall_enable_safe_area_paddings">false</bool> </resources> ``` Lưu ý rằng các thay đổi này áp dụng toàn cục cho tất cả các paywall trong ứng dụng của bạn. </SDKv3> --- # File: react-native-handle-paywall-actions --- --- title: "Phản hồi các hành động flow - React Native" description: "Xử lý các hành động nút từ flow và paywall trong React Native bằng Adapty để tối ưu hóa monetization cho ứng dụng." --- <SDKv4> Nếu bạn đang xây dựng flow hoặc paywall bằng Adapty Flow Builder hoặc 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 flow/paywall builder](paywall-buttons) và gán cho nó một action có sẵn hoặc tạo một custom action ID. 2. Viết code trong ứng dụng của bạn để xử lý từng action bạn đã gán. Hướng dẫn này trình bày cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Việc mua hàng, khôi phục, đóng flow/paywall và mở URL được xử lý tự động.** Bạn có thể cấu hình hành vi mặc định của chúng hoặc triển khai phản hồi cho các custom action. ::: :::note SDK cung cấp một flow handler `onRequestPermission` để xử lý các yêu cầu cấp quyền hệ thống như thông báo đẩy hoặc truy cập camera. Hiện tại các flow chưa kích hoạt những yêu cầu này, nên bạn chưa cần triển khai nó. ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall: 1. Trong builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai một handler cho hành động `close` để đóng flow hoặc paywall. :::info Trong React Native SDK, hành động `close` mặc định sẽ tự động đóng flow hoặc paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ: đóng một flow có thể kích hoạt mở một flow khác. ::: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> For React component, handle the close action through individual event handler props: ```javascript function MyPaywall({ flow }) { const onCloseButtonPress = useCallback<FlowEventHandlers['onCloseButtonPress']>(() => { // Handle close button press - navigate away or hide component navigation.goBack(); }, [navigation]); return ( <AdaptyFlowView flow={flow} style={styles.container} onCloseButtonPress={onCloseButtonPress} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Đối với modal presentation, hãy triển khai close handler: ```javascript const view = await createFlowView(flow); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; // allow flow or paywall closing } }); ``` </TabItem> </Tabs> ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch), hãy thêm phần tử **Link** trong builder và xử lý nó giống như các nút với hành động **Open URL**. ::: Để thêm nút mở liên kết từ flow hoặc paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng của bạn, triển khai một handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong React Native SDK, action `openUrl` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Với React component, hãy xử lý việc mở URL thông qua prop event handler: ```javascript function MyPaywall({ flow }) { const onUrlPress = useCallback<FlowEventHandlers['onUrlPress']>((url) => { Linking.openURL(url); }, []); return ( <AdaptyFlowView flow={flow} style={styles.container} onUrlPress={onUrlPress} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> For modal presentation, implement the URL handler: ```javascript const view = await createFlowView(flow); const unsubscribe = view.setEventHandlers({ onUrlPress(url) { Linking.openURL(url); return false; // Keep flow or paywall open }, }); ``` </TabItem> </Tabs> ## 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 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, hãy implement handler cho ID hành động bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút để hiển thị một flow hoặc paywall khác: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Với React component, hãy xử lý các hành động tùy chỉnh thông qua prop event handler: ```javascript function MyPaywall({ flow }) { const onCustomAction = useCallback<FlowEventHandlers['onCustomAction']>((actionId) => { if (actionId === 'openNewPaywall') { // Display another flow or paywall } }, []); return ( <AdaptyFlowView flow={flow} style={styles.container} onCustomAction={onCustomAction} /> ); } ``` </TabItem> <TabItem value="standalone" label="Trình bày dạng modal"> Để xử lý modal presentation, hãy triển khai các action handler tùy chỉnh: ```javascript const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another flow or paywall } }, }); ``` </TabItem> </Tabs> </SDKv4> <SDKv3> Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập các button đú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 để xử lý từng hành động bạn đã gán. Hướng dẫn này mô tả cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code. :::warning **Chỉ có mua hàng, khôi phục, đóng paywall và mở URL được xử lý tự động.** Tất cả các hành động nút khác đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai handler cho hành động `close` để đóng paywall. :::info Trong React Native SDK, hành động `close` mặc định sẽ đóng paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ: đóng một paywall có thể kích hoạt mở một paywall khác. ::: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> For React component, handle the close action through individual event handler props: ```javascript function MyPaywall({ paywall }) { const onCloseButtonPress = useCallback<EventHandlers['onCloseButtonPress']>(() => { // Handle close button press - navigate away or hide component navigation.goBack(); }, [navigation]); return ( <AdaptyPaywallView paywall={paywall} style={styles.container} onCloseButtonPress={onCloseButtonPress} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Đối với modal presentation, hãy triển khai trình xử lý đóng: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; // allow paywall closing } }); ``` </TabItem> </Tabs> ## Mở URL từ paywall \{#open-urls-from-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch), hãy thêm một phần tử **Link** trong paywall builder và xử lý nó giống như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết từ paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong paywall builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong mã ứng dụng của bạn, implement một handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong React Native SDK, action `openUrl` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Với React component, hãy xử lý việc mở URL thông qua prop event handler: ```javascript function MyPaywall({ paywall }) { const onUrlPress = useCallback<EventHandlers['onUrlPress']>((url) => { Linking.openURL(url); }, []); return ( <AdaptyPaywallView paywall={paywall} style={styles.container} onUrlPress={onUrlPress} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Để hiển thị modal, hãy triển khai URL handler: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onUrlPress(url) { Linking.openURL(url); return false; // Keep paywall open }, }); ``` </TabItem> </Tabs> ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng: 1. Trong paywall builder, thêm một nút và gán cho nó hành động **Login**. 2. Trong code ứng dụng, triển khai một handler cho hành động `login` để xác định người dùng của bạn. <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Với React component, xử lý đăng nhập thông qua prop event handler: ```javascript function MyPaywall({ paywall }) { const onCustomAction = useCallback<EventHandlers['onCustomAction']>((actionId) => { if (actionId === 'login') { navigation.navigate('Login'); } }, [navigation]); return ( <AdaptyPaywallView paywall={paywall} style={styles.container} onCustomAction={onCustomAction} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Đối với modal presentation, hãy implement handler đăng nhập: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'login') { navigation.navigate('Login'); } } }); ``` </TabItem> </Tabs> ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong 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 mã ứng dụng của bạn, 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 một nút để hiển thị một paywall khác: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Đối với React component, xử lý các hành động tùy chỉnh thông qua prop event handler: ```javascript function MyPaywall({ paywall }) { const onCustomAction = useCallback<EventHandlers['onCustomAction']>((actionId) => { if (actionId === 'openNewPaywall') { // Display another paywall } }, []); return ( <AdaptyPaywallView paywall={paywall} style={styles.container} onCustomAction={onCustomAction} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Để xử lý modal, hãy triển khai các action handler tùy chỉnh: ```javascript const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another paywall } }, }); ``` </TabItem> </Tabs> </SDKv3> --- # File: react-native-handling-events-1 --- --- title: "Xử lý sự kiện flow & paywall - React Native" description: "Xử lý các sự kiện flow và paywall trong ứng dụng React Native của bạn với SDK của Adapty." --- <SDKv4> :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, lựa chọn sản phẩm và hiển thị flow. Bạn cũng có thể thiết lập xử lý nút (đóng flow, mở liên kết, các hành động tùy chỉnh, v.v.). Xem [hướng dẫn xử lý hành động nút](react-native-handle-paywall-actions) để biết thêm chi tiết. ::: Flow và paywall được xây dựng bằng Flow Builder không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Những sự kiện đó bao gồm các lần nhấn nút (nút đóng, URL, lựa chọn sản phẩm, v.v.) cũng như các thông báo về các hành động liên quan đến giao dịch mua được thực hiện trên flow. Hãy tìm hiểu cách phản hồi các sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình diễn ra trên màn hình flow trong ứng dụng di động của bạn, hãy triển khai các event handler: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Đối với React component, bạn xử lý sự kiện thông qua các prop xử lý sự kiện riêng lẻ trong component `AdaptyFlowView`: ```typescript showLineNumbers title="React Native (TSX)" function MyFlow({ flow }) { const onCloseButtonPress = useCallback<FlowEventHandlers['onCloseButtonPress']>(() => {}, []); const onProductSelected = useCallback<FlowEventHandlers['onProductSelected']>((productId) => {}, []); const onPurchaseStarted = useCallback<FlowEventHandlers['onPurchaseStarted']>((product) => {}, []); const onPurchaseCompleted = useCallback<FlowEventHandlers['onPurchaseCompleted']>((purchaseResult, product) => {}, []); const onPurchaseFailed = useCallback<FlowEventHandlers['onPurchaseFailed']>((error, product) => {}, []); const onRestoreStarted = useCallback<FlowEventHandlers['onRestoreStarted']>(() => {}, []); const onRestoreCompleted = useCallback<FlowEventHandlers['onRestoreCompleted']>((profile) => {}, []); const onRestoreFailed = useCallback<FlowEventHandlers['onRestoreFailed']>((error) => {}, []); const onAppeared = useCallback<FlowEventHandlers['onAppeared']>(() => {}, []); const onError = useCallback<FlowEventHandlers['onError']>((error) => {}, []); const onLoadingProductsFailed = useCallback<FlowEventHandlers['onLoadingProductsFailed']>((error) => {}, []); const onUrlPress = useCallback<FlowEventHandlers['onUrlPress']>((url, openIn) => { adapty.openWebUrl(url, openIn); return false; }, []); const onCustomAction = useCallback<FlowEventHandlers['onCustomAction']>((actionId) => {}, []); const onWebPaymentNavigationFinished = useCallback<FlowEventHandlers['onWebPaymentNavigationFinished']>(() => {}, []); return ( <AdaptyFlowView flow={flow} style={styles.container} onCloseButtonPress={onCloseButtonPress} onProductSelected={onProductSelected} onPurchaseStarted={onPurchaseStarted} onPurchaseCompleted={onPurchaseCompleted} onPurchaseFailed={onPurchaseFailed} onRestoreStarted={onRestoreStarted} onRestoreCompleted={onRestoreCompleted} onRestoreFailed={onRestoreFailed} onAppeared={onAppeared} onError={onError} onLoadingProductsFailed={onLoadingProductsFailed} onUrlPress={onUrlPress} onCustomAction={onCustomAction} onWebPaymentNavigationFinished={onWebPaymentNavigationFinished} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Đối với modal presentation, hãy implement phương thức event handlers. :::important Gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã được thiết lập trước đó cho những event cụ thể đó. ::: ```javascript showLineNumbers title="React Native (TSX)" const view = await createFlowView(flow); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; }, onAndroidSystemBack() { return true; }, onPurchaseCompleted(purchaseResult, product) { return purchaseResult.type !== 'user_cancelled'; }, onPurchaseStarted(product) { /***/}, onPurchaseFailed(error, product) { /***/ }, onRestoreCompleted(profile) { /***/ }, onRestoreFailed(error) { /***/ }, onProductSelected(productId) { /***/}, onError(error) { /***/ }, onLoadingProductsFailed(error) { /***/ }, onUrlPress(url, openIn) { adapty.openWebUrl(url, openIn); return false; // Keep flow open }, onAppeared() { /***/ }, onDisappeared() { /***/ }, onWebPaymentNavigationFinished() { /***/ }, }); ``` </TabItem> </Tabs> <Details> <summary>Ví dụ về sự kiện (Nhấn để mở rộng)</summary> ```javascript // onCloseButtonPress { //Record the event } // onAndroidSystemBack { //Record the event } // onUrlPress { "url": "https://example.com/terms" } // onCustomAction { "actionId": "login" } // onProductSelected { "productId": "premium_monthly" } // onPurchaseStarted { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onPurchaseCompleted - Success { "purchaseResult": { "type": "success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onPurchaseCompleted - Cancelled { "purchaseResult": { "type": "user_cancelled" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onPurchaseFailed { "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onRestoreCompleted { "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" } ] } } // onRestoreFailed { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } // onError { "error": { "code": "rendering_failed", "message": "Failed to render flow interface", "details": { "underlyingError": "Invalid flow configuration" } } } // onLoadingProductsFailed { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } // onAppeared { //Record the event } // onDisappeared { //Record the event } // onWebPaymentNavigationFinished { //Record the event } ``` </Details> Bạn có thể đăng ký những event handler cần thiết và bỏ qua những cái không cần dùng. Như vậy, các event listener không cần thiết sẽ không được tạo ra. Không có event handler nào là bắt buộc. Event handler trả về một giá trị boolean. Nếu trả về `true`, quá trình hiển thị được coi là hoàn tất, flow screen sẽ đóng lại và các event listener của view đó sẽ bị xóa. Một số event handler có hành vi mặc định mà bạn có thể ghi đè nếu cần: - `onCloseButtonPress`: đóng flow khi nhấn nút đóng. - `onUrlPress`: mở URL được nhấn và giữ flow mở. - `onAndroidSystemBack` (chỉ dành cho modal presentation): giữ flow mở khi nhấn nút **Back**. Trả về `true` để đóng flow. - `onRestoreCompleted`: giữ flow mở sau khi khôi phục thành công. Trả về `true` để đóng flow. - `onPurchaseCompleted`: giữ flow mở sau khi hoàn tất mua hàng. Trả về `true` để đóng flow. - `onError`: đóng flow nếu quá trình render thất bại. ### Trình xử lý sự kiện \{#event-handlers\} | Trình xử lý sự kiện | Mô tả | |:-----------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCustomAction** | Được gọi khi người dùng thực hiện một hành động tùy chỉnh, ví dụ: nhấn vào [nút tùy chỉnh](paywall-buttons). | | **onUrlPress** | Được gọi khi người dùng nhấn vào một URL trong flow của bạn. | | **onAndroidSystemBack** | Chỉ dành cho chế độ hiển thị modal: Được gọi khi người dùng nhấn nút **Back** hệ thống của Android. | | **onCloseButtonPress** | Được gọi khi nút đóng hiển thị và người dùng nhấn vào đó. Nên đóng màn hình flow trong trình xử lý này. | | **onPurchaseCompleted** | Được gọi khi giao dịch mua hoàn tất, dù thành công, người dùng hủy, hay đang chờ phê duyệt. Trong trường hợp mua thành công, sự kiện này cung cấp `AdaptyProfile` đã được cập nhật. Các trường hợp người dùng hủy và thanh toán đang chờ (ví dụ: cần phê duyệt của phụ huynh) sẽ kích hoạt sự kiện này, không phải `onPurchaseFailed`. | | **onPurchaseStarted** | Được gọi khi người dùng nhấn nút hành động "Purchase" để bắt đầu quá trình mua hàng. | | **onPurchaseFailed** | Được gọi khi giao dịch mua thất bại do lỗi (ví dụ: hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng, lỗi xác minh giao dịch). Không được gọi khi người dùng hủy hoặc thanh toán đang chờ — những trường hợp đó sẽ kích hoạt `onPurchaseCompleted`. | | **onRestoreStarted** | Được gọi khi người dùng bắt đầu quá trình khôi phục giao dịch mua. | | **onRestoreCompleted** | Được gọi khi khôi phục giao dịch mua thành công và cung cấp `AdaptyProfile` đã được cập nhật. Nên đóng màn hình nếu người dùng có `accessLevel` yêu cầu. Tham khảo [Trạng thái gói đăng ký](react-native-listen-subscription-changes) để biết cách kiểm tra. | | **onRestoreFailed** | Được gọi khi quá trình khôi phục thất bại và cung cấp `AdaptyError`. | | **onProductSelected** | Được gọi khi bất kỳ sản phẩm nào trong giao diện flow được chọn, cho phép bạn theo dõi lựa chọn của người dùng trước khi mua hàng. | | **onError** | Được gọi khi xảy ra lỗi trong quá trình render giao diện và cung cấp `AdaptyError`. Những 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. | | **onLoadingProductsFailed** | Được gọi khi tải sản phẩm thất bại và cung cấp `AdaptyError`. Nếu bạn chưa đặt `prefetchProducts: true` khi tạo view, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ máy chủ. | | **onAppeared** | Được gọi khi flow được hiển thị cho người dùng. Trên iOS, cũng được gọi khi người dùng nhấn [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong flow và một web paywall mở ra trong trình duyệt trong ứng dụng. | | **onDisappeared** | Chỉ dành cho chế độ hiển thị modal: Được gọi khi người dùng đóng flow. Trên iOS, cũng được gọi khi [web paywall](web-paywall#step-2a-add-a-web-purchase-button) được mở từ flow trong trình duyệt trong ứng dụng biến mất khỏi màn hình. | | **onWebPaymentNavigationFinished** | Được gọi sau khi cố gắng mở [web paywall](web-paywall) để thực hiện mua hàng, dù thành công hay thất bại. | | **onAnalytics** | Dành riêng cho các sự kiện phân tích tùy chỉnh từ flow. Các flow hiện chưa phát ra những sự kiện này cho code của bạn, vì vậy bạn không cần triển khai. | | **onRequestAppReview** | Dành riêng cho các yêu cầu đánh giá ứng dụng từ flow. Các flow hiện chưa kích hoạt yêu cầu đánh giá ứng dụng, vì vậy bạn không cần triển khai. | | **onRequestPermission** | Dành riêng cho các yêu cầu quyền hệ thống (chẳng hạn như thông báo đẩy hoặc truy cập camera) từ flow. Các flow hiện chưa kích hoạt yêu cầu quyền, vì vậy bạn không cần triển khai. | | **onObserverPurchaseInitiated** | Chỉ dành cho chế độ observer: Được gọi khi người dùng nhấn nút mua hàng trong flow. Adapty không thực hiện giao dịch mua — hãy tự thực hiện bằng code mua hàng của bạn, sau đó báo cáo giao dịch cho Adapty. Xem [Xử lý giao dịch mua trong chế độ observer](#handle-purchases-in-observer-mode) bên dưới. | | **onObserverRestoreInitiated** | Chỉ dành cho chế độ observer: Được gọi khi người dùng nhấn nút khôi phục trong flow. Adapty không thực hiện khôi phục — hãy tự thực hiện, sau đó báo cáo các giao dịch đã khôi phục. Xem [Xử lý giao dịch mua trong chế độ observer](#handle-purchases-in-observer-mode) bên dưới. | ### Xử lý mua hàng ở chế độ Observer \{#handle-purchases-in-observer-mode\} Nếu bạn kích hoạt SDK ở [chế độ Observer](implement-observer-mode-react-native) (`observerMode: true`) và hiển thị flow do Adapty render, SDK sẽ không thực hiện mua hàng thay bạn. Khi người dùng nhấn nút mua hàng hoặc khôi phục, SDK sẽ gọi `onObserverPurchaseInitiated` hoặc `onObserverRestoreInitiated` thay thế. Hãy tự thực hiện việc mua hàng hoặc khôi phục bằng code của bạn, điều khiển trạng thái loading của flow thông qua các callback được cung cấp, và sau đó [báo cáo giao dịch](report-transactions-observer-mode-react-native) cho Adapty. ```typescript showLineNumbers const unsubscribe = view.setEventHandlers({ onObserverPurchaseInitiated(product, onStartPurchase, onFinishPurchase) { onStartPurchase(); // show the flow's loading indicator myPurchaseApi(product.vendorProductId) .then((transactionId) => adapty.reportTransaction(transactionId)) .finally(() => onFinishPurchase()); // hide the loading indicator return false; // keep the flow open; dismiss it yourself after success }, onObserverRestoreInitiated(onStartRestore, onFinishRestore) { onStartRestore(); myRestoreApi() .finally(() => onFinishRestore()); return false; }, }); ``` </SDKv4> <SDKv3> :::important Hướng dẫn này đề cập đến việc xử lý các sự kiện cho 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](react-native-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 được thực hiện trên paywall. Hãy xem cách phản hồi các sự kiện này bên dưới. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** yêu cầu Adapty SDK v3.0 trở lên. ::: Щоб контролювати або theo dõi các tiến trình xảy 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 trình xử lý sự kiện: <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Với React component, bạn xử lý sự kiện thông qua các prop trình xử lý sự kiện riêng lẻ trong component `AdaptyPaywallView`: ```typescript showLineNumbers title="React Native (TSX)" function MyPaywall({ paywall }) { const onCloseButtonPress = useCallback<EventHandlers['onCloseButtonPress']>(() => {}, []); const onProductSelected = useCallback<EventHandlers['onProductSelected']>((productId) => {}, []); const onPurchaseStarted = useCallback<EventHandlers['onPurchaseStarted']>((product) => {}, []); const onPurchaseCompleted = useCallback<EventHandlers['onPurchaseCompleted']>((purchaseResult, product) => {}, []); const onPurchaseFailed = useCallback<EventHandlers['onPurchaseFailed']>((error, product) => {}, []); const onRestoreStarted = useCallback<EventHandlers['onRestoreStarted']>(() => {}, []); const onRestoreCompleted = useCallback<EventHandlers['onRestoreCompleted']>((profile) => {}, []); const onRestoreFailed = useCallback<EventHandlers['onRestoreFailed']>((error) => {}, []); const onPaywallShown = useCallback<EventHandlers['onPaywallShown']>(() => {}, []); const onRenderingFailed = useCallback<EventHandlers['onRenderingFailed']>((error) => {}, []); const onLoadingProductsFailed = useCallback<EventHandlers['onLoadingProductsFailed']>((error) => {}, []); const onUrlPress = useCallback<EventHandlers['onUrlPress']>((url) => { Linking.openURL(url); }, []); const onCustomAction = useCallback<EventHandlers['onCustomAction']>((actionId) => {}, []); const onWebPaymentNavigationFinished = useCallback<EventHandlers['onWebPaymentNavigationFinished']>(() => {}, []); return ( <AdaptyPaywallView paywall={paywall} style={styles.container} onCloseButtonPress={onCloseButtonPress} onProductSelected={onProductSelected} onPurchaseStarted={onPurchaseStarted} onPurchaseCompleted={onPurchaseCompleted} onPurchaseFailed={onPurchaseFailed} onRestoreStarted={onRestoreStarted} onRestoreCompleted={onRestoreCompleted} onRestoreFailed={onRestoreFailed} onPaywallShown={onPaywallShown} onRenderingFailed={onRenderingFailed} onLoadingProductsFailed={onLoadingProductsFailed} onUrlPress={onUrlPress} onCustomAction={onCustomAction} onWebPaymentNavigationFinished={onWebPaymentNavigationFinished} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Đối với modal presentation, hãy triển khai phương thức event handlers. :::important Việc gọi `setEventHandlers` nhiều lần sẽ ghi đè các handlers bạn đã cung cấp, thay thế cả handlers mặc định lẫn các handlers đã được thiết lập trước đó cho những sự kiện cụ thể đó. ::: ```javascript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; }, onAndroidSystemBack() { return true; }, onPurchaseCompleted(purchaseResult, product) { return purchaseResult.type !== 'user_cancelled'; }, onPurchaseStarted(product) { /***/}, onPurchaseFailed(error) { /***/ }, onRestoreCompleted(profile) { /***/ }, onRestoreFailed(error) { /***/ }, onProductSelected(productId) { /***/}, onRenderingFailed(error) { /***/ }, onLoadingProductsFailed(error) { /***/ }, onUrlPress(url) { Linking.openURL(url); return false; // Keep paywall open }, onPaywallShown() { /***/ }, onPaywallClosed() { /***/ }, onWebPaymentNavigationFinished() { /***/ }, }); ``` </TabItem> </Tabs> <Details> <summary>Ví dụ về sự kiện (Nhấp để mở rộng)</summary> ```javascript // onCloseButtonPress { //Record the event } // onAndroidSystemBack { //Record the event } // onUrlPress { "url": "https://example.com/terms" } // onCustomAction { "actionId": "login" } // onProductSelected { "productId": "premium_monthly" } // onPurchaseStarted { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onPurchaseCompleted - Success { "purchaseResult": { "type": "success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onPurchaseCompleted - Cancelled { "purchaseResult": { "type": "user_cancelled" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onPurchaseFailed { "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "price": { "amount": 9.99, "currencyCode": "USD", "currencySymbol": "$", "localizedString": "$9.99" } } } // onRestoreCompleted { "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" } ] } } // onRestoreFailed { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } // onRenderingFailed { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } // onLoadingProductsFailed { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } // onPaywallShown { //Record the event } // onPaywallClosed { //Record the event } // onWebPaymentNavigationFinished { //Record the event } ``` </Details> Bạn có thể đăng ký các event handler mà bạn cần và bỏ qua những cái không cần thiết. Nhờ vậy, các event listener không dùng đến sẽ không được tạo ra. Không có event handler nào là bắt buộc. Event handler trả về một giá trị boolean. Nếu trả về `true`, quá trình hiển thị được coi là hoàn tất, màn hình paywall sẽ đóng lại và các event listener cho view này sẽ bị xóa. Một số event handler có hành vi mặc định mà bạn có thể ghi đè nếu cần: - `onCloseButtonPress`: đóng paywall khi nhấn nút đóng. - `onUrlPress`: mở URL được nhấn và giữ paywall mở. - `onAndroidSystemBack` (chỉ dành cho modal presentation): đóng paywall khi nhấn nút **Back**. - `onRestoreCompleted`: đóng paywall sau khi khôi phục thành công. - `onPurchaseCompleted`: đóng paywall trừ khi người dùng hủy. - `onRenderingFailed`: đóng paywall nếu quá trình render thất bại. ### Trình xử lý sự kiện \{#event-handlers\} | Event handler | Mô tả | |:-----------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCustomAction** | Được gọi khi người dùng thực hiện một hành động tùy chỉnh, ví dụ: nhấn vào [nút tùy chỉnh](paywall-buttons). | | **onUrlPress** | Được gọi khi người dùng nhấn vào một URL trong paywall. | | **onAndroidSystemBack** | Chỉ dành cho modal presentation: Được gọi khi người dùng nhấn nút **Back** hệ thống của Android. | | **onCloseButtonPress** | Được gọi khi nút đóng hiển thị và người dùng nhấn vào đó. Nên đóng màn hình paywall trong handler này. | | **onPurchaseCompleted** | Được gọi khi quá trình mua hoàn tất, dù thành công, người dùng hủy, hay đang chờ phê duyệt. Nếu mua thành công, sẽ trả về `AdaptyProfile` đã được cập nhật. Việc người dùng hủy và các khoản thanh toán đang chờ (ví dụ: cần phê duyệt của phụ huynh) sẽ kích hoạt sự kiện này, không phải `onPurchaseFailed`. | | **onPurchaseStarted** | Được gọi khi người dùng nhấn nút hành động "Purchase" để bắt đầu quá trình mua. | | **onPurchaseFailed** | Được gọi khi giao dịch mua thất bại do lỗi (ví dụ: hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng, lỗi xác minh giao dịch). Không được gọi khi người dùng hủy hoặc thanh toán đang chờ — những trường hợp đó sẽ kích hoạt `onPurchaseCompleted`. | | **onRestoreStarted** | Được gọi khi người dùng bắt đầu quá trình khôi phục giao dịch. | | **onRestoreCompleted** | Được gọi khi khôi phục giao dịch thành công và trả về `AdaptyProfile` đã được cập nhật. Nên đóng màn hình nếu người dùng đã có `accessLevel` cần thiết. Xem thêm tại [Trạng thái gói đăng ký](react-native-listen-subscription-changes) để biết cách kiểm tra. | | **onRestoreFailed** | Được gọi khi quá trình khôi phục thất bại và trả về `AdaptyError`. | | **onProductSelected** | Được gọi khi người dùng chọn bất kỳ sản phẩm nào trong paywall, giúp bạn theo dõi lựa chọn của người dùng trước khi mua. | | **onRenderingFailed** | Được gọi khi xảy ra lỗi trong quá trình render view và trả về `AdaptyError`. Lỗi này thường không nên xảy ra, vì vậy nếu gặp phải, hãy liên hệ với chúng tôi. | | **onLoadingProductsFailed** | Được gọi khi tải sản phẩm thất bại và trả về `AdaptyError`. Nếu bạn chưa đặt `prefetchProducts: true` khi tạo view, AdaptyUI sẽ tự lấy các đối tượng cần thiết từ server. | | **onPaywallShown** | Được gọi khi paywall được hiển thị cho người dùng. 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) trong paywall và web paywall mở trong trình duyệt in-app. | | **onPaywallClosed** | Chỉ dành cho modal presentation: Được gọi khi người dùng đóng paywall. Trên iOS, cũng được gọi khi [web paywall](web-paywall#step-2a-add-a-web-purchase-button) mở từ paywall trong trình duyệt in-app biến mất khỏi màn hình. | | **onWebPaymentNavigationFinished** | Được gọi sau khi cố gắng mở [web paywall](web-paywall) để mua, dù thành công hay thất bại. | </SDKv3> --- # File: react-native-use-fallback-paywalls-expo --- --- title: "Sử dụng paywall dự phòng trong dự án Expo" description: "Cấu hình paywall dự phòng trong dự án Expo React Native thông qua config plugin react-native-adapty." --- :::important Hướng dẫn này áp dụng cho **các dự án Expo**. Nếu bạn đang dùng **React Native thuần (không dùng Expo)**, hãy xem [hướng dẫn paywall dự phòng cho React Native thuần](react-native-use-fallback-paywalls-pure). ::: Để 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. ::: SDK Adapty đọc file dự phòng từ **native** bundle — một resource iOS bên trong gói `.app`, hoặc một mục trong `android/app/src/main/assets/`. Trong dự án Expo, `npx expo prebuild --clean` sẽ tái tạo các thư mục đó mỗi lần chạy, vì vậy bạn không thể thêm file thủ công. Config plugin của `react-native-adapty` sẽ tự động tích hợp file vào native bundle cho bạn. :::tip Một ví dụ hoạt động hoàn chỉnh có sẵn trong [ứng dụng mẫu `FocusJournalExpo`](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo). ::: ## Cấu hình \{#configuration\} 1. Đặt các file JSON dự phòng ở bất kỳ đâu trong dự án — thường là cạnh các asset khác: ``` <your-project>/ └── assets/ ├── ios_fallback.json └── android_fallback.json ``` 2. Thêm tùy chọn `fallbackFile` vào mục `react-native-adapty` trong `app.json` (hoặc `app.config.js`). Mỗi key của từng nền tảng là tùy chọn — chỉ cần cấu hình những nền tảng bạn cần: ```json title="app.json" { "expo": { "plugins": [ [ "react-native-adapty", { "fallbackFile": { "ios": "./assets/ios_fallback.json", "android": "./assets/android_fallback.json" } } ] ] } } ``` :::note Adapty xuất một file JSON dự phòng khác nhau cho từng nền tảng — ID sản phẩm Apple trên iOS, ID sản phẩm Google Play trên Android. Hãy trỏ mỗi nền tảng đến file tương ứng của nó. ::: 3. Tạo lại các native project: ```sh title="Shell" npx expo prebuild ``` Plugin sẽ thêm file iOS vào bundle resources của Xcode project và sao chép file Android vào `android/app/src/main/assets/`. Output của lệnh prebuild sẽ có các dòng như: ``` [react-native-adapty] Registered ios_fallback.json as iOS bundle resource [react-native-adapty] Copied android_fallback.json to android assets/ ``` 4. Đăng ký file với SDK lúc runtime: ```typescript showLineNumbers title="App.tsx" import { adapty } from 'react-native-adapty'; await adapty.activate('PUBLIC_SDK_KEY'); await adapty.setFallback({ ios: { fileName: 'ios_fallback.json' }, android: { relativeAssetPath: 'android_fallback.json' }, }); ``` Tên file truyền vào `setFallback` phải khớp với tên cơ sở (basename) của các file đã cấu hình trong `fallbackFile`. :::important `setFallback` phải chạy trước khi SDK tải flow, paywall hoặc onboarding mục tiêu. ::: ## Kiểm tra \{#verification\} Sau khi chạy `npx expo prebuild`, hãy kiểm tra cả hai platform: - **Android**: Liệt kê nội dung của `android/app/src/main/assets/`. File đã cấu hình trong `fallbackFile.android` phải có mặt ở đây, và tên file chỉ dành cho iOS không được xuất hiện ở đây. - **iOS**: Tìm kiếm tên file iOS trong `ios/<ProjectName>.xcodeproj/project.pbxproj`. Nó phải xuất hiện trong `PBXFileReference`, nhóm `Resources`, và `PBXResourcesBuildPhase`. Tên file chỉ dành cho Android không được xuất hiện trong `project.pbxproj`. --- # File: react-native-use-fallback-paywalls-pure --- --- title: "Sử dụng paywall dự phòng trong dự án React Native thuần" description: "Cấu hình paywall dự phòng trong dự án React Native thuần (không dùng Expo)." --- :::important Hướng dẫn này áp dụng cho **các dự án React Native thuần túy (không dùng Expo)**. Nếu bạn đang dùng **Expo**, hãy làm theo [hướng dẫn paywall dự phòng dành cho Expo](react-native-use-fallback-paywalls-expo). ::: Để 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\} ### Android 1. Thêm file cấu hình dự phòng vào ứng dụng của bạn. Chọn một trong các thư mục sau: * **android/app/src/main/assets/** * **android/app/src/main/res/raw/** Lưu ý: Thư mục `res/raw` có quy tắc đặt tên file đặc biệt (phải bắt đầu bằng chữ cái, không dùng chữ hoa, không dùng ký tự đặc biệt ngoại trừ dấu gạch dưới, và không có dấu cách trong tên file). 2. Cập nhật thuộc tính `android` của hằng số `FileLocation`: * Nếu file nằm trong thư mục `assets`, truyền đường dẫn của file tương đối so với thư mục đó. * Nếu file nằm trong thư mục `res/raw`, truyền tên file không có phần mở rộng. ### iOS \{#ios\} 1. Thêm file JSON dự phòng vào bundle dự án: mở menu **File** trong XCode và chọn tùy chọn **Add Files to "YourProjectName"**. 2. Truyền tên file cấu hình vào thuộc tính `ios` của hằng số `FileLocation`. ## Ví dụ \{#example\} <Tabs groupId="current-os" queryString> <TabItem value="current" label="Current (v3.8+)" default> ```typescript showLineNumbers //after v3.8 const fileLocation = { ios: { fileName: 'ios_fallback.json' }, android: { //if the file is located in 'android/app/src/main/assets/' relativeAssetPath: 'android_fallback.json' } } await adapty.setFallback(fileLocation); ``` </TabItem> <TabItem value="old" label="Legacy (before v3.8)"> ```typescript showLineNumbers //Legacy (before v3.8) const paywallsLocation = { ios: { fileName: 'ios_fallback.json' }, android: { //if the file is located in 'android/app/src/main/assets/' relativeAssetPath: 'android_fallback.json' } } await adapty.setFallbackPaywalls(paywallsLocation); ``` </TabItem> </Tabs> | Tham số | Mô tả | | :------------------- | :------------------------------------------------------- | | **fileLocation** | Đối tượng đại diện cho vị trí của tệp cấu hình paywall dự phòng. | --- # File: react-native-localizations-and-locale-codes --- --- title: "Sử dụng localizations và locale codes trong React Native SDK" description: "Tìm hiểu cách localize paywall trong ứng dụng React Native của bạn với Adapty SDK." --- <SDKv4> ## Tại sao điều này quan trọng \{#why-this-is-important\} Mã ngôn ngữ (locale code) được dùng khi Adapty chọn bản địa hóa cho một flow và khi bạn đọc Remote Config cho một paywall tùy chỉnh. Mã ngôn ngữ khá phức tạp và có thể khác nhau tùy nền tảng, vì vậy Adapty sử dụng một chuẩn nội bộ thống nhất trên mọi nền tảng được hỗ trợ. Hiểu được chuẩn đó sẽ giúp bạn dự đoán người dùng sẽ nhận được bản địa hóa nào. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng tiêu chuẩn [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) được chỉnh sửa nhẹ: mỗi mã bao gồm các subtag viết thường, phân cách bằng dấu gạch nối. Ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Khớp mã ngôn ngữ \{#locale-code-matching\} Khi Adapty tìm kiếm bản địa hóa phù hợp với ngôn ngữ của người dùng, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay bằng dấu gạch ngang (`-`) 2. Adapty tìm kiếm bản địa hóa có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy kết quả, Adapty lấy phần chuỗi trước dấu gạch ngang đầu tiên (`pt` trong `pt-br`) và tìm kiếm bản địa hóa tương ứng 4. Nếu vẫn không tìm thấy, Adapty trả về bản địa hóa mặc định `en` Theo cách này, `'pt_BR'`, `pt-BR` và `pt-br` đều được ánh xạ đến cùng một bản địa hóa. ## Triển khai bản địa hóa \{#implementing-localizations\} Trong SDK v4, bạn không cần truyền mã ngôn ngữ khi tải một flow. - **Paywall dùng Flow Builder và Paywall Builder**: Adapty tự động xác định bản địa hóa dựa trên thiết bị và các bản địa hóa bạn đã cấu hình trong builder. Render flow bằng `createFlowView` — không cần mã ngôn ngữ. - **Paywall tùy chỉnh (remote config)**: `getFlow` trả về tất cả các bản địa hóa đã cấu hình trong `flow.remoteConfigs`. Mỗi mục có mã `lang` và đối tượng `data`. Chọn mục phù hợp với người dùng, kèm theo logic dự phòng của riêng bạn: ```typescript showLineNumbers const flow = await adapty.getFlow('placement_id'); const config = flow.remoteConfigs?.find((c) => c.lang === 'en') ?? flow.remoteConfigs?.[0]; // read your values from config?.data ``` Các quy tắc khớp mã locale ở trên mô tả cách Adapty chuẩn hóa các mã `lang` được lưu trên mỗi remote config. </SDKv4> <SDKv3> ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một vài tình huống mà mã locale phát huy vai trò — chẳng hạn, khi bạn cần lấy đúng paywall theo ngôn ngữ 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 từng nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ chung cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng bản địa hóa, 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ã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng một phiên bản đượ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, được phân tách bằng dấu gạch ngang. Một vài ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung Giản thể), `zh-hant` (tiếng Trung Phồn thể). ## Khớp mã ngôn ngữ \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client cùng với mã ngôn ngữ và bắt đầu tìm kiếm bản dịch tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Chúng tôi tìm kiếm bản dịch có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy, chúng tôi lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` trong trường hợp `pt-br`) và tìm kiếm bản dịch khớp với chuỗi con đó 4. Nếu vẫn không tìm thấy, chúng tôi trả về bản dịch mặc định `en` Theo cách này, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` sẽ nhận được cùng một kết quả. ## Triển khai bản địa hóa: cách được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang tìm hiểu về bản địa hóa, có thể bạn đã làm việc với các tệp chuỗi đã được bản địa hóa trong dự án của mình. Trong trường hợp đó, chúng tôi khuyến nghị bạn thêm một cặp key-value với mã locale Adapty tương ứng vào mỗi tệp bản địa hóa. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như sau: ```javascript showLineNumbers // 1. Modify your localization files (e.g., using react-i18next) /* en.json */ { "adapty_paywalls_locale": "en" } /* es.json */ { "adapty_paywalls_locale": "es" } /* pt-BR.json */ { "adapty_paywalls_locale": "pt-br" } // 2. Extract and use the locale code const MyComponent = () => { const { t } = useTranslation(); const fetchPaywall = async () => { const locale = t('adapty_paywalls_locale'); // pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale); }; }; ``` Bằng cách này, bạn có thể đảm bảo rằng mình hoàn toàn kiểm soát được bản dịch nào sẽ được truy xuất cho từng người dùng trong ứng dụng của bạn. ## Triển khai bản địa hóa: cách khác \{#implementing-localizations-the-other-way\} Bạn có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống nhau) mà không cần phải xác định rõ mã ngôn ngữ cho từng bản địa hóa. Thay vào đó, bạn có thể trích xuất mã ngôn ngữ từ thiết bị, ví dụ thông qua [`react-native-localize`](https://github.com/zoontek/react-native-localize): ```javascript showLineNumbers const fetchPaywall = async () => { // getLocales() returns the user's preferred locales in BCP-47 format (e.g., 'en-US', 'pt-BR') const locale = RNLocalize.getLocales()[0].languageTag; // pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale); }; ``` Lưu ý rằng chúng tôi không khuyến nghị cách tiếp cận này vì một số lý do: 1. Trên iOS, ngôn ngữ ưu tiên và locale khu vực hiện tại không giống nhau. Nếu bạn muốn bản địa hóa được chọn đúng, bạn cần dựa vào logic phân giải của Apple — hoạt động tự động khi bạn dùng cách tiếp cận được khuyến nghị với các file chuỗi đã bản địa hóa — hoặc tự xây dựng lại logic đó. 2. Locale của thiết bị có thể không khớp với bất kỳ bản địa hóa nào bạn đã cấu hình trong Adapty. Trong trường hợp đó, SDK sẽ dùng kết quả khớp theo subtag đầu tiên, hoặc cuối cùng là `en` — có thể không phải ngôn ngữ mặc định bạn muốn hiển thị cho người dùng đó. Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. </SDKv3> --- # File: react-native-web-paywall --- --- title: "Triển khai web paywall" description: "Tìm hiểu cách triển khai web paywall trong ứng dụng React Native với Adapty SDK." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trong dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: ## Mở web paywall \{#open-web-paywalls\} Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywall bằng phương thức SDK. Phương thức `.openWebPaywall`: 1. Tạo URL duy nhất giúp Adapty liên kết paywall cụ thể đang hiển thị cho một người dùng cụ thể với trang web họ được chuyển hướng đến. 2. Theo dõi khi người dùng quay lại ứng dụng rồi gọi `.getProfile` theo chu kỳ ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không. Như vậy, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ kích hoạt trong ứng dụng gần như ngay lập tức. ```typescript showLineNumbers title="React Native (TSX)" try { await adapty.openWebPaywall(product); } catch (error) { console.warn('Failed to open web paywall:', error); } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall(product)` tạo URL theo paywall và đồng thời thêm dữ liệu sản phẩm vào URL. 2. `openWebPaywall(paywall)` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng phiên bản này khi các sản phẩm trong Adapty paywall khác với các 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 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 web | Xác minh cấu hình sản phẩm trong Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Không mở được 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 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 người dùng liền mạch, bạn có thể mở web paywall trong trình duyệt trong ứng dụng. Tính năng này hiển thị trang mua hàng web ngay bên trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển ứng dụng. Để bật tính năng này, truyền `WebPresentation.BrowserInApp` làm đối số thứ hai cho `openWebPaywall`: ```typescript showLineNumbers title="React Native (TSX)" try { await adapty.openWebPaywall( product, WebPresentation.BrowserInApp, // default – WebPresentation.BrowserOutApp ); } catch (error) { console.warn('Failed to open web paywall:', error); } ``` --- # File: react-native-troubleshoot-paywall-builder --- --- title: "Khắc phục sự cố Paywall Builder trong React Native SDK" description: "Khắc phục sự cố Paywall Builder trong React Native SDK" --- Hướng dẫn này giúp bạn giải quyết các sự cố thường gặp khi sử dụng paywall được thiết kế bằng Adapty Paywall Builder trong React Native SDK. ## Lấy cấu hình paywall bị lỗi \{#getting-a-paywall-configuration-fails\} **Vấn đề**: Việc lấy cấu hình hiển thị cho một flow hoặc paywall bị thất bại. **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 nút **Show on device** trong Paywall Builder. <img src="/assets/shared/img/show-on-device.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Số lượt xem paywall quá lớn \{#the-paywall-view-number-is-too-big\} **Vấn đề**: Số lượt xem paywall hiển thị gấp đôi so với mong đợi. **Nguyên nhân**: Bạn có thể đang gọi `logShowFlow` (React Native SDK v4+) / `logShowPaywall` trong code, khiến số lượt xem bị tính trùng nếu bạn đang dùng Paywall Builder hoặc Flow Builder. Với các flow và paywall được tạo bằng những công cụ này, analytics đã được theo dõi tự động, nên bạn không cần dùng phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowFlow` (React Native SDK v4+) / `logShowPaywall` trong code nếu đang sử dụng Paywall Builder hoặc Flow Builder. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các sự cố liên quan đến Paywall Builder 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](react-native-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: react-native-quickstart-manual --- --- title: "Kích hoạt mua hàng trong paywall tùy chỉnh của bạn với React Native SDK" description: "Tích hợp Adapty SDK vào các paywall React Native tùy chỉnh của bạn để kích hoạt in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Toàn quyền kiểm soát việc triển khai paywall, trong khi Adapty SDK lấy sản phẩm, xử lý các 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 developer đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để kích hoạt tính năng mua hàng, hãy sử dụng [Adapty Flow Builder](react-native-quickstart-paywalls). Với Flow Builder, bạn tạo flow trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic mua hàng, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để kích hoạt in-app purchase, bạn cần 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) – cấu hình xác định những sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy danh sách 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. - [**Placement**](placements) – nơi và thời điểm hiển thị paywall trong ứng dụng của bạn (ví dụ: `main`, `onboarding`, `settings`). Bạn cài đặt paywall cho các placement trên dashboard, sau đó gọi chúng bằng placement ID trong code. Điều này giúp việc chạy A/B test và hiển thị các paywall khác nhau cho từng nhóm người dùng trở nên dễ dàng hơn. Hãy đảm bảo bạn hiểu rõ các khái niệm này, dù bạn đang dùng paywall tùy chỉnh của riêng mình. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm mà bạn bán trong ứng dụng. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Cài đặt này cho phép bạn lấy danh sách sản phẩm. Để hiểu những gì cần thực hiện trên dashboard, hãy xem hướng dẫn quickstart [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc có hoặc không cần xác thực backend ở phía mình. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo cách khác nhau. Đọc [hướng dẫn bắt đầu nhanh về xác định người dùng](react-native-quickstart-identify) để hiểu các điểm đặc thù và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy danh sách sản phẩm \{#step-1-get-products\} Để lấy danh sách sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền ID [placement](placements) vào phương thức `getFlow`. 2. Lấy mảng sản phẩm cho flow này bằng phương thức `getPaywallProducts`. ```typescript showLineNumbers async function loadPaywall() { try { const flow: AdaptyFlow = await adapty.getFlow('YOUR_PLACEMENT_ID'); const products: AdaptyPaywallProduct[] = await adapty.getPaywallProducts(flow); // Use products to build your custom paywall UI } catch (error) { // Handle the error } } ``` ## Bước 2. Xử lý 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ý flow thanh toán và trả về hồ sơ người dùng đã được cập nhật. ```typescript showLineNumbers async function purchaseProduct(product: AdaptyPaywallProduct) { try { const purchaseResult: AdaptyPurchaseResult = await adapty.makePurchase(product); switch (purchaseResult.type) { case 'success': // Purchase successful, profile updated break; case 'user_cancelled': // User canceled the purchase break; case 'pending': // Purchase is pending (e.g., user will pay offline with cash) break; } } catch (error) { // Handle the error } } ``` ## Bước 3. Khôi phục giao dịch mua \{#step-3-restore-purchases\} Các cửa hàng ứng dụng yêu cầu tất cả ứng dụng có gói đăng ký phải cung cấp cách để người dùng khôi phục giao dịch mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Thao tác này sẽ đồng bộ lịch sử giao dịch mua của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```typescript showLineNumbers async function restorePurchases() { try { const profile: AdaptyProfile = await adapty.restorePurchases(); // Restore successful, profile updated } catch (error) { // Handle the error } } ``` ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra các giao dịch mua trong [sandbox App Store](test-purchases-in-sandbox) hoặc trong [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một giao dịch mua thử nghiệm 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 [CustomPurchaseScreen.tsx](https://github.com/adaptyteam/AdaptySDK-React-Native/blob/master/examples/ExpoGoWebMock/src/CustomPurchaseScreen.tsx) 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 xử lý lỗi đúng cách, trạng thái tải, và quản lý trạng thái giao diện. Tiếp theo, [kiểm tra xem người dùng đã hoàn tất giao dịch mua chưa](react-native-check-subscription-status) để quyết định 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-react-native --- --- title: "Tải paywall và sản phẩm cho paywall remote config trong React Native SDK" description: "Tải paywall và sản phẩm trong Adapty React Native SDK để tối ưu hóa việc kiếm tiền từ người dùng." --- <SDKv4> Trước khi hiển thị Remote Config và paywall tùy chỉnh, bạn cần tải thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và paywall tùy chỉnh. Để biết hướng dẫn tải flow hoặc paywall được tùy chỉnh trong **Flow Builder** hoặc **Paywall Builder**, vui lòng tham khảo [Tải flow từ Flow Builder và paywall từ Paywall Builder cùng cấu hình của chúng](react-native-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. ::: <details> <summary>Trước khi bắt đầu tải flow và sản phẩm trong ứng dụng mobile của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo một flow hoặc paywall và tích hợp các sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo các placement và tích hợp flow hoặc paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-reactnative) trong ứng dụng di động của bạn. </details> ## Lấy thông tin flow \{#fetch-flow-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các flow và paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một `AdaptyFlow` từ một trong các [placement](placements) của mình bằng phương thức `getFlow`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn cần hardcode là placement ID. Flow được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay một flow trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const flow = await adapty.getFlow(id); // the requested flow } catch (error) { // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |-------------------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyên dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn, bất kể kết nối internet có chập chờn đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ flow và paywall theo hai lớp: cache được cập nhật định kỳ như mô tả ở trên và [paywall dự phòng](react-native-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải flow và paywall nhanh hơn cùng một server dự phòng độc lập phòng trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của flow trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **loadTimeoutMs** | mặc định: 5 giây | <p>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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | :::note Trong v4, `getFlow` không còn nhận tham số `locale` nữa. Đối với các paywall tùy chỉnh, tất cả các ngôn ngữ khả dụng đều được trả về trong Remote Config của flow (`flow.remoteConfigs`) — hãy chọn ngôn ngữ phù hợp với thiết bị hoặc cài đặt ngôn ngữ của người dùng. ::: Đừng hardcode ID sản phẩm! Vì các flow được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (chẳng hạn như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được 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 đó. Nhưng nếu sau này lấy được 3 sản phẩm, ứng dụng phải hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn cần hardcode là placement ID. Tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, các biến thể paywall của nó (`paywalls`), và một mảng `remoteConfigs` (một mục cho mỗi locale đã cấu hình). Để lấy sản phẩm cho flow, hãy gọi `getPaywallProducts(flow)`. | ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có flow, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```typescript showLineNumbers try { // ...flow const products = await adapty.getPaywallProducts(flow); // the requested products list } catch (error) { // handle the error } ``` Tham số phản hồi: | Tham số | Mô tả | | :-------- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi triển khai thiết kế paywall của riêng bạn, bạn có thể sẽ cần truy cập các thuộc tính sau từ đối tượng [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct). Dưới đây là các thuộc tính được sử dụng phổ biến nhất, nhưng hãy tham khảo tài liệu được liên kết để biết đầy đủ chi tiết về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề của sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng bản địa hóa này dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, dùng `product.price?.localizedString`. Bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price?.amount`. Giá trị sẽ được trả về theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price?.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedSubscriptionPeriod`. Bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo cách lập trình, dùng `product.subscription?.subscriptionPeriod`. Từ đó bạn có thể truy cập thuộc tính `unit` để lấy đơn vị thời gian (tức là `'day'`, `'week'`, `'month'`, `'year'` hoặc `'unknown'`). Giá trị `numberOfUnits` sẽ cho bạn biết số đơn vị trong mỗi chu kỳ. Ví dụ: với gói đăng ký theo quý, thuộc tính `unit` sẽ là `'month'` và `numberOfUnits` sẽ là `3`. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ báo rằng gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `paymentMode`: một chuỗi với các giá trị `'free_trial'`, `'pay_as_you_go'`, `'pay_up_front'` và `'unknown'`. Dùng thử miễn phí sẽ có kiểu `'free_trial'`.<br/>• `price`: Giá giảm dưới dạng số. Với dùng thử miễn phí, giá trị này sẽ là `0`.<br/>• `localizedNumberOfPeriods`: một chuỗi đã được bản địa hóa theo ngôn ngữ thiết bị, mô tả thời lượng 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.<br/>• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết từng phần của chu kỳ ưu đãi bằng thuộc tính này. Cách dùng tương tự như phần mô tả ở trên.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo ngôn ngữ của người dùng. | ## Tăng tốc độ tải flow với flow đối tượng mặc định \{#speed-up-flow-fetching-with-default-audience-flow\} Thông thường, flow được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng của bạn có kết nối internet yếu, việc tải flow có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow bằng phương thức `getFlow`, như đã mô tả chi tiết trong phần [Lấy thông tin flow](fetch-paywalls-and-products-react-native#fetch-flow-information) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng targeting cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để đổi lấy tốc độ tải flow nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã được mô tả [ở trên](fetch-paywalls-and-products-react-native#fetch-flow-information). ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const flow = await adapty.getFlowForDefaultAudience(id); // the requested flow } catch (error) { // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng kết nối mạng không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có trải nghiệm tải nhanh hơn, bất kể kết nối mạng của họ có chập chờn đến đâu. Cache được cập nhật thường xuyên, vì vậy việc sử dụng trong phiên làm việc để tránh các yêu cầu mạng là hoàn toàn an toàn.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện xóa thủ công.</p> | </SDKv4> <SDKv3> Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng phần này đề cập đến Remote Config và paywall tùy chỉnh. Để biết hướng dẫn lấy paywall cho các paywall được tùy chỉnh bằng Paywall Builder, vui lòng xem [Lấy paywall Paywall Builder và cấu hình của chúng](react-native-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. ::: <details> <summary>Trước khi bắt đầu lấy paywall và sản phẩm trong ứng dụng di động của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall của bạn](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-reactnative) vào ứng dụng di động của bạn. </details> ## Lấy thông tin paywall \{#fetch-paywall-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của bạn bằng phương thức `getPaywall`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất cứ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt — nếu hôm nay paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywall(id, locale); // the requested paywall } catch (error) { // handle the error } ``` | Tham số | Bắt buộc | Mô tả | |-------------------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản dịch paywall](add-remote-config-locale). Tham số này cần là mã ngôn ngữ gồm một hoặc nhiều subtag được phân tách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là mã ngôn ngữ, subtag thứ hai là mã vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Localizations và locale codes](react-native-localizations-and-locale-codes) để biết thêm thông tin về locale codes và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet có kém đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc được dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](react-native-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</p> | | **loadTimeoutMs** | mặc định: 5 giây | <p>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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | Đừng hardcode product ID! Vì paywall được cấu hình từ xa, các sản phẩm hiển thị, số lượng sản phẩm, và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những tình huống này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị đúng 2 sản phẩm đó. Nhưng nếu sau này lấy được 3 sản phẩm, ứng dụng cũng nên hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn cần hardcode là placement ID. Các tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Đối tượng [`AdaptyPaywall`](https://react-native.adapty.io/interfaces/adaptypaywall) bao gồm: danh sách ID sản phẩm, mã định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy danh sách sản phẩm \{#fetch-products\} Sau khi có paywall, bạn có thể lấy danh sách sản phẩm tương ứng với nó: ```typescript showLineNumbers try { // ...paywall const products = await adapty.getPaywallProducts(paywall); // the requested products list } catch (error) { // handle the error } ``` Tham số trong kết quả trả về: | Tham số | Mô tả | | :-------- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi triển khai thiết kế paywall của riêng bạn, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct). Dưới đây là các thuộc tính được dùng phổ biến nhất — xem tài liệu được liên kết để biết đầy đủ chi tiết 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 việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Price** | Để hiển thị giá đã được bản địa hóa, dùng `product.price?.localizedString`. Việc bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số 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,...), dùng `product.subscription?.localizedSubscriptionPeriod`. Việc bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo chương trình, dùng `product.subscription?.subscriptionPeriod`. Từ đó bạn có thể truy cập thuộc tính `unit` để lấy đơn vị thời gian (tức là `'day'`, `'week'`, `'month'`, `'year'` hoặc `'unknown'`). Giá trị `numberOfUnits` cho biết số đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `'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 nào đó cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `paymentMode`: chuỗi với các giá trị `'free_trial'`, `'pay_as_you_go'`, `'pay_up_front'` và `'unknown'`. Dùng thử miễn phí sẽ có kiểu `'free_trial'`.<br/>• `price`: Giá sau giảm dưới dạng số. Với dùng thử miễn phí, giá trị ở đây là `0`.<br/>• `localizedNumberOfPeriods`: chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày sẽ hiển thị `'3 days'` trong trường này.<br/>• `subscriptionPeriod`: Thay thế, bạn có thể lấy thông tin chi tiết từng phần của chu kỳ ưu đãi bằng thuộc tính này. Cách hoạt động tương tự như mô tả ở phần trước đối với các ưu đãi.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký được định dạng theo ngôn ngữ người dùng cho phần giảm giá. | ## Tăng tốc tải paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ 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ả chi tiết trong phần [Lấy thông tin paywall](fetch-paywalls-and-products-react-native#fetch-paywall-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng targeting cá nhân hóa (bao gồm theo quốc gia, marketing attribution hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` đã được mô tả [ở trên](fetch-paywalls-and-products-react-native#fetch-paywall-information). ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywallForDefaultAudience(id, locale); // the requested paywall } catch (error) { // handle the error } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ React Native SDK phiên bản 2.11.2 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con phân cách bởi ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Bản địa hóa và mã locale](react-native-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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối của họ có chập chờn đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.</p> | </SDKv3> --- # File: present-remote-config-paywalls-react-native --- --- title: "Hiển thị paywall được thiết kế bằng Remote Config trong React Native SDK" description: "Khám phá cách hiển thị paywall Remote Config trong Adapty React Native SDK để cá nhân hóa trải nghiệm người dùng." --- <SDKv4> Nếu bạn đã tùy chỉnh một flow bằng Remote Config, bạn cần triển khai phần render trong code ứng dụng di động để hiển thị flow đó cho người dùng. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn chủ động trong việc quyết định những gì được đưa vào và giao diện flow của bạn trông như thế nào. Chúng tôi cung cấp một phương thức để lấy cấu hình Remote Config, giúp bạn tự do hiển thị flow tùy chỉnh đã được cấu hình qua Remote Config. ## Lấy remote config của flow và hiển thị nó \{#get-flow-remote-config-and-present-it\} Trong v4, một flow chứa một mục `AdaptyRemoteConfig` cho mỗi ngôn ngữ đã được cấu hình trong mảng `remoteConfigs`. Chọn ngôn ngữ phù hợp với tùy chọn của người dùng, sau đó đọc các giá trị cần thiết từ `data` của nó. ```typescript showLineNumbers try { const flow = await adapty.getFlow("YOUR_PLACEMENT_ID"); const config = flow.remoteConfigs?.find((c) => c.lang === "en") ?? flow.remoteConfigs?.[0]; const headerText = config?.data?.["header_text"]; } catch (error) { // handle the error } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, bạn cần render và kết hợp chúng thành một trang trông đẹp mắt. 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 của điện thoại, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy đảm bảo [ghi lại sự kiện xem paywall](present-remote-config-paywalls-react-native#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho funnel và A/B test. ::: Sau khi hoàn tất việc hiển thị flow, hãy tiếp tục thiết lập luồng 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ừ flow của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](react-native-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](react-native-use-fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các flow. Mặc dù chúng tôi tự động thu thập dữ liệu về các giao dịch mua, việc ghi lại lượt xem flow 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 flow. Để ghi lại sự kiện xem flow, chỉ cần gọi `.logShowFlow(flow)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowFlow(flow)` nếu bạn đang hiển thị các flow hoặc paywall được render bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder). Adapty tự động theo dõi lượt xem trong những trường hợp đó. ::: ```typescript showLineNumbers await adapty.logShowFlow(flow); ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :-------- | :------- |:-------------------------------------------------------------------------------------| | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `adapty.getFlow(placementId)`. | </SDKv4> <SDKv3> Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng có thể thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn chủ động trong việc 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 Config, 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ị \{#get-paywall-remote-config-and-present-it\} Để lấy Remote Config của một paywall, truy cập thuộc tính `remoteConfig` và lấy các giá trị cần thiết. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: "YOUR_PLACEMENT_ID" }); const headerText = paywall.remoteConfig?.data?.["header_text"]; } catch (error) { // handle the error } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang trông đẹp mắt. 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 các thiết bị khác nhau. :::warning Hãy nhớ [ghi lại sự kiện xem paywall](present-remote-config-paywalls-react-native#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](react-native-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](react-native-use-fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi dữ liệu về các giao dịch mua được thu thập tự động, việc ghi lại lượt xem paywall cần có sự chủ động từ phía bạn vì chỉ bạn mới biết khi nào người dùng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, hãy gọi `.logShowPaywall(paywall)`, và sự kiện này sẽ được phản ánh trong các chỉ số paywall của bạn trong funnels và A/B test. :::important Bạn không cần gọi `.logShowPaywall(paywall)` nếu đang hiển thị các paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```typescript showLineNumbers await adapty.logShowPaywall(paywall); ``` 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://react-native.adapty.io/interfaces/adaptypaywall). | </SDKv3> --- # File: react-native-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng với React Native SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng là bước quan trọng để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ cần hiển thị paywall là đủ để hỗ trợ mua hàng nếu bạn dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall của mình. Nếu không dùng Paywall Builder, bạn phải sử dụng một phương thức riêng là `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung. Phương thức này là cổng để người dùng tương tác với paywall và tiến hành giao dịch. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm mà người dùng muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng. :::warning Lưu ý rằng ưu đãi giới thiệu chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh tính đủ điều kiện của người dùng để nhận ưu đãi giới thiệu trên iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Bỏ qua bước này có thể dẫn đến ứng dụng bị từ chối khi phát hành. Ngoài ra, điều này có thể khiến người dùng đủ điều kiện nhận ưu đãi giới thiệu bị tính giá đầy đủ. ::: 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 giao dịch mua hàng. ## 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](react-native-implement-paywalls-manually) để có hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // 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://react-native.adapty.io/interfaces/adaptypaywallproduct) lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>Nếu yêu cầu thành công, phản hồi chứa đối tượng này. Đối tượng [AdaptyProfile](https://react-native.adapty.io/interfaces/adaptyprofile) cung cấp thông tin toàn diện về 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.</p><p>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.</p> | :::warning **Lưu ý:** Nếu bạn vẫn đang dùng StoreKit của Apple phiên bản thấp hơn 2.0 và Adapty SDK phiên bản thấp hơn 2.9.0, bạn cần cung cấp [Apple App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple ngừng hỗ trợ. ::: ## 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 xử lý phụ thuộc vào cửa hàng: - Với App Store, gói đăng ký sẽ tự động được cập nhật trong cùng nhóm gói đăng ký. Nếu người dùng mua một gói từ nhóm này trong khi đã có gói từ nhóm khác, cả hai gói đều sẽ hoạt động đồng thời. - Với Google Play, gói đăng ký không tự động được cập nhật. Bạn cần xử lý việc chuyển đổi trong code ứng dụng như mô tả bên dưới. Để thay thế gói đăng ký bằng gói khác trên Android, gọi phương thức `.makePurchase()` với tham số bổ sung: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product, params); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` Tham số yêu cầu bổ sung: | Tham số | Bắt buộc | Mô tả | | :--------- | :------- | :----------------------------------------------------------- | | **params** | bắt buộc | Đối tượng thuộc kiểu [`MakePurchaseParamsInput`](https://react-native.adapty.io/types/makepurchaseparamsinput). | :::info **Phiên bản 3.8.2+**: Cấu trúc `MakePurchaseParamsInput` đã được cập nhật. `oldSubVendorProductId` và `prorationMode` giờ nằm trong `subscriptionUpdateParams`, và `isOfferPersonalized` được chuyển lên cấp trên. Ví dụ: ```javascript makePurchase(product, { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } }); ``` ::: 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ị của Google về chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Chế độ thay thế [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Lưu ý: phương thức này chỉ áp dụng cho việc nâng cấp gói đăng ký. Hạ cấp không được hỗ trợ. - Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Việc thay đổi gói đăng ký thực sự chỉ xảy ra khi chu kỳ thanh toán hiện tại kết thúc. ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} <Details> <summary>Về offer code</summary> Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} :::warning Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. ::: Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác. </Details> Để hiển thị sheet đổi mã trong ứng dụng: ```typescript showLineNumbers adapty.presentCodeRedemptionSheet(); ``` :::danger Qua quan sát của chúng tôi, sheet Offer Code Redemption trong một số ứng dụng có thể hoạt động không ổn định. Chúng tôi khuyến nghị chuyển hướng người dùng trực tiếp đến App Store. Để 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 cho vài tháng), bạn có thể bật [giao dịch đang chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước. ```typescript showLineNumbers adapty.activate("PUBLIC_SDK_KEY", { android: { pendingPrepaidPlansEnabled: true } }); ``` --- # File: react-native-restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng với React Native 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 dùng đã gỡ cài đặt và cài lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập nội dung đã mua trước đó mà không 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 đượ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 dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`: ```typescript showLineNumbers try { const profile = await adapty.restorePurchases(); const isSubscribed = profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // restore access } } catch (error) { // handle the error } ``` Tham số phản hồi: | Tham số | Mô tả | |---------|-----------| | **Profile** | <p>Đối tượng [`AdaptyProfile`](https://react-native.adapty.io/interfaces/adaptyprofile). 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.</p><p>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.</p> | :::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-react-native --- --- title: "Triển khai Observer mode trong React Native SDK" description: "Triển khai observer mode trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong React Native SDK." --- Nếu bạn đã có hệ thống mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp analytics nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics. Nếu điều này phù hợp với nhu cầu của bạn, bạn chỉ cần: 1. Bật nó 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 thiết lập cho [React Native](sdk-installation-reactnative). 2. [Báo cáo các giao dịch](report-transactions-observer-mode-react-native) từ hệ thống mua hàng hiện có của bạn tới Adapty. ### Thiết lập Observer mode \{#observer-mode-setup\} Bật Observer mode nếu bạn tự xử lý việc mua hàng và trạng thái gói đăng ký, và chỉ dùng Adapty để gửi sự kiện gói đăng ký và analytics. :::important Khi chạy ở Observer mode, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều đó. ::: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { observerMode: true, // Enable observer mode }); ``` Các tham số: | Tham số | Mô tả | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean điều khiển [Observer mode](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thêm một số thiết lập trong Observer mode. Đây là những gì bạn cần thực hiện ngoài các bước trên: 1. Hiển thị paywall như bình thường cho [remote config paywalls](present-remote-config-paywalls-react-native). 3. [Liên kết paywall](report-transactions-observer-mode-react-native) với các giao dịch mua hàng. --- # File: report-transactions-observer-mode-react-native --- --- title: "Báo cáo giao dịch trong Observer Mode trên React Native SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trên React Native SDK." --- <Tabs groupId="sdk-version" queryString> <TabItem value="current" label="Adapty SDK v3.4+ (current)" default> Trong Observer mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống thanh toán hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng 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 phân tích. Sử dụng `reportTransaction` để báo cáo từng giao dịch cho 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, giao dịch đó sẽ không xuất hiện trong phân tích 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 đưa `variationId` vào 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 phân tích paywall chính xác. ```typescript showLineNumbers const variationId = paywall.variationId; try { await adapty.reportTransaction(transactionId, variationId); } catch (error) { // handle the `AdaptyError` } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | <ul><li> Với iOS: Định danh của giao dịch.</li><li> Với Android: Định danh 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.</li></ul> | | variationId | tùy chọn | Định danh chuỗi của biến thể. Bạn có thể lấy giá trị này qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://react-native.adapty.io/interfaces/adaptypaywall). | </TabItem> <TabItem value="old" label="Adapty SDK 3.3.x (legacy)" default> Trong Observer mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống thanh toán hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng hoặc khôi phục chúng. Điều quan trọng là phải thiết lập điều này **trước** khi phát hành ứng dụng để tránh sai sót trong phân tích. Sử dụng `reportTransaction` trên cả hai nền tảng để báo cáo 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!** Nếu bạn không gọi các phương thức này, Adapty sẽ không nhận ra giao dịch, giao dịch đó sẽ không xuất hiện trong phân tích 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 đưa `variationId` vào 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 phân tích paywall chính xác. ```typescript showLineNumbers if (Platform.OS === 'android') { try { await adapty.restorePurchases(); } catch (error) { // handle the error } } ... const variationId = paywall.variationId; try { await adapty.reportTransaction(transactionId, variationId); } catch (error) { // handle the `AdaptyError` } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | <ul><li> Với iOS, StoreKit 1: đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).</li><li> Với iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).</li><li> Với Android: Định danh 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.</li></ul> | | variationId | tùy chọn | Định danh chuỗi của biến thể. Bạn có thể lấy giá trị này qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://react-native.adapty.io/interfaces/adaptypaywall). | </TabItem> <TabItem value="old2" label="Adapty SDK up to 3.2.x (legacy)" default> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> **Báo cáo giao dịch** - Các phiên bản đến 3.1.x tự động lắng nghe các giao dịch trên App Store, vì vậy không cần báo cáo thủ công. - Phiên bản 3.2 không hỗ trợ Observer Mode. </TabItem> <TabItem value="kotlin" label="Android and Android-based cross-platforms" default> **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 [Restore Purchases in Mobile Code](react-native-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, giao dịch đó sẽ không xuất hiện trong phân tích và sẽ không được gửi đến các tích hợp. ::: </TabItem> </Tabs> **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 dự đị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 với paywall tương ứng trong code ứng dụng di động của mình. Điều này rất quan trọng cần thực hiện đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến sai sót trong phân tích. ```typescript const variationId = paywall.variationId; try { await adapty.setVariationId('transactionId', variationId); } catch (error) { // handle the `AdaptyError` } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | <p>Với iOS, StoreKit 1: đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).</p><p>Với iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).</p><p>Với Android: Định danh 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.</p> | | variationId | bắt buộc | Định danh chuỗi của biến thể. Bạn có thể lấy giá trị này qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://react-native.adapty.io/interfaces/adaptypaywall). | </TabItem> </Tabs> --- # File: react-native-troubleshoot-purchases --- --- title: "Xử lý sự cố mua hàng trong React Native SDK" description: "Xử lý sự cố mua hàng trong React Native 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 React Native 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 thiết lập Google Play Store chưa hoàn chỉnh hoặc có vấn đề 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 bị 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 trong observer mode \{#adaptyer-ror-cantmakepayments-in-observer-mode\} **Vấn đề**: Bạn nhận được lỗi `AdaptyError.cantMakePayments` khi dùng `makePurchase` ở observer mode. **Nguyên nhân**: Trong observer mode, bạn cần tự xử lý việc mua hàng ở phía mình, không dùng phương thức `makePurchase` của Adapty. **Giải pháp**: Nếu bạn dùng `makePurchase` để xử lý mua hàng, hãy tắt observer mode. Bạn chỉ được chọn một trong hai: dùng `makePurchase` hoặc tự xử lý mua hàng trong observer mode. Xem [Triển khai Observer mode](implement-observer-mode-react-native) để 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 billing 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ể xem thêm thông tin 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**: Điều này thường liên quan đến vấn đề kiểm thử trong 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 đề liên quan đến purchase completion handler trong sandbox. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các sự cố liên quan đến mua hàng khác chưa được đề cập ở trên. **Giải pháp**: Nếu cần, hãy migrate SDK lên phiên bản mới nhất theo [hướng dẫn migration](react-native-sdk-migration-guides). Nhiều vấn đề đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: react-native-identifying-users --- --- title: "Xác định người dùng trong React Native SDK" description: "Tìm hiểu cách xác định người dùng trong ứng dụng React Native với Adapty SDK." --- Adapty tạo một internal profile ID cho mỗi người dùng. Tuy nhiên, nếu bạn có hệ thống xác thực riêng, bạn nên đặt Customer User ID của mình. Bạn có thể tìm người dùng theo Customer User ID trong mục [Profiles](profiles-crm) và sử dụng nó trong [server-side API](getting-started-with-server-side-api), vốn 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 trong quá trình cấu hình, chỉ cần truyền nó làm tham số `customerUserId` vào phương thức `.activate()`: ```typescript showLineNumbers adapty.activate("PUBLIC_SDK_KEY", { customerUserId: "YOUR_USER_ID" }); ``` :::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 trong cấu hình SDK, bạn có thể đặt nó sau bất kỳ lúc nào bằng phương thức `.identify()`. Các trường hợp phổ biến nhất khi dùng phương thức này là sau khi đăng ký hoặc đăng nhập, khi người dùng chuyển từ trạng thái ẩn danh sang người dùng đã xác thực. ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // successfully identified } catch (error) { // handle the error } ``` Tham số yêu cầu: - **Customer User ID** (bắt buộc): chuỗi định danh người dùng. :::warning Gửi lại dữ liệu người dùng quan trọng Trong một số trường hợp, chẳng hạn khi người dùng đăng nhập lại vào tài khoản, máy chủ Adapty đã có thông tin về người dùng đó. Trong các 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 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ũng cần gọi lại tất cả paywall và sản phẩm sau khi xác định người dùng, vì dữ liệu của người dùng mới có thể khác. ::: ### Đăng xuất và đăng nhập \{#logging-out-and-logging-in\} Bạn có thể đăng xuất người dùng bất cứ lúc nào bằng cách gọi phương thức `.logout()`: ```typescript showLineNumbers try { await adapty.logout(); // successful logout } catch (error) { // handle the 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 gắn token này vào mỗi giao dịch, giúp backend của bạn có thể khớp dữ liệu App Store với người dùng. Hãy sử dụng một UUID ổn định được tạo cho mỗi người dùng và tái sử dụng nó cho cùng một tài khoản trên nhiều thiết bị. Điều này đảm bảo rằng các lần mua và thông báo App Store luôn được liên kết chính xác. 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 chỉ truyền token mà không có `customerUserId`, nó sẽ không được đưa vào giao dịch. ::: ```typescript showLineNumbers // During configuration: adapty.activate("PUBLIC_SDK_KEY", { customerUserId: "YOUR_USER_ID", ios: { appAccountToken: "YOUR_APP_ACCOUNT_TOKEN" }, }); // Or when identifying users try { await adapty.identify("YOUR_USER_ID", { ios: {appAccountToken: 'YOUR_APP_ACCOUNT_TOKEN'} }); // successfully identified } catch (error) { // handle the error } ``` ### Đặ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ất định nhằm tăng cường quyền riêng tư và bảo mật cho người dùng. Các ID này giúp Google Play xác định 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 để ngăn chặn 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 phải 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 giao dịch mua mà không làm lộ định danh người dùng thực. ```typescript showLineNumbers // During configuration: adapty.activate("PUBLIC_SDK_KEY", { customerUserId: "YOUR_USER_ID", android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' } }); // Or when identifying users try { await adapty.identify("YOUR_USER_ID", { android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' } }); // successfully identified } catch (error) { // handle the error } ``` ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: react-native-setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong React Native SDK" description: "Tìm hiểu cách cập nhật thuộc tính người dùng và dữ liệu hồ sơ trong ứng dụng React Native của bạn với Adapty SDK." --- 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 của mình. 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 đơ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()`: ```typescript showLineNumbers // Only for TypeScript validation const params: AdaptyProfileParameters = { email: 'email@email.com', phoneNumber: '+18888888888', firstName: 'John', lastName: 'Appleseed', gender: 'other', birthday: new Date().toISOString(), }; try { await adapty.updateProfile(params); } catch (error) { // handle `AdaptyError` } ``` 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 `<Key>` được phép của `AdaptyProfileParameters.Builder` và các giá trị `<Value>` tương ứng được liệt kê bên dưới: | Key | Value | |---|-----| | <p>email</p><p>phoneNumber</p><p>firstName</p><p>lastName</p> | String | | gender | Enum, các giá trị cho phép là: `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 sử dụng ứng dụng. Ví dụ, với ứng dụng thể dục, chúng có thể là số buổi tập mỗi tuần; với ứng dụng học ngôn ngữ, chúng có thể là trình độ kiến thức của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi có mục tiêu, đồng thời dùng trong analytics để tìm ra chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```typescript showLineNumbers try { await adapty.updateProfile({ codableCustomAttributes: { key_1: 'value_1', key_2: 2, }, }); } catch (error) { // handle `AdaptyError` } ``` Để xóa một key hiện có, sử dụng phương thức `.withRemoved(customAttributeForKey:)`: ```typescript showLineNumbers try { // to remove a key, pass null as its value await adapty.updateProfile({ codableCustomAttributes: { key_1: null, key_2: null, }, }); } catch (error) { // handle `AdaptyError` } ``` Đô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 sử dụng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không phải là mới nhất, vì thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau vào bất kỳ lúc nào, nên các thuộc tính trên server có thể đã thay đổi sau lần đồng bộ cuối cùng. ::: ### Giới hạn \{#limits\} - Tối đa 30 thuộc tính tùy chỉnh mỗi người dùng - Tên key dài 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 bất kỳ ký tự nào sau đây: `_` `-` `.` - Giá trị có thể là chuỗi hoặc số thực (float) với tối đa 50 ký tự. --- # File: react-native-listen-subscription-changes --- --- title: "Kiểm tra trạng thái gói đăng ký trong React Native SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng trong ứng dụng React Native của bạn." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên rất đơn giản. Bạn không cần phải chèn thủ công các 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. <details> <summary>Trước khi bắt đầu kiểm tra trạng thái gói đăng ký (Nhấn để mở rộng)</summary> - Đối với iOS, hãy thiết lập [App Store Server Notifications](enable-app-store-server-notifications) - Đối với Android, hãy thiết lập [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn) </details> ## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là các thuộc tính của đối tượng [AdaptyProfile](https://react-native.adapty.io/interfaces/adaptyprofile). Chúng tôi khuyến nghị lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](react-native-identifying-users#setting-customer-user-id-on-configuration), rồi cập nhật lại mỗi khi có thay đổi. Bằng cách này, bạn có thể sử dụng đối tượng hồ sơ mà không cần liên tục gửi yêu cầu lấy dữ liệu. Để nhận thông báo khi hồ sơ người dùng được cập nhật, hãy lắng nghe các thay đổi như mô tả trong phần [Lắng nghe cập nhật hồ sơ, bao gồm mức độ truy cập](react-native-listen-subscription-changes) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ máy chủ \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ máy chủ, sử dụng phương thức `.getProfile()`: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); } catch (error) { // handle the error } ``` Các tham số phản hồi: | Tham số | Mô tả | | --------- | ------------------------------------------------------------ | | Profile | <p>Một đối tượng [AdaptyProfile](https://react-native.adapty.io/interfaces/adaptyprofile). 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ơ để xác định xem người dùng có quyền truy cập premium vào ứng dụng hay không.</p><p></p><p>Phương thức `.getProfile` cung cấp kết quả mới nhất vì nó luôn cố gắng truy vấn API. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ máy chủ, dữ liệu từ bộ nhớ cache sẽ được trả về. Cần lưu ý rằng Adapty SDK cập nhật cache của `AdaptyProfile` thường xuyên để giữ thông tin này luôn cập nhật nhất có thể.</p> | Phương thức `.getProfile()` cung cấp hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Bạn có thể có nhiều mức độ truy cập trong một ứng dụng. Ví dụ: nếu bạn có ứng dụng báo và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Nhưng hầu hết thời gian, bạn chỉ cần một mức độ truy cập — trong trường hợp đó, bạn có thể dùng mức độ truy cập mặc định "premium". Dưới đây là ví dụ kiểm tra mức độ truy cập "premium" mặc định: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); const isActive = profile.accessLevels?.["premium"]?.isActive; if (isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` ### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận tin nhắn từ Adapty, bạn cần thực hiện một số cấu hình bổ sung: ```typescript showLineNumbers // Create an "onLatestProfileLoad" event listener adapty.addEventListener('onLatestProfileLoad', 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 gói đăng ký \{#subscription-status-cache\} Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi máy chủ không khả dụng, dữ liệu được lưu trong cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ. 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 máy chủ mỗi phút để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ. 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 nó luôn nhất quán với máy chủ. --- # File: react-native-deal-with-att --- --- title: "Xử lý ATT trong React Native SDK" description: "Bắt đầu với Adapty trên React Native để đơ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. ```typescript showLineNumbers try { await adapty.updateProfile({ // you can also pass a string value (validated via tsc) if you prefer appTrackingTransparencyStatus: AppTrackingTransparencyStatus.Authorized, }); } catch (error) { // handle `AdaptyError` } ``` :::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 chuyển đến các tích hợp bạn đã cấu hình một cách kịp thời. ::: --- # File: kids-mode-react-native --- --- title: "Chế độ Kids trong React Native SDK" description: "Dễ dàng bật Chế độ Kids để 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 React Native SDK." --- Nếu ứng dụng React Native của bạn dành cho trẻ em, bạn phải tuân theo 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 bạn đang sử dụng Adapty SDK, chỉ cần vài bước đơn giản để cấu hình SDK phù hợp với các chính sách này và vượt qua quá trình kiểm duyệt của cửa hàng ứng dụng. :::important Trên iOS, Chế độ Trẻ em được bật thông qua trait Swift package `KidsMode`, giúp loại bỏ toàn bộ code liên quan đến IDFA, AdSupport và AppTrackingTransparency. Tính năng này yêu cầu SDK v4 (cài đặt iOS SDK gốc qua Swift Package Manager) và **Xcode 26** trở lên. Xem [Cập nhật trong iOS Podfile của bạn](#updates-in-your-ios-podfile) bên dưới. ::: ## Cần cấu hình những gì? \{#whats-required\} Bạn cần cấu hình SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách thận trọng. User ID có định dạng `<FirstName.LastName>` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc sử dụng email. Đối với Kids Mode, phương pháp tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: hashed ID hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ. ## Bật Chế Độ Kids Mode \{#enabling-kids-mode\} ### Cập nhật trong 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, hãy vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** trong mục **Collect users' IP address**. ### Cập nhật trong code ứng dụng di động của bạn \{#updates-in-your-mobile-app-code\} Để tuân thủ các chính sách, hãy tắt tính năng thu thập IDFA (iOS), GAID/AAID (Android) và địa chỉ IP của người dùng khi bạn kích hoạt Adapty SDK: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { // Disable IP address collection ipAddressCollectionDisabled: true, // Disable IDFA collection on iOS ios: { idfaCollectionDisabled: true, }, // Disable Google Advertising ID collection on Android android: { adIdCollectionDisabled: true, }, }); ``` ### Cập nhật Podfile cho iOS \{#updates-in-your-ios-podfile\} Để đáp ứng yêu cầu của danh mục Kids trên App Store (hoặc tuân thủ COPPA), native iOS SDK phải được build với trait Swift package `KidsMode`, giúp loại bỏ toàn bộ code liên quan đến IDFA, AdSupport và AppTrackingTransparency. React Native cài đặt native SDK thông qua Swift Package Manager, vốn không thể chuyển tiếp package traits, nên SDK cung cấp một helper cho Podfile để tự động áp dụng trait đó. Bước này yêu cầu **Xcode 26** trở lên. Trong `ios/Podfile`, hãy require helper và gọi nó **sau** `react_native_post_install`: ```ruby showLineNumbers title="ios/Podfile" require Pod::Executable.execute_command('node', ['-p', 'require.resolve( "react-native-adapty/ios/adapty_kids_mode.rb", {paths: [process.argv[1]]}, )', __dir__]).strip # ... post_install do |installer| react_native_post_install( installer, config[:reactNativePath], :mac_catalyst_enabled => false ) adapty_enable_kids_mode(installer) end ``` Sau đó chạy `pod install`: ```sh showLineNumbers title="Shell" cd ios && pod install ``` Để xác nhận Kids Mode đang hoạt động, kiểm tra dòng log `adapty.activate(...)` có ghi `kids_mode_enabled: true`. Giữ lời gọi helper trong `post_install` mãi mãi — React Native tạo lại các tham chiếu Swift package mỗi lần chạy `pod install`, và helper sẽ áp dụng lại trait mỗi lần như vậy. ### Cập nhật trong Android manifest của bạn \{#updates-in-your-android-manifest\} :::note Nếu ứng dụng của bạn chỉ nhắm đến đối tượng trẻ em **và** biên dịch với Android 13 (API 33) trở lên, Google Play yêu cầu bạn không được yêu cầu quyền `AD_ID`. Một SDK khác trong ứng dụng của bạn (analytics, attribution, hoặc quảng cáo) có thể thêm quyền này thông qua manifest merging. Việc đặt `adIdCollectionDisabled` ngăn Adapty thu thập ID, nhưng không xóa được quyền mà một SDK khác đã khai báo. ::: Để xóa quyền này, thêm đoạn sau vào bên trong phần tử `<manifest>` của `android/app/src/main/AndroidManifest.xml`. Phần tử `<manifest>` phải khai báo `xmlns:tools="http://schemas.android.com/tools"`. ```xml showLineNumbers title="AndroidManifest.xml" <uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" /> ``` --- # File: react-native-get-onboardings --- --- title: "Lấy onboarding trong React Native SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho React Native." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](react-native-get-pb-paywalls) thay thế: không giống như onboarding chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](react-native-get-pb-paywalls) và [Hiển thị flows & paywalls](react-native-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding của mình](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng React Native của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement và cấu hình view của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty React Native SDK](sdk-installation-reactnative) 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). ## Tải onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng công cụ xây dựng không cần code của chúng tôi, nó được lưu trữ dưới dạng một container chứa cấu hình mà ứng dụng của bạn cần tải về và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung hiển thị, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần phải tự tích hợp tính năng theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy tải cấu hình onboarding sớm để hình ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy onboarding, hãy sử dụng phương thức `getOnboarding`: ```typescript showLineNumbers try { const placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const onboarding = await adapty.getOnboarding(placementId, locale); // the requested onboarding } catch (error) { // handle the error } ``` Sau đó, gọi phương thức `createOnboardingView` để tạo một instance view. :::warning Kết quả của phương thức `createOnboardingView` chỉ có thể sử dụng một lần. Nếu cần dùng lại, hãy gọi lại phương thức `createOnboardingView` từ đầu. Gọi hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```typescript showLineNumbers // for the Adapty SDK < 3.14 – import {createOnboardingView} from 'react-native-adapty/dist/ui'; if (onboarding.hasViewConfiguration) { try { const view = await createOnboardingView(onboarding); } catch (error) { // handle the error } } else { //use your custom logic } ``` 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh bản địa hóa của onboarding. Tham số này là mã ngôn ngữ gồm một hoặc hai thẻ con được ngăn cách bởi ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>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.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng thường gặp kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn dù kết nối internet có kém đến đâu. Cache được cập nhật thường xuyên nên có thể dùng trong suốt phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như đã mô tả ở trên và các onboarding dự phòng. Chúng tôi cũng 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 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 onboarding, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.</p> | | **loadTimeoutMs** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên trong.</p> | Tham số phản hồi: | Tham số | Mô tả | |:--------|:------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://react-native.adapty.io/interfaces/adaptyonboarding) 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 độ tải onboarding với onboarding mặc định cho đối tượng \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc tải onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này sẽ lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như đã trình bày chi tiết trong phần [Lấy Onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, đòi hỏi phải thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không chính xác. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ khả năng nhắm mục tiêu theo quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ tải nhanh hơn quan trọng 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ư hướng dẫn bên dưới. Nếu không, hãy dùng `getOnboarding` như đã mô tả [ở trên](#fetch-onboarding). ::: ```typescript showLineNumbers try { const placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const onboarding = await adapty.getOnboardingForDefaultAudience(placementId, locale); // onboarding được yêu cầu } catch (error) { // xử lý lỗi } ``` 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của bản địa hóa onboarding. Tham số này cần là mã ngôn ngữ gồm một hoặc hai thẻ con phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>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.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp tình trạng 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ó tốc độ tải nhanh hơn dù kết nối internet không ổn định. Cache được cập nhật thường xuyên nên an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng.</p><p></p><p>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 cài đặt lại ứng dụng hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập 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 hạn chế.</p> | --- # File: react-native-present-onboardings --- --- title: "Hiển thị onboarding trong React Native SDK" description: "Khám phá cách hiển thị onboarding trên React Native để tăng tỷ lệ chuyển đổi và doanh thu." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong một bản phát hành tương lai.** Chúng không còn nhận được các bản sửa lỗi hoặc cải tiến nữa. Hãy sử dụng [flows](react-native-get-pb-paywalls) thay thế: không giống như onboardings chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn, và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](react-native-get-pb-paywalls) và [Hiển thị flows & paywalls](react-native-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Onboarding đó chứa cả nội dung cần hiển thị lẫn cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty React Native SDK](sdk-installation-reactnative) 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 React Native SDK cung cấp hai cách để hiển thị onboarding: - **React component**: Embedded component cho phép bạn tích hợp vào kiến trúc và hệ thống điều hướng của ứng dụng. - **Modal presentation** ## React component \{#react-component\} Để nhúng một onboarding vào cây component hiện có của bạn, hãy sử dụng component `AdaptyOnboardingView` trực tiếp trong cấu trúc phân cấp component React Native. Component nhúng này cho phép bạn tích hợp nó vào kiến trúc và hệ thống điều hướng của ứng dụng. :::note Trên Android, chúng tôi khuyến nghị cấu hình bổ sung cho `AdaptyOnboardingView` để tránh hiện tượng lỗi hiển thị trực quan. Xem [Giao diện hệ thống chồng lên nội dung onboarding trên Android](#system-ui-overlaps-onboarding-content-on-android). ::: <Tabs groupId="version" queryString> <TabItem value="new" label="SDK phiên bản 3.14 trở lên" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const onAnalytics = useCallback<OnboardingEventHandlers['onAnalytics']>((event, meta) => {}, []); const onClose = useCallback<OnboardingEventHandlers['onClose']>((actionId, meta) => {}, []); const onCustom = useCallback<OnboardingEventHandlers['onCustom']>((actionId, meta) => {}, []); const onPaywall = useCallback<OnboardingEventHandlers['onPaywall']>((actionId, meta) => {}, []); const onStateUpdated = useCallback<OnboardingEventHandlers['onStateUpdated']>((action, meta) => {}, []); const onFinishedLoading = useCallback<OnboardingEventHandlers['onFinishedLoading']>((meta) => {}, []); const onError = useCallback<OnboardingEventHandlers['onError']>((error) => {}, []); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} onAnalytics={onAnalytics} onClose={onClose} onCustom={onCustom} onPaywall={onPaywall} onStateUpdated={onStateUpdated} onFinishedLoading={onFinishedLoading} onError={onError} /> ); } ``` </TabItem> <TabItem value="old" label="Phiên bản SDK < 3.14" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { return ( <AdaptyOnboardingView onboarding={onboarding} style={{ flex: 1 }} eventHandlers={{ onAnalytics(event, meta) { // Handle analytics events }, onClose(actionId, meta) { // Handle close actions }, onCustom(actionId, meta) { // Handle custom actions }, onPaywall(actionId, meta) { // Handle paywall actions }, onStateUpdated(action, meta) { // Handle state updates }, onFinishedLoading(meta) { // Handle when onboarding finishes loading }, onError(error) { // Handle errors }, }} /> ); } ``` </TabItem> </Tabs> ## Hiển thị dạng modal \{#modal-presentation\} Để hiển thị onboarding dưới dạng màn hình độc lập mà người dùng có thể đóng lại, sử dụng phương thức `view.present()` trên `view` đượ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 `view` mới. :::warning Không được tái sử dụng cùng một `view` mà không tạo lại. Việc này sẽ dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: <Tabs groupId="version" queryString> <TabItem value="new" label="SDK version 3.14 or later" default> ```typescript showLineNumbers title="React Native (TSX)" const view = await createOnboardingView(onboarding); // Optional: handle onboarding events (close, custom actions, etc) // view.setEventHandlers({ ... }); try { await view.present(); } catch (error) { // handle the error } ``` </TabItem> <TabItem value="old" label="SDK version < 3.14" default> ```typescript showLineNumbers title="React Native (TSX)" const view = await createOnboardingView(onboarding); view.setEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` </TabItem> </Tabs> ### Cấu hình kiểu hiển thị 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 các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. ```typescript showLineNumbers try { await view.present({ iosPresentationStyle: 'page_sheet' }); } catch (error) { // handle the error } ``` ## Loader trong quá trình hiển thị onboarding \{#loader-during-onboarding\} Khi hiển thị onboarding trong React Native, bạn có thể thấy một màn hình trắng hoặc màn hình loading ngắn xuất hiện trước khi onboarding hiện ra. Điều này xảy ra trong khi native view bên dưới đang được khởi tạo. Bạn có thể xử lý vấn đề này theo nhiều cách khác nhau tùy thuộc vào nhu cầu và quy trình làm việc của bạn. #### Kiểm soát splash screen bằng onFinishedLoading \{#control-splash-screen-using-onfinishedloading\} :::note Cách này chỉ khả dụng khi sử dụng React component. Không áp dụng được cho modal presentation. ::: Cách được khuyến nghị cho React Native là giữ màn hình chờ (splash screen) hoặc lớp phủ tùy chỉnh hiển thị cho đến khi onboarding tải xong hoàn toàn, sau đó ẩn nó đi theo cách thủ công. Khi sử dụng React component (`AdaptyOnboardingView`), hãy chờ sự kiện `onFinishedLoading` trước khi ẩn splash screen hoặc lớp phủ: <Tabs groupId="version" queryString> <TabItem value="new" label="SDK phiên bản 3.14 trở lên" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const [isLoading, setIsLoading] = useState(true); const onFinishedLoading = useCallback<OnboardingEventHandlers['onFinishedLoading']>((meta) => { // Hide your splash screen or custom overlay here setIsLoading(false); }, []); return ( <> <AdaptyOnboardingView onboarding={onboarding} onFinishedLoading={onFinishedLoading} // ... other callbacks /> {isLoading && <YourCustomLoadingOverlay />} </> ); } ``` </TabItem> <TabItem value="old" label="Phiên bản SDK < 3.14"> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const [isLoading, setIsLoading] = useState(true); return ( <> <AdaptyOnboardingView onboarding={onboarding} eventHandlers={{ onFinishedLoading(meta) { // Hide your splash screen or custom overlay here setIsLoading(false); }, // ... other handlers }} /> {isLoading && <YourCustomLoadingOverlay />} </> ); } ``` </TabItem> </Tabs> #### Tùy chỉnh native loader \{#customize-native-loader\} :::important Expo-managed workflow không hỗ trợ việc đặt các layout native tùy chỉnh (ví dụ: `res/layout` trên Android). Đối với ứng dụng Expo, cách duy nhất khả thi là kiểm soát splash screen hoặc sử dụng React Native overlay. ::: Bạn có thể thay thế native loader bằng các layout theo từng nền tảng trên Android và iOS. Nếu bạn đang sử dụng modal presentation, đây là lựa chọn duy nhất của bạn. Tuy nhiên, cách tiếp cận này thường kém tiện lợi hơn đối với ứng dụng React Native: - Yêu cầu triển khai riêng biệt cho Android và iOS - Không tương thích với Expo-managed workflow Xác định placeholder cho từng nền tảng: - **iOS**: Thêm `AdaptyOnboardingPlaceholderView.xib` vào dự án Xcode của bạn. [Tìm hiểu thêm](ios-present-onboardings#add-smooth-transitions-between-the-splash-screen-and-onboarding). - **Android**: Tạo `adapty_onboarding_placeholder_view.xml` trong `res/layout` và định nghĩa placeholder tại đó. [Tìm hiểu thêm](android-present-onboardings#add-smooth-transitions-between-the-splash-screen-and-onboarding). ## Tùy chỉnh cách mở liên kết trong onboardings \{#customize-how-links-open-in-onboardings\} :::important Tính năng tùy chỉnh cách mở liên kết trong onboardings được hỗ trợ từ Adapty SDK v3.15.1 trở lên. ::: Theo mặc định, các liên kết trong onboardings sẽ mở trong trình duyệt nội bộ của ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị các trang web ngay trong ứng dụng, giúp họ xem nội dung mà không cần rời khỏi app. Nếu bạn muốn mở liên kết bằng trình duyệt bên ngoài, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `WebPresentation.BrowserOutApp`: <Tabs groupId="rn-onboarding-views" queryString> <TabItem value="component" label="React component" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const onAnalytics = useCallback<OnboardingEventHandlers['onAnalytics']>((event, meta) => {}, []); const onClose = useCallback<OnboardingEventHandlers['onClose']>((actionId, meta) => {}, []); const onCustom = useCallback<OnboardingEventHandlers['onCustom']>((actionId, meta) => {}, []); const onPaywall = useCallback<OnboardingEventHandlers['onPaywall']>((actionId, meta) => {}, []); const onStateUpdated = useCallback<OnboardingEventHandlers['onStateUpdated']>((action, meta) => {}, []); const onFinishedLoading = useCallback<OnboardingEventHandlers['onFinishedLoading']>((meta) => {}, []); const onError = useCallback<OnboardingEventHandlers['onError']>((error) => {}, []); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} externalUrlsPresentation={WebPresentation.BrowserOutApp} // default – BrowserInApp onAnalytics={onAnalytics} onClose={onClose} onCustom={onCustom} onPaywall={onPaywall} onStateUpdated={onStateUpdated} onFinishedLoading={onFinishedLoading} onError={onError} /> ); } ``` </TabItem> <TabItem value="modal" label="Modal presentation"> ```typescript showLineNumbers title="React Native (TSX)" const view = await createOnboardingView( onboarding, { externalUrlsPresentation: WebPresentation.BrowserOutApp } // default – BrowserInApp ); try { await view.present(); } catch (error) { // handle the error } ``` </TabItem> </Tabs> ## Xử lý sự cố \{#troubleshooting\} ### Giao diện hệ thống che khuất nội dung onboarding trên Android \{#system-ui-overlaps-onboarding-content-on-android\} :::note Cài đặt này chỉ được hỗ trợ trong các dự án React Native thuần (bare). Nếu bạn đang dùng Expo managed workflow, bạn không thể thêm Android resource này trực tiếp. Để áp dụng cài đặt này, bạn cần tạo một custom Expo config plugin để thêm Android resource tương ứng và đăng ký nó trong `app.config.js`. Điều này là bắt buộc vì Expo quản lý native Android project cho bạn. ::: Khi sử dụng `AdaptyOnboardingView` trên Android, các thành phần giao diện hệ thống như thanh trạng thái và thanh điều hướng có thể hiển thị đè lên nội dung paywall. Để tránh điều này, hãy thêm tài nguyên boolean sau vào ứng dụng của bạn: 1. Truy cập `android/app/src/main/res/values`. Nếu chưa có file `bools.xml`, hãy tạo mới. 2. Thêm tài nguyên sau: ```xml <resources> <bool name="adapty_onboarding_enable_safe_area_paddings">false</bool> </resources> ``` Lưu ý rằng thay đổi này áp dụng toàn cục cho tất cả các onboarding trong ứng dụng của bạn. ## Các bước tiếp theo \{#next-steps\} Sau khi hiển thị onboarding, bạn sẽ muốn [xử lý các tương tác và sự kiện của người dùng](react-native-handling-onboarding-events). Tìm hiểu cách xử lý các sự kiện onboarding để phản hồi hành động của người dùng và theo dõi analytics. --- # File: react-native-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong React Native SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong React Native bằng Adapty." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](react-native-get-pb-paywalls) thay thế: không giống như onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](react-native-get-pb-paywalls) và [Hiển thị flows & paywalls](react-native-present-paywalls) để bắt đầu. ::: Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể xử lý. Cách xử lý những sự kiện này phụ thuộc vào phương pháp hiển thị bạn đang dùng: - **Trình bày dạng Modal**: Yêu cầu thiết lập các event handler để xử lý sự kiện cho tất cả các onboarding view - **React component**: Xử lý sự kiện thông qua các tham số callback trực tiếp trong widget Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty React Native SDK](sdk-installation-reactnative) 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). Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình onboarding trong ứng dụng di động của bạn, hãy triển khai các event handler: <Tabs groupId="version" queryString> <TabItem value="new" label="SDK version 3.14 or later" default> <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Đối với React component, bạn xử lý các sự kiện thông qua các prop xử lý sự kiện riêng lẻ trong component `AdaptyOnboardingView`: ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const onAnalytics = useCallback<OnboardingEventHandlers['onAnalytics']>((event, meta) => {}, []); const onClose = useCallback<OnboardingEventHandlers['onClose']>((actionId, meta) => {}, []); const onCustom = useCallback<OnboardingEventHandlers['onCustom']>((actionId, meta) => {}, []); const onPaywall = useCallback<OnboardingEventHandlers['onPaywall']>((actionId, meta) => {}, []); const onStateUpdated = useCallback<OnboardingEventHandlers['onStateUpdated']>((action, meta) => {}, []); const onFinishedLoading = useCallback<OnboardingEventHandlers['onFinishedLoading']>((meta) => {}, []); const onError = useCallback<OnboardingEventHandlers['onError']>((error) => {}, []); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} onAnalytics={onAnalytics} onClose={onClose} onCustom={onCustom} onPaywall={onPaywall} onStateUpdated={onStateUpdated} onFinishedLoading={onFinishedLoading} onError={onError} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Đối với modal presentation, hãy triển khai phương thức event handlers. :::important Việc gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã thiết lập trước đó cho những sự kiện cụ thể đó. ::: ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.setEventHandlers({ onAnalytics(event, meta) { // Track analytics events }, onClose(actionId, meta) { // Handle close action view.dismiss(); return true; }, onCustom(actionId, meta) { // Handle custom actions }, onPaywall(actionId, meta) { // Handle paywall actions }, onStateUpdated(action, meta) { // Handle user input updates }, onFinishedLoading(meta) { // Onboarding finished loading }, onError(error) { // Handle loading errors }, }); try { await view.present(); } catch (error) { // handle the error } ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14"> Đối với SDK version < 3.14, chỉ hỗ trợ modal presentation: ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.registerEventHandlers({ onAnalytics(event, meta) { // Track analytics events }, onClose(actionId, meta) { // Handle close action view.dismiss(); return true; }, onCustom(actionId, meta) { // Handle custom actions }, onPaywall(actionId, meta) { // Handle paywall actions }, onStateUpdated(action, meta) { // Handle user input updates }, onFinishedLoading(meta) { // Onboarding finished loading }, onError(error) { // Handle loading errors }, }); try { await view.present(); } catch (error) { // handle the error } ``` </TabItem> </Tabs> ## Các loại sự kiện \{#event-types\} Các phần dưới đây 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ị bạn đang dùng là gì. ### Xử lý custom action \{#handle-custom-actions\} Trong builder, bạn có thể thêm action **custom** cho một nút và gán cho nó một ID. <img src="/assets/shared/img/ios-events-1.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. Ví dụ: nếu người dùng nhấn vào một nút tùy chỉnh như **Login** hay **Allow notifications**, event handler sẽ được kích hoạt với tham số `actionId` khớp với **Action ID** trong builder. Bạn có thể tự tạo ID theo ý muốn, chẳng hạn như "allowNotifications". <Tabs groupId="version" queryString> <TabItem value="new" label="SDK version 3.14 or later" default> <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const onCustom = useCallback<OnboardingEventHandlers['onCustom']>((actionId, meta) => { switch (actionId) { case 'login': login(); break; case 'allow_notifications': allowNotifications(); break; } }, []); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} onCustom={onCustom} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.setEventHandlers({ onCustom(actionId, meta) { switch (actionId) { case 'login': login(); break; case 'allow_notifications': allowNotifications(); break; } }, }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.registerEventHandlers({ onCustom(actionId, meta) { switch (actionId) { case 'login': login(); break; case 'allow_notifications': allowNotifications(); break; } }, }); ``` </TabItem> </Tabs> <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "actionId": "allow_notifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ``` </Details> ### Hoàn tất tải onboarding \{#finishing-loading-onboarding\} Khi một onboarding hoàn tất tải, sự kiện này sẽ được kích hoạt: <Tabs groupId="version" queryString> <TabItem value="new" label="SDK phiên bản 3.14 trở lên" default> <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const onFinishedLoading = useCallback<OnboardingEventHandlers['onFinishedLoading']>((meta) => { console.log('Onboarding loaded:', meta.onboardingId); }, []); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} onFinishedLoading={onFinishedLoading} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.setEventHandlers({ onFinishedLoading(meta) { console.log('Onboarding loaded:', meta.onboardingId); }, }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.registerEventHandlers({ onFinishedLoading(meta) { console.log('Onboarding loaded:', meta.onboardingId); }, }); ``` </TabItem> </Tabs> <Details> <summary>Ví dụ sự kiện (Nhấp để mở rộng)</summary> ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ``` </Details> ### Đóng onboarding \{#closing-onboarding\} Onboarding được coi là đã đóng khi người dùng nhấn vào một nút có gán hành động **Close**. <img src="/assets/shared/img/ios-events-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important Lưu ý rằng bạn cần tự xử lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ, bạn cần dừng hiển thị chính onboarding đó. ::: <Tabs groupId="version" queryString> <TabItem value="new" label="SDK phiên bản 3.14 trở lên" default> <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding, navigation }) { const onClose = useCallback<OnboardingEventHandlers['onClose']>((actionId, meta) => { navigation.goBack(); }, [navigation]); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} onClose={onClose} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.setEventHandlers({ onClose(actionId, meta) { await view.dismiss(); return true; }, }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.registerEventHandlers({ onClose(actionId, meta) { await view.dismiss(); return true; }, }); ``` </TabItem> </Tabs> <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ``` </Details> ### Mở một 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 nó đã đóng, có cách đơn giản hơn – xử lý hành động đóng và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall. <Tabs groupId="version" queryString> <TabItem value="new" label="SDK version 3.14 or later" default> <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const onPaywall = useCallback<OnboardingEventHandlers['onPaywall']>((actionId, meta) => { openPaywall(actionId); }, []); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} onPaywall={onPaywall} /> ); } const openPaywall = async (placementId) => { // Implement your paywall opening logic here }; ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Lưu ý rằng trên iOS, chỉ có thể hiển thị một view (paywall hoặc onboarding) trên màn hình tại một thời điểm. Nếu bạn hiển thị một paywall chồng lên onboarding, bạn không thể điều khiển onboarding ở phía sau theo cách lập trình. Việc cố đóng onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn đóng onboarding view trước khi hiển thị paywall. ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.setEventHandlers({ onPaywall(actionId, meta) { view.dismiss().then(() => { openPaywall(actionId); }); }, }); const openPaywall = async (placementId) => { // Implement your paywall opening logic here }; ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14"> Lưu ý rằng, trên iOS, chỉ có thể hiển thị một view (paywall hoặc onboarding) trên màn hình tại một thời điểm. Nếu bạn hiển thị một paywall chồng lên một onboarding, bạn không thể điều khiển onboarding ở phía sau theo cách lập trình. Việc cố gắng đóng onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn còn hiển thị. Để tránh điều này, hãy luôn đóng view onboarding trước khi hiển thị paywall. ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.registerEventHandlers({ onPaywall(actionId, meta) { view.dismiss().then(() => { openPaywall(actionId); }); }, }); const openPaywall = async (placementId) => { // Implement your paywall opening logic here }; ``` </TabItem> </Tabs> <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ``` </Details> ### Theo dõi điều hướng \{#tracking-navigation\} Bạn nhận được một sự kiện analytics khi các sự kiện liên quan đến điều hướng xảy ra trong flow onboarding: <Tabs groupId="version" queryString> <TabItem value="new" label="SDK phiên bản 3.14 trở lên" default> <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> ```typescript showLineNumbers title="React Native (TSX)" function MyOnboarding({ onboarding }) { const onAnalytics = useCallback<OnboardingEventHandlers['onAnalytics']>((event, meta) => { trackEvent(event.name, meta.onboardingId); }, []); return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} onAnalytics={onAnalytics} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.setEventHandlers({ onAnalytics(event, meta) { trackEvent(event.name, meta.onboardingId); }, }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14"> ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.registerEventHandlers({ onAnalytics(event, meta) { trackEvent(event.name, meta.onboardingId); }, }); ``` </TabItem> </Tabs> Đối tượng `event` có thể là một trong các kiểu sau: | Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình hoàn tất. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn tất) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `secondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, [hãy gán ID `final` cho màn hình cuối cùng](design-onboarding). | | `unknown` | Dành cho bất kỳ loại sự kiện không được nhận dạng nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (siêu dữ liệu bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Mã định danh duy nhất của flow onboarding | | `screenClientId` | Mã đị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 | <Details> <summary>Ví dụ về sự kiện (Nhấn để mở rộng)</summary> ```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 } } ``` </Details> --- # File: react-native-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong React Native SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng React Native của bạn với Adapty SDK." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](react-native-get-pb-paywalls) thay thế: không giống như onboarding chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại hiệu ứng chuyển động mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](react-native-get-pb-paywalls) và [Hiển thị flows & paywalls](react-native-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời câu hỏi trong quiz hoặc nhập dữ liệu vào ô input, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường dữ liệu đó trong code của mình. Ví dụ: ```javascript // Full-screen presentation const unsubscribe = view.setEventHandlers({ onStateUpdated(action, meta) { // Process data }, }); // Embedded widget <AdaptyOnboardingView onboarding={onboarding} eventHandlers={{ onStateUpdated(action, meta) { // Process data }, }} /> ``` Xem định dạng action [tại đây](https://react-native.adapty.io/types/onboardingstateupdatedaction). <Details> <summary>Ví dụ dữ liệu đã lưu (định dạng có thể khác nhau tùy cách triển khai của bạn)</summary> ```javascript // Example of a saved select action { "elementId": "preference_selector", "elementType": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" }, "meta": { "onboardingId": "onboarding_123", "screenClientId": "preferences_screen", "screenIndex": 1, "totalScreens": 3 } } // Example of a saved multi-select action { "elementId": "interests_selector", "elementType": "multi_select", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ], "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "totalScreens": 3 } } // Example of a saved input action { "elementId": "name_input", "elementType": "input", "value": { "type": "text", "value": "John Doe" }, "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "totalScreens": 3 } } // Example of a saved date picker action { "elementId": "birthday_picker", "elementType": "date_picker", "value": { "day": 15, "month": 6, "year": 1990 }, "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "totalScreens": 3 } } ``` </Details> ## Các trường hợp sử dụng \{#use-cases\} ### Làm phong phú hồ sơ người dùng với dữ liệu \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ hai lần cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](react-native-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à bạn muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code của ứng dụng, nó có thể trông như thế này: ```javascript showLineNumbers // Full-screen presentation const unsubscribe = view.setEventHandlers({ onStateUpdated(action, meta) { // Store user preferences or responses if (action.elementType === 'input') { const profileParams = {}; // Map elementId to appropriate profile field switch (action.elementId) { case 'name': if (action.value.type === 'text') { profileParams.firstName = action.value.value; } break; case 'email': if (action.value.type === 'email') { profileParams.email = action.value.value; } break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile(profileParams).catch(error => { // handle the error }); } } }, }); // Embedded widget <AdaptyOnboardingView onboarding={onboarding} eventHandlers={{ onStateUpdated(action, meta) { // Store user preferences or responses if (action.elementType === 'input') { const profileParams = {}; // Map elementId to appropriate profile field switch (action.elementId) { case 'name': if (action.value.type === 'text') { profileParams.firstName = action.value.value; } break; case 'email': if (action.value.type === 'email') { profileParams.email = action.value.value; } break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile(profileParams).catch(error => { // handle the error }); } } }, }} /> ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Sử dụng quiz trong onboarding, bạn có thể tùy chỉnh các paywall hiển thị cho người dùng sau khi họ hoàn thành onboarding. Ví dụ: bạn có thể hỏi người dùng về kinh nghiệm thể thao của họ và hiển thị các CTA cũng như sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) trong trình xây dựng onboarding và gán ID có ý nghĩa cho các lựa chọn. 2. Xử lý các phản hồi quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](react-native-setting-user-attributes) cho người dùng. ```javascript showLineNumbers // Full-screen presentation const unsubscribe = view.setEventHandlers({ onStateUpdated(action, meta) { // Handle quiz responses and set custom attributes if (action.elementType === 'select') { const profileParams = {}; // Map quiz responses to custom attributes switch (action.elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) profileParams.codableCustomAttributes = { experience: action.value.value }; break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile(profileParams).catch(error => { // handle the error }); } } }, }); // Embedded widget <AdaptyOnboardingView onboarding={onboarding} eventHandlers={{ onStateUpdated(action, meta) { // Handle quiz responses and set custom attributes if (action.elementType === 'select') { const profileParams = {}; // Map quiz responses to custom attributes switch (action.elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) profileParams.codableCustomAttributes = { experience: action.value.value }; break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile(profileParams).catch(error => { // handle the error }); } } }, }} /> ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho từng phân khúc bạn đã tạo. 5. [Hiển thị paywall](react-native-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 hành động của nút này](react-native-handling-onboarding-events#opening-a-paywall). --- # File: react-native-sdk-call-order --- --- title: "Thứ tự gọi trong React Native SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution, và lỗi #2002 không ổn định bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự." --- `adapty.activate()` phải hoàn thành trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi nó resolve, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` đều thất bại với lỗi [`#2002 notActivated`](react-native-handle-errors#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 của người dùng cho đến khi `identify` resolve. Các lệnh gọi chạy đua với nó hoặc thất bại với lỗi [`#3006 profileWasChanged`](react-native-handle-errors#custom-network-codes), hoặc tác động lên hồ sơ người dùng ẩn danh được tạo lúc kích hoạt. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo 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ẽ được gán vào hồ sơ người dùng ẩn danh tạm thời và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Để biết thêm chi tiết về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Quy trình của bạn phụ thuộc vào hai yếu tố: thời điểm bạn biết customer user ID, và liệu bạn có dùng MMP hoặc analytics SDK hay không. - **Bước 2 và 5**: Bắt buộc cho 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 MMP hoặc analytics SDK (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ bắt buộc nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn có customer user ID ngay lúc khởi chạy ứng dụng, hãy truyền trực tiếp vào `activate()` (bước 2a). Cách này không bao giờ tạo hồ sơ người dùng ẩn danh, nên bước 4 là không cần thiết. | Bước | Lệnh gọi | Thời điểm | Ghi chú | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo MMP hoặc analytics SDK của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi chạy ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `adapty.activate('YOUR_PUBLIC_SDK_KEY', { customerUserId: 'YOUR_USER_ID' })` | Khởi chạy ứng dụng, sau bước 1, nếu bạn có customer user ID | Khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `adapty.activate('YOUR_PUBLIC_SDK_KEY')` không có `customerUserId` | 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 bao giờ thu thập) | Adapty tạo hồ sơ người dùng ẩn danh. | | 3 | `adapty.updateAttribution(data, source, networkUserId)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi hành động người dùng nào | Bắt buộc để MMP ID được gán vào đúng hồ sơ người dùng. | | 4 | `await adapty.identify('YOUR_USER_ID')` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên đường dẫn 2b với xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong `identify` sẽ gây ra `#3006 profileWasChanged`. | | 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; ngược lại sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này gây ra mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về sai đối tượng. ::: ## Cài đặt web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng qua web checkout (Stripe, Paddle) và sau đó cài đặt ứng dụng native, lần `activate()` đầu tiên trên thiết bị sẽ tạo một hồ sơ người dùng ẩn danh mới. Hồ sơ người dùng này không được liên kết với hồ sơ người dùng 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 đó là `restorePurchases`. Để biết metadata cần gửi kèm theo từng web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: react-native-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc lấy paywall trong React Native SDK" description: "Lấy paywall Adapty đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho React Native." --- Một lần lấy paywall đáng tin cậy trên React Native cần đảm bảo ba điều: hiển thị nhanh, trả về paywall đúng với đối tượng được nhắm mục tiêu, và có phương án dự phòng hợp lý khi mạng chậm. Các quy tắc dưới đây đề cập đến thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định rằng `adapty.activate()` và `adapty.identify()` đã được resolve xong. Xem [Thứ tự gọi trong React Native SDK](react-native-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 | |---|---|---| | Lấy placement mà bạn sắp hiển thị. | Prefetch tất cả các placement cùng lúc khi khởi động. | Bulk prefetch chặn JS thread và gây ra màn hình đen trong khoảng thời gian đó. | | Gọi `getPaywall` sau khi attribution đã có cơ hội resolve — ví dụ, 1–2 giây sau `activate` hoặc sau khi `onProfileUpdate` kích hoạt. | Gọi `getPaywall` tại component gốc khi mount. | Attribution chưa hoàn tất. Paywall sẽ resolve theo đối tượng mặc định và âm thầm bỏ qua các phân khúc và cá nhân hóa ASA. | | Đặt `loadTimeoutMs` 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ản hồi — hoặc họ sẽ tắt ứng dụng. | Xem [Lấy paywall và sản phẩm](fetch-paywalls-and-products-react-native) để tham khảo các tham số `fetchPolicy` và `loadTimeoutMs`, và [Placements](placements) để chọn placement phù hợp. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối kém liên tục (khu vực nông thôn, phương tiện giao thông, vùng bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy: .returnCacheDataElseLoad` cho mọi lần lấy dữ liệu, trừ lần đầu tiên. - Cấu hình [paywall dự phòng](fallback-paywalls) cho mọi placement trên Adapty Dashboard. - Đặt `loadTimeoutMs` từ 3–5 giây và chấp nhận phương án dự phòng khi timeout xảy ra. - Đừng chặn việc hiển thị paywall bằng `getProfile()`. Gọi `getPaywall` độc lập để tránh trường hợp profile chậm làm chặn giao diện. --- # File: react-native-show-aa-targeted-paywall --- --- title: "Hiển thị paywall nhắm mục tiêu AA ngay khi khởi chạy lần đầu trong React Native SDK" description: "Hiển thị paywall ngay lập tức và cập nhật cho người dùng Apple Ads khi attribution được áp dụng trong React Native, sử dụng AdaptyProfile.appliedAttributionSources." --- Attribution của Apple Ads (AA) được nhận không đồng bộ sau khi `adapty.activate()` được gọi. Ở lần khởi chạy đầu tiên, attribution thường chưa có, nên `getPaywall` sẽ giải quyết theo đối tượng mặc định và người dùng Apple Ads sẽ bỏ lỡ paywall được phân khúc theo AA của bạn. Thay vì chờ attribution đến mới hiển thị paywall, hãy hiển thị paywall ngay lập tức và làm mới khi attribution AA được áp dụng — để người dùng Apple Ads nhận được biến thể nhắm mục tiêu còn những người khác thấy paywall ngay không cần chờ. `AdaptyProfile.appliedAttributionSources` cho bạn biết khi nào attribution AA đã được áp dụng. ## Trước khi bắt đầu \{#before-you-start\} Bạn cần: - Adapty React Native SDK phiên bản **3.17.1** trở lên. - Apple Ads đã được cấu hình cho ứng dụng trong Adapty. Xem [Apple Ads](apple-search-ads). ## Cách hoạt động \{#how-it-works\} Sau khi `adapty.activate()` được gọi, SDK sẽ yêu cầu attribution Apple Ads từ Apple ở chế độ nền và chuyển kết quả đến backend của Adapty. Khi AA trở thành nguồn attribution đang hoạt động cho hồ sơ người dùng, SDK sẽ gửi một `AdaptyProfile` được cập nhật đến listener `onLatestProfileLoad` của bạn, với `'apple_search_ads'` trong mảng `appliedAttributionSources`. Điều này cho phép bạn tải paywall theo hai bước: 1. Gọi `getPaywall` ngay lập tức. Khi chưa có attribution nào được áp dụng, Adapty sẽ giải quyết yêu cầu theo đối tượng mặc định, người dùng thấy paywall ngay. 2. Khi `'apple_search_ads'` xuất hiện, gọi lại `getPaywall`. Lúc này Adapty giải quyết yêu cầu theo đối tượng Apple Ads và trả về paywall nhắm mục tiêu, thay thế paywall đầu tiên. `appliedAttributionSources` có thể rỗng hoặc không có giá trị. Đ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 - Chưa có attribution nào đến. Dù thế nào, bước 1 vẫn an toàn — Adapty giải quyết yêu cầu theo đối tượng phù hợp với trạng thái hồ sơ người dùng hiện tại, thường là đối tượng mặc định. Bước 2 chỉ chạy khi `'apple_search_ads'` xuất hiện. :::important Ở mỗi lần khởi chạy tiếp theo, hồ sơ người dùng đã được cache sẵn với `'apple_search_ads'` trong `appliedAttributionSources`, nên lần `getPaywall` đầu tiên đã trả về paywall được phân khúc theo Apple Ads — không có lần tải thứ hai hay thay đổi nào hiển thị. Luồng hai bước chỉ quan trọng ở lần khởi chạy đầu tiên, khi attribution vẫn đang trong quá trình xử lý. ::: ## Triển khai \{#implementation\} Hiển thị paywall ngay lập tức, sau đó lắng nghe `'apple_search_ads'` và làm mới paywall khi nó xuất hiện. 1. **Kích hoạt SDK.** Xem [Cài đặt & cấu hình React Native SDK](sdk-installation-reactnative). 2. **Tải và hiển thị paywall** bằng `getPaywall` như bình thường — không chặn chờ attribution. 3. **Đăng ký nhận cập nhật hồ sơ người dùng** bằng `adapty.addEventListener('onLatestProfileLoad', …)` và theo dõi `'apple_search_ads'`. Khi nó xuất hiện, tải lại paywall và hiển thị paywall mới. Nếu bạn chưa thiết lập listener, xem [Lắng nghe cập nhật gói đăng ký](react-native-check-subscription-status#listen-to-subscription-updates): ```typescript const subscription = adapty.addEventListener('onLatestProfileLoad', async profile => { if (!profile.appliedAttributionSources?.includes('apple_search_ads')) return; const targeted = await adapty.getPaywall(placementId); // present the targeted paywall in place of the first one }); // Call subscription.remove() after the upgrade, or after a timeout (see below). ``` 4. **Dừng lắng nghe sau một khoảng thời gian.** Hầu hết người dùng không bao giờ nhận được attribution Apple Ads, vì vậy hãy xóa listener sau một khoảng thời gian thay vì giữ nó mở suốt phiên. Cấu hình [paywall dự phòng](react-native-use-fallback-paywalls) cho placement để người dùng luôn thấy gì đó nếu yêu cầu thất bại. ## Ví dụ hoàn chỉnh \{#complete-example\} `onAppleAdsAttribution` sẽ resolve khi attribution Apple Ads được áp dụng, hoặc reject sau `timeoutMs`. Đoạn code bên dưới tải paywall ngay lập tức, sau đó tải lại khi attribution đến — người dùng Apple Ads nhận được paywall nhắm mục tiêu, và nếu attribution không bao giờ đến thì paywall đầu tiên vẫn được giữ nguyên: ```typescript const APPLE_ADS_SOURCE = 'apple_search_ads'; const placementId = 'YOUR_PLACEMENT_ID'; function hasAppleAdsAttribution(profile: AdaptyProfile): boolean { return profile.appliedAttributionSources?.includes(APPLE_ADS_SOURCE) ?? false; } /** * Resolves once Apple Ads attribution is applied to the profile. * Rejects with a timeout error if attribution never arrives within `timeoutMs`. * Call after `adapty.activate()`. */ export function onAppleAdsAttribution(timeoutMs: number): Promise<void> { return new Promise((resolve, reject) => { let timer: ReturnType<typeof setTimeout> | undefined; let subscription: { remove: () => void } | undefined; const stop = () => { clearTimeout(timer); subscription?.remove(); }; subscription = adapty.addEventListener('onLatestProfileLoad', profile => { if (!hasAppleAdsAttribution(profile)) return; stop(); resolve(); }); timer = setTimeout(() => { stop(); reject(new Error(`Apple Ads attribution timed out after ${timeoutMs}ms`)); }, timeoutMs); }); } let paywall = await adapty.getPaywall(placementId); onAppleAdsAttribution(30_000) .then(() => adapty.getPaywall(placementId)) .then(updated => { paywall = updated; }) .catch(() => { console.log('Apple Ads attribution or loading failed'); }); ``` Ở lần khởi chạy đầu tiên, người dùng Apple Ads sẽ thấy paywall mặc định trong một khoảnh khắc trước khi nó được thay thế. Nếu bạn hiển thị paywall bằng Paywall Builder, hãy cân nhắc xem việc hiển thị lại có phù hợp không, hoặc chỉ áp dụng bản cập nhật trước khi paywall được hiển thị. Điều chỉnh `timeoutMs` tùy theo thời gian bạn sẵn sàng lắng nghe — attribution khi đến thường xuất hiện trong vài giây sau khi khởi chạy. Nếu ứng dụng của bạn đã lắng nghe `onLatestProfileLoad` cho các mục đích khác (ví dụ, [kiểm tra trạng thái gói đăng ký](react-native-check-subscription-status#listen-to-subscription-updates)), bạn không cần thay đổi gì. `adapty.addEventListener` hỗ trợ nhiều listener độc lập, nên listener này sẽ được thêm vào mà không ảnh hưởng đến các listener khác. --- # File: react-native-test --- --- title: "Test & release in React Native SDK" description: "Learn how to test and release your React Native app with Adapty SDK." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng React Native của mình, bạn sẽ muốn 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 kiểm thử 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 kiểm thử 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 gửi ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối với cửa hàng và thông báo máy chủ đã được cấu hình - Giao dịch mua hoàn tất và được báo cáo về Adapty - Quyền 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à kiểm duyệt đã được đáp ứng --- # File: InvalidProductIdentifiers-react-native --- --- title: "Sửa lỗi Code-1000 noProductIDsFound trong React Native 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 khả dụng để mua trong App Store, dù chúng đã được liệt kê ở đó. Lỗi này đôi khi đi kèm cảnh báo `InvalidProductIdentifiers`. Nếu cảnh báo xuất hiện mà không có lỗi, bạn có thể bỏ qua an toàn. Nếu bạn gặp lỗi `noProductIDsFound`, hãy làm theo các bước sau để khắc phục: ## Bước 1. Kiểm tra bundle ID \{#step-2-check-bundle-id\} 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <Zoom> <img src="/docs/img/afd5012-bundle_id_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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**. <Zoom> <img src="/docs/img/2d64163-bundle_id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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. Vào **App Store Connect** và điều hướng đến [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) trong menu bên trái. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào tên nhóm gói đăng ký. Bạn sẽ thấy các sản phẩm được liệt kê trong mục **Subscriptions**. 3. Đảm bảo sản phẩm bạn đang kiểm tra được đánh dấu là **Ready to Submit**. <img src="/assets/shared/img/ready-to-submit.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/product-id-copy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 3. Kiểm tra tình trạng khả dụng của sản phẩm \{#step-4-check-product-availability\} 1. Quay lại **App Store Connect** và mở mục **Subscriptions** tương tự. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống mục **Availability** và kiểm tra rằng tất cả các quốc gia và khu vực cần thiết đều được liệt kê. <img src="/assets/shared/img/product-availability.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 4. Kiểm tra giá sản phẩm \{#step-5-check-product-prices\} 1. Truy cập lại mục **Monetization** → **Subscriptions** trong **App Store Connect**. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống **Subscription Pricing** và mở rộng mục **Current Pricing for New Subscribers**. <img src="/assets/shared/img/check-prices.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Đảm bảo rằng tất cả các mức giá cần thiết đều được liệt kê. <img src="/assets/shared/img/product-pricing.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 5. Kiểm tra trạng thái thanh toán ứng dụng, 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**. <img src="/assets/shared/img/business.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Chọn tên công ty của bạn. <img src="/assets/shared/img/business-name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Cuộn xuống và kiểm tra rằng **Paid Apps Agreement**, **Bank Account** và **Tax forms** đều hiển thị trạng thái **Active**. <img src="/assets/shared/img/appstore-connect-status.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bằng cách làm theo các bước trên, bạn sẽ có thể khắc phục cảnh báo `InvalidProductIdentifiers` và đưa sản phẩm 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ể đang bị kẹt trong registry của Apple. Registry sản phẩm của Apple đôi khi rơi vào trạng thái mà sản phẩm tồn tại trong giao diện App Store Connect nhưng không được hiển thị qua đường 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 để thay đổi được phổ biến. --- # File: cantMakePayments-react-native --- --- title: "Sửa lỗi Code-1003 cantMakePayment trong React Native SDK" description: "Giải quyết lỗi thực hiện thanh toán khi quản lý gói đăng ký trong Adapty." --- Lỗi 1003, `cantMakePayments`, cho biết thiết bị không thể thực hiện in-app purchase. Nếu bạn gặp lỗi `cantMakePayments`, thường là do một trong các nguyên nhân sau: - Giới hạn thiết bị: Lỗi này không liên quan đến Adapty. Xem cách khắc phục bên dưới. - Cấu hình Observer mode: Không thể dùng đồng thời phương thức `makePurchase` và Observer mode. Xem phần bên dưới. ## Sự cố: Giới hạn thiết bị \{#issue-device-restrictions\} | Sự cố | Giải pháp | |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------| | Giới hạn Screen Time | Tắt giới hạn In-App Purchase trong [Screen Time](https://support.apple.com/en-us/102470) | | Tài khoản bị tạm khóa | Liên hệ Apple Support để giải quyết vấn đề tài khoản | | Giới hạn khu vực | Sử dụng tài khoản App Store từ vùng được hỗ trợ | ## Sự cố: Dùng đồng thời Observer mode và makePurchase \{#issue-using-both-observer-mode-and-makepurchase\} Nếu bạn đang dùng `makePurchases` để xử lý giao dịch mua, bạn không cần dùng Observer mode. [Observer mode](observer-vs-full-mode) chỉ cần thiết khi bạn tự triển khai logic mua hàng. Vì vậy, nếu bạn đang dùng `makePurchase`, bạn có thể xóa phần kích hoạt Observer mode khỏi code khởi tạo SDK một cách an toàn. --- # File: migration-to-react-native-sdk-v4 --- --- title: "Migrate Adapty React Native SDK sang v. 4.0" description: "Migrate sang Adapty React Native SDK v4.0 (beta) bằng cách thay thế các API paywall bằng API flow, tương thích với cả Flow Builder và Paywall Builder." --- Adapty React Native SDK 4.0 (beta) giới thiệu flows và đổi tên các API paywall tương ứng. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi cấu hình nào trên Adapty Dashboard. ## Tham chiếu nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `adapty.getPaywall(placementId, locale?, params?)` | `adapty.getFlow(placementId, params?)` | | `adapty.getPaywallForDefaultAudience(placementId, locale?, params?)` | `adapty.getFlowForDefaultAudience(placementId, params?)` | | `adapty.getPaywallProducts(paywall)` | `adapty.getPaywallProducts(flow)` | | `adapty.logShowPaywall(paywall)` | `adapty.logShowFlow(flow)` | | `AdaptyPaywall` (kiểu) | `AdaptyFlow` | | `createPaywallView(paywall)` | `createFlowView(flow)` | | `AdaptyPaywallView` (component) | `AdaptyFlowView` | | `EventHandlers` (kiểu) | `FlowEventHandlers` | | `onPaywallShown` | `onAppeared` | | `onPaywallClosed` | `onDisappeared` | | `onRenderingFailed` | `onError` | `AdaptyPaywallProduct` giữ nguyên tên — các sản phẩm vẫn thuộc về một flow, và `getPaywallProducts` giờ nhận một `AdaptyFlow`. Các phương thức `getFlow` và `getFlowForDefaultAudience` không còn nhận tham số `locale` nữa. Các phương thức view `present`, `dismiss`, `setEventHandlers`, và `showDialog`, cùng các event handler `onCloseButtonPress`, `onUrlPress`, `onCustomAction`, `onProductSelected`, `onPurchaseStarted`, `onPurchaseCompleted`, `onPurchaseFailed`, `onRestoreStarted`, `onRestoreCompleted`, `onRestoreFailed`, `onLoadingProductsFailed`, `onWebPaymentNavigationFinished`, và `onAndroidSystemBack` giữ nguyên tên như trong v3. Một số hành vi mặc định đã thay đổi — xem [Thay đổi hành vi mặc định](#default-behavior-changes). ## Phiên bản iOS tối thiểu \{#minimum-ios-version\} Adapty React Native SDK 4.0 nâng phiên bản iOS deployment target tối thiểu từ iOS 13.0 lên **iOS 15.0**. Hãy đặt iOS deployment target của bạn thành 15.0 hoặc cao hơn trước khi nâng cấp. ## Cài đặt \{#installation\} ### Cập nhật gói v4.0 là bản phát hành thử nghiệm (pre-release), vì vậy hãy chỉ định chính xác phiên bản — npm không tự động chọn các phiên bản pre-release qua cú pháp caret/tilde: ```bash showLineNumbers npm install react-native-adapty@4.0.0 # or yarn add react-native-adapty@4.0.0 ``` ### iOS: Các SDK native hiện được phân phối qua Swift Package Manager \{#ios-native-sdks-now-come-through-swift-package-manager\} [Repo spec của CocoaPods sẽ chuyển sang chế độ chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/), vì vậy bắt đầu từ v4, các SDK native `Adapty`, `AdaptyUI` và `AdaptyPlugin` **không còn được kéo vào dưới dạng sub-dependency của CocoaPods nữa** — podspec sẽ kéo chúng qua **Swift Package Manager** (thông qua helper `spm_dependency`). Điều này yêu cầu hai thay đổi sau: - **React Native 0.75 trở lên** — cần thiết cho helper `spm_dependency` trong podspec. Nếu dùng phiên bản cũ hơn, `pod install` sẽ báo lỗi rõ ràng; hãy nâng cấp React Native trước, hoặc tiếp tục dùng `react-native-adapty` 3.x. - **Dynamic frameworks** — các dependency SPM yêu cầu dynamic linkage. Cách bật tính năng này khác nhau giữa Expo và bare React Native. #### Expo Thêm config plugin [`expo-build-properties`](https://docs.expo.dev/versions/latest/sdk/build-properties/) và đặt iOS frameworks thành dynamic trong `app.json` (hoặc `app.config.js`): ```json showLineNumbers title="app.json" { "expo": { "plugins": [ [ "expo-build-properties", { "ios": { "useFrameworks": "dynamic" } } ] ] } } ``` Sau đó cài plugin và tạo lại native project: ```bash showLineNumbers npx expo install expo-build-properties npx expo prebuild --clean ``` #### Bare React Native Thêm dynamic frameworks vào iOS target, sau đó cài lại pods: ```ruby showLineNumbers title="ios/Podfile" use_frameworks! :linkage => :dynamic ``` ```bash showLineNumbers cd ios && pod install --repo-update ``` Nếu trước đây bạn đã kéo `Adapty`, `AdaptyUI`, hoặc `AdaptyPlugin` dưới dạng sub-dependencies của CocoaPods, hãy xóa các dòng `pod 'Adapty'`, `pod 'AdaptyUI'`, hoặc `pod 'AdaptyPlugin'` khỏi `Podfile` trước. :::warning Chuyển từ liên kết tĩnh mặc định sang dynamic framework có thể xung đột với các thư viện chưa hỗ trợ modular headers, và không tương thích với Flipper. Nếu gặp lỗi build, xem bài viết này về [tích hợp Swift Package Manager với thư viện React Native](https://www.callstack.com/blog/integrating-swift-package-manager-with-react-native-libraries). ::: Xem [Cài đặt Adapty SDK](sdk-installation-reactnative) để biết hướng dẫn thiết lập đầy đủ. ## Lấy flows \{#fetching-flows\} ### getPaywall → getFlow Kiểu trả về thay đổi từ `AdaptyPaywall` sang `AdaptyFlow`, và tham số `locale` bị xóa — khi render một flow, locale sẽ được tự động xác định; với các paywall tùy chỉnh, tất cả locale được trả về trong `flow.remoteConfigs`: ```diff showLineNumbers - const paywall = await adapty.getPaywall('YOUR_PLACEMENT_ID', 'en'); + const flow = await adapty.getFlow('YOUR_PLACEMENT_ID'); ``` `getPaywallForDefaultAudience` được đổi tên theo cách tương tự: ```diff showLineNumbers - const paywall = await adapty.getPaywallForDefaultAudience('YOUR_PLACEMENT_ID', 'en'); + const flow = await adapty.getFlowForDefaultAudience('YOUR_PLACEMENT_ID'); ``` ### getPaywallProducts(paywall) → getPaywallProducts(flow) `getPaywallProducts` giữ nguyên tên nhưng giờ nhận một `AdaptyFlow`: ```diff showLineNumbers - const products = await adapty.getPaywallProducts(paywall); + const products = await adapty.getPaywallProducts(flow); ``` ## Mô hình dữ liệu \{#data-model\} `getFlow` trả về `AdaptyFlow` thay vì `AdaptyPaywall`, và cấu trúc đối tượng đã thay đổi: | Trường `AdaptyPaywall` v3 | Trường `AdaptyFlow` v4 | Hành động | |---|---|---| | `remoteConfig?` (đơn lẻ) | `remoteConfigs?: AdaptyRemoteConfig[]` (mảng) | Một flow chứa một remote config cho mỗi ngôn ngữ đã cấu hình. Đọc cái phù hợp với người dùng: `flow.remoteConfigs?.find((c) => c.lang === 'en')`. | | `products` | `flow.paywalls[i].productIdentifiers` | Các định danh sản phẩm hiện nằm trên từng biến thể flow, không còn trên flow nữa. | | `webPurchaseUrl?` | `flow.paywalls[i].webPurchaseUrl` | Chuyển từ flow sang từng biến thể paywall. | | `version?: number` | `flowVersionId?: string` | Đổi tên, và kiểu dữ liệu thay đổi từ `number` sang `string`. | | `hasViewConfiguration` | đã xóa | Xóa mọi kiểm tra `hasViewConfiguration` khỏi code của bạn. | | `requestLocale` | đã xóa | Locale không còn là một phần của model nữa. | | _(mới)_ | `paywalls: AdaptyFlowPaywall[]` | Mỗi mục là một biến thể paywall trong flow. | | _(mới)_ | `responseCreatedAt: number` | Timestamp phản hồi từ server, tính bằng mili giây. | Product identifiers moved from the flow to each variation: ```diff showLineNumbers - const ids = paywall.products; + const ids = flow.paywalls[0].productIdentifiers; ``` ## Các phương thức Web paywall \{#web-paywall-methods\} `openWebPaywall` và `createWebPaywallUrl` vẫn giữ nguyên tên, nhưng tham số đầu tiên bây giờ là `AdaptyFlowPaywall` (một biến thể flow) thay vì `AdaptyPaywall`. Bạn vẫn có thể truyền vào `AdaptyPaywallProduct`. ```diff showLineNumbers const flow = await adapty.getFlow('YOUR_PLACEMENT_ID'); - await adapty.openWebPaywall(paywall); + await adapty.openWebPaywall(flow.paywalls[0]); ``` ## Theo dõi lượt xem flow \{#tracking-flow-views\} ### logShowPaywall → logShowFlow `logShowPaywall` được đổi tên thành `logShowFlow` và giờ nhận vào một `AdaptyFlow`. Sự kiện vẫn được ghi lại theo cùng một biến thể, nên các chỉ số funnel và A/B test hiện có vẫn hoạt động bình thường mà không cần thay đổi trên dashboard. ```diff showLineNumbers - await adapty.logShowPaywall(paywall); + await adapty.logShowFlow(flow); ``` Giống như ở v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được dựng bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi các lượt xem đó. ## Hiển thị flow \{#displaying-flows\} ### createPaywallView → createFlowView Đổi tên hàm factory và truyền vào `AdaptyFlow`. Các phương thức của controller được trả về (`present`, `dismiss`, `setEventHandlers`, `showDialog`) không thay đổi: ```diff showLineNumbers - import { createPaywallView } from 'react-native-adapty'; + import { createFlowView } from 'react-native-adapty'; - const view = await createPaywallView(paywall); + const view = await createFlowView(flow); await view.present(); ``` ### AdaptyPaywallView → AdaptyFlowView Nếu bạn render bằng React component, hãy đổi tên component và truyền prop `flow`: ```diff showLineNumbers - import { AdaptyPaywallView } from 'react-native-adapty'; + import { AdaptyFlowView } from 'react-native-adapty'; - <AdaptyPaywallView paywall={paywall} /* … */ /> + <AdaptyFlowView flow={flow} /* … */ /> ``` :::note Một flow view được tạo bằng `createFlowView` chỉ dùng một lần: sau khi gọi `dismiss()`, view đó sẽ bị hủy, vì vậy hãy gọi lại `createFlowView` để hiển thị flow thêm lần nữa. Một `AdaptyFlowView` được nhúng sẽ bị đóng khi unmount — trả về `true` từ một handler không đóng view được nhúng, vì vậy hãy tự thay đổi state của bạn thay thế, ví dụ trong `onCloseButtonPress`. ::: ## Xử lý sự kiện \{#handling-events\} Giao diện xử lý sự kiện được đổi tên từ `EventHandlers` thành `FlowEventHandlers`, và ba callback cũng được đổi tên. Phần thân của các handler hiện có không cần thay đổi code — chỉ cần đổi tên: ```diff showLineNumbers - onPaywallShown: () => { /* … */ }, + onAppeared: () => { /* … */ }, - onPaywallClosed: () => { /* … */ }, + onDisappeared: () => { /* … */ }, - onRenderingFailed: (error) => { /* … */ }, + onError: (error) => { /* … */ }, ``` Tất cả các event handler khác vẫn giữ nguyên tên. Hai handler cũng nhận thêm tham số thứ hai: `onPurchaseCompleted` nay là `(purchaseResult, product)` và `onPurchaseFailed` nay là `(error, product)`, trong đó `product` là `AdaptyPaywallProduct` liên quan. Xem [Xử lý sự kiện flow & paywall](react-native-handling-events-1) để biết danh sách đầy đủ. :::note `onDisappeared` chỉ kích hoạt khi một flow được trình bày dưới dạng modal với `createFlowView().present()`. Component `AdaptyFlowView` không expose nó dưới dạng prop — để đóng một embedded view, hãy unmount nó. ::: v4 cũng bổ sung một số tính năng bạn có thể tùy chọn bật: - Các phương thức `adapty.openWebUrl(url, openIn?)` và `adapty.requestAppReview()` — hỗ trợ các handler mặc định `onUrlPress` và `onRequestAppReview`, giúp xử lý URL và yêu cầu đánh giá ứng dụng theo cách native ngay từ đầu. Chỉ gọi trực tiếp khi bạn ghi đè các handler đó. - Xử lý giao dịch mua trong Observer mode bên trong flow thông qua các handler mới `onObserverPurchaseInitiated` / `onObserverRestoreInitiated`. Xem [Xử lý giao dịch mua trong Observer mode](react-native-handling-events-1#handle-purchases-in-observer-mode). ## Các API đã bị xóa và deprecated \{#removed-and-deprecated-apis\} ### setFallbackPaywalls → setFallback `setFallbackPaywalls` đã bị xóa. Hãy dùng `setFallback`, nhận cùng tham số: ```diff showLineNumbers - await adapty.setFallbackPaywalls(fileLocation); + await adapty.setFallback(fileLocation); ``` ### Các export đã bị xóa \{#removed-exports\} Các symbol này không còn được export từ `react-native-adapty` nữa. Hãy xóa các import liên quan: - **`AdaptyPaywall`**: Dùng `AdaptyFlow` thay thế. - **`ProductReference`**: Dùng `AdaptyProductIdentifier`, đọc từ `flow.paywalls[i].productIdentifiers`. - **`AdaptyPaywallBuilder`**: Đã bị xóa. Flow và paywall được render natively. - **`AdaptyAndroidSubscriptionUpdateParameters`**: Dùng shape lồng nhau `subscriptionUpdateParams` (xem bên dưới). ### activate: lockMethodsUntilReady `lockMethodsUntilReady` đã bị xóa và hành vi này hiện luôn được bật. Hãy xóa nó khỏi lệnh gọi `activate` — giữ lại nó sẽ gây lỗi biên dịch: ```diff showLineNumbers - await adapty.activate('PUBLIC_SDK_KEY', { lockMethodsUntilReady: true }); + await adapty.activate('PUBLIC_SDK_KEY'); ``` ### Cập nhật gói đăng ký Android trong makePurchase \{#makepurchase-android-subscription-update\} Cấu trúc phẳng cho việc cập nhật gói đăng ký Android đã bị loại bỏ. Hãy chuyển `oldSubVendorProductId` và `prorationMode` vào một object lồng nhau tên là `subscriptionUpdateParams`, và giữ `isOfferPersonalized` ở cấp cao nhất. Xem [Thực hiện mua hàng](react-native-making-purchases) để xem ví dụ đầy đủ. ### Android: safe-area paddings Tài nguyên boolean Android `<bool name="adapty_paywall_enable_safe_area_paddings">…</bool>` đã bị xóa. Hãy xóa nó khỏi `res/values/bools.xml` và kiểm soát safe-area paddings tại runtime bằng tham số `enableSafeArea` khi bạn tạo flow view. Mặc định là `true` cho kiểu hiển thị modal và `false` cho component nhúng. ### Chế độ mock \{#mock-mode\} Nếu bạn chạy SDK ở chế độ mock (Expo Go hoặc web preview), hãy đổi tên khóa cấu hình mock `paywalls` thành `flows`. ## Thay đổi hành vi mặc định \{#default-behavior-changes\} Những thay đổi này không gây lỗi biên dịch, vì vậy hãy kiểm tra chúng trong khi chạy: - **`onAndroidSystemBack`**: Mặc định đã thay đổi từ đóng view sang giữ nguyên. Để khôi phục hành vi cũ, trả về `true` từ handler. - **`onPurchaseCompleted`**: Mặc định đã thay đổi từ đóng view (trừ khi người dùng hủy mua) sang luôn giữ nguyên. Để khôi phục hành vi cũ, trả về `purchaseResult.type !== 'user_cancelled'` từ handler. - **`onRestoreCompleted`**: Mặc định đã thay đổi từ đóng view sau khi khôi phục thành công sang giữ nguyên. Để khôi phục hành vi cũ, trả về `true` từ handler. - **`onUrlPress`**: Mặc định hiện tại mở URL thông qua native layer, tuân theo cài đặt trình duyệt trong ứng dụng hoặc trình duyệt ngoài từ dashboard. Ghi đè handler để tự xử lý việc mở URL. ## Onboarding API không còn được hỗ trợ \{#onboarding-api-deprecation\} Onboarding API cũ đã bị deprecated trong v4.0, thay thế bằng [Flow Builder](adapty-flow-builder). API này vẫn hoạt động bình thường, và IDE của bạn sẽ đánh dấu các symbol bị deprecated thông qua annotation `@deprecated` — không có cảnh báo nào xuất hiện lúc runtime. Các symbol này sẽ bị xóa trong một bản phát hành tương lai, vì vậy hãy lên kế hoạch migration các onboarding của bạn sang Flow Builder. Các symbol bị deprecated: `getOnboarding`, `getOnboardingForDefaultAudience`, `createOnboardingView`, và `AdaptyOnboardingView`. --- # File: migration-react-native-314 --- --- title: "Migrate Adapty React Native SDK to v3.14" description: "Migrate to Adapty React Native SDK v3.14 for better performance and new monetization features." --- Adapty React Native SDK 3.14.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: - Phương thức `registerEventHandlers` đã được thay thế bằng phương thức `setEventHandlers`. - Trong `AdaptyOnboardingView`, các event handler giờ được truyền dưới dạng các prop riêng lẻ thay vì một object `eventHandlers` - Một kiểu import mới, đơn giản hơn đã được giới thiệu cho các UI component - Phương thức `logShowOnboarding` đã bị xóa - Phiên bản React Native tối thiểu đã được cập nhật lên 0.73.0 - Style trình bày iOS mặc định cho paywall và onboarding đã thay đổi từ page sheet sang full screen ## Thay thế `registerEventHandlers` bằng `setEventHandlers` \{#replace-registereventhandlers-with-seteventhandlers\} Phương thức `registerEventHandlers` dùng để làm việc với Adapty Paywall và Onboarding Builder đã được thay thế bằng phương thức `setEventHandlers`. Nếu bạn đang dùng Adapty Paywall Builder và/hoặc Adapty Onboarding Builder, hãy tìm `registerEventHandlers` trong code của bạn và thay thế bằng `setEventHandlers`. Thay đổi này được thực hiện để làm rõ hơn hành vi của phương thức: Các handler giờ hoạt động lần lượt vì mỗi handler trả về `true`/`false`, và việc có nhiều handler cho một sự kiện khiến hành vi kết quả không rõ ràng. Lưu ý rằng khi sử dụng các React component như `AdaptyOnboardingView` hoặc `AdaptyPaywallView`, bạn không cần trả về `true`/`false` từ các event handler vì bạn kiểm soát khả năng hiển thị của component thông qua state management của riêng mình. Giá trị trả về chỉ cần thiết khi trình bày màn hình modal, nơi SDK quản lý vòng đời của view. :::important Gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn cung cấp, thay thế cả handler mặc định lẫn các handler đã được đặt trước đó cho những sự kiện cụ thể đó. ::: ```diff showLineNumbers - const unsubscribe = view.registerEventHandlers({ - // your event handlers - }) const unsubscribe = view.setEventHandlers({ // your event handlers }) ``` ## Cập nhật đường dẫn import cho UI component \{#update-import-paths-for-ui-components\} Adapty SDK 3.14.0 giới thiệu kiểu import đơn giản hơn cho các UI component. Thay vì import từ `react-native-adapty/dist/ui`, giờ bạn có thể import trực tiếp từ `react-native-adapty`. Kiểu import mới nhất quán hơn với các thông lệ React Native tiêu chuẩn và giúp các câu lệnh import gọn gàng hơn. Nếu bạn đang sử dụng các UI component như `AdaptyPaywallView` hoặc `AdaptyOnboardingView`, hãy cập nhật các import như bên dưới: ```diff showLineNumbers - import { AdaptyPaywallView } from 'react-native-adapty/dist/ui'; + import { AdaptyPaywallView } from 'react-native-adapty'; - import { AdaptyOnboardingView } from 'react-native-adapty/dist/ui'; + import { AdaptyOnboardingView } from 'react-native-adapty'; - import { createPaywallView } from 'react-native-adapty/dist/ui'; + import { createPaywallView } from 'react-native-adapty'; - import { createOnboardingView } from 'react-native-adapty/dist/ui'; + import { createOnboardingView } from 'react-native-adapty'; ``` :::note Để tương thích ngược, kiểu import cũ (`react-native-adapty/dist/ui`) vẫn được hỗ trợ. Tuy nhiên, chúng tôi khuyến nghị sử dụng kiểu import mới để đảm bảo tính nhất quán và rõ ràng. ::: ## Cập nhật event handler của onboarding trong React component \{#update-onboarding-event-handlers-in-the-react-component\} Các event handler cho onboarding đã được chuyển ra khỏi object `eventHandlers` trong `AdaptyOnboardingView`. Nếu bạn đang hiển thị onboarding bằng `AdaptyOnboardingView`, hãy cập nhật cấu trúc xử lý sự kiện. :::important Lưu ý cách chúng tôi khuyến nghị triển khai các event handler. Để tránh tạo lại object mỗi lần render, hãy sử dụng `useCallback` cho các hàm xử lý sự kiện. ::: ```diff showLineNumbers import React, { useCallback } from 'react'; - import { AdaptyOnboardingView } from 'react-native-adapty/dist/ui'; + import { AdaptyOnboardingView } from 'react-native-adapty'; + import type { OnboardingEventHandlers } from 'react-native-adapty'; + + function MyOnboarding({ onboarding }) { + const onAnalytics = useCallback<OnboardingEventHandlers['onAnalytics']>((event, meta) => {}, []); + const onClose = useCallback<OnboardingEventHandlers['onClose']>((actionId, meta) => {}, []); + const onCustom = useCallback<OnboardingEventHandlers['onCustom']>((actionId, meta) => {}, []); + const onPaywall = useCallback<OnboardingEventHandlers['onPaywall']>((actionId, meta) => {}, []); + const onStateUpdated = useCallback<OnboardingEventHandlers['onStateUpdated']>((action, meta) => {}, []); + const onFinishedLoading = useCallback<OnboardingEventHandlers['onFinishedLoading']>((meta) => {}, []); + const onError = useCallback<OnboardingEventHandlers['onError']>((error) => {}, []); + return ( <AdaptyOnboardingView onboarding={onboarding} style={styles.container} - eventHandlers={{ - onAnalytics(event, meta) { /* ... */ }, - onClose(actionId, meta) { /* ... */ }, - onCustom(actionId, meta) { /* ... */ }, - onPaywall(actionId, meta) { /* ... */ }, - onStateUpdated(action, meta) { /* ... */ }, - onFinishedLoading(meta) { /* ... */ }, - onError(error) { /* ... */ }, - }} + onAnalytics={onAnalytics} + onClose={onClose} + onCustom={onCustom} + onPaywall={onPaywall} + onStateUpdated={onStateUpdated} + onFinishedLoading={onFinishedLoading} + onError={onError} /> ); + } ``` :::note Để tương thích ngược, prop `eventHandlers` vẫn được hỗ trợ nhưng đã bị deprecated. Chúng tôi khuyến nghị migrate sang các prop event handler riêng lẻ như minh họa ở trên. ::: ## Xóa `logShowOnboarding` \{#delete-logshowonboarding\} Trong Adapty SDK 3.14.0, chúng tôi đã xóa phương thức `logShowOnboarding` khỏi SDK. Nếu bạn đã sử dụng phương thức này, nó sẽ không còn khả dụng khi bạn nâng cấp SDK lên phiên bản 3.14 trở lên. Thay vào đó, bạn có thể [tạo onboarding trong Adapty no-code onboarding builder](onboardings). Analytics cho các onboarding này được theo dõi tự động và bạn có nhiều tùy chọn tùy chỉnh. ## Cập nhật React Native \{#update-react-native\} Kể từ Adapty SDK 3.14.0, phiên bản React Native tối thiểu được hỗ trợ là 0.73.0. Nếu bạn đang dùng phiên bản cũ hơn, hãy cập nhật React Native lên phiên bản 0.73.0 trở lên để trải nghiệm với Adapty SDK được nhất quán và ổn định. ## Cập nhật iOS presentation style cho paywall và onboarding modal \{#update-ios-presentation-style-for-modal-paywalls-and-onboardings\} Trong Adapty SDK 3.14.0, style trình bày iOS mặc định cho paywall và onboarding hiển thị bằng phương thức `view.present()` đã thay đổi từ page sheet sang full screen. Nếu bạn muốn giữ style page sheet như trước, hãy truyền tham số `iosPresentationStyle` vào phương thức `present()`: ```typescript showLineNumbers title="React Native (TSX)" try { await view.present({ iosPresentationStyle: 'page_sheet' }); } catch (error) { // handle the error } ``` --- # File: react-native-migration-guide-380 --- --- title: "Migrate Adapty React Native SDK to v3.8" description: "Migrate sang Adapty React Native SDK v3.8 để cải thiện hiệu năng và sử dụng các tính năng kiếm tiền mới." --- Adapty SDK 3.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. ## Cập nhật kiểu đầu vào để lấy tham số placement \{#update-input-type-for-getting-placement-params\} `GetPaywallParamsInput` đã được đổi tên thành `GetPlacementParamsInput`: ```diff showLineNumbers - type GetPaywallParamsInput = { + type GetPlacementParamsInput = { placementId: string; locale?: string; fetchPolicy?: AdaptyPlacementFetchPolicy; loadTimeoutMs?: number; } ``` ## Cập nhật phương thức fallback \{#update-fallback-method\} Phương thức để thiết lập paywall dự phòng đã được cập nhật, và kiểu dùng để chỉ định vị trí fallback cũng đã được đổi tên: ```diff showLineNumbers - adapty.setFallbackPaywalls(paywallsLocation: Input.FallbackPaywallsLocation); + adapty.setFallback(fileLocation: Input.FileLocation); ``` ## Cập nhật cách truy cập thuộc tính paywall \{#update-paywall-property-access\} Các thuộc tính sau đã được chuyển từ `AdaptyPaywall` sang `AdaptyPlacement`: ```diff showLineNumbers - paywall.abTestName - paywall.audienceName - paywall.revision - paywall.placementId + paywall.placement.abTestName + paywall.placement.audienceName + paywall.placement.revision + paywall.placement.id ``` --- # File: migration-to-react-native-sdk-34 --- --- title: "Migrate Adapty React Native SDK to v3.4" description: "Migrate sang Adapty React Native SDK v3.4 để có hiệu suất tốt hơn và các tính năng kiếm tiền mới." --- Adapty SDK 3.4.0 là 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 một số bước migration. ## Cập nhật 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 mobile](react-native-use-fallback-paywalls) bằng các file mới. ## Cập nhật 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 lẫn iOS. Phương thức này báo cáo rõ ràng từng giao dịch cho Adapty, đảm bảo giao dịch đượ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 bướ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 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 integration. ::: ```diff showLineNumbers - if (Platform.OS === 'android') { - try { - await adapty.restorePurchases(); - } catch (error) { - // handle the error - } - } const variationId = paywall.variationId; try { await adapty.reportTransaction(transactionId, variationId); } catch (error) { // handle the `AdaptyError` } ``` --- # File: migration-to-react-native330 --- --- title: "Migrate Adapty React Native SDK to v3.3" description: "Migrate to Adapty React Native SDK v3.3 for better performance and new monetization features." --- Adapty SDK 3.3.1 là bản phát hành lớn mang lại một số cải tiến có thể yêu cầu bạn thực hiện các bước migration. 1. Nâng cấp lên Adapty SDK v3.3.x. 2. Cập nhật các model. 3. Xóa phương thức `getProductsIntroductoryOfferEligibility`. 4. Cập nhật cách thực hiện giao dịch mua. 5. Cập nhật cách hiển thị paywall trong Paywall Builder. 6. Xem lại cách triển khai timer do developer định nghĩa. 7. Cập nhật cách xử lý sự kiện mua hàng trong Paywall Builder. 8. Cập nhật cách xử lý sự kiện custom action trong Paywall Builder. 9. Sửa đổi callback `onProductSelected`. 10. Xóa các tham số tích hợp bên thứ ba khỏi phương thức `updateProfile`. 11. 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, và Pushwoosh. 12. Cập nhật cách triển khai Observer mode. ## Nâng cấp Adapty React Native SDK lên 3.3.x \{#upgrade-adapty-react-native-sdk-to-33x\} Trước phiên bản 3.3.1, SDK `react-native-adapty` là SDK cốt lõi và bắt buộc để Adapty hoạt động đúng trong ứng dụng của bạn. SDK `@adapty/react-native-ui` là tùy chọn và chỉ cần thiết nếu bạn sử dụng Adapty Paywall Builder. Kể từ phiên bản 3.3.1, SDK `@adapty/react-native-ui` đã bị deprecated và chức năng của nó đã được tích hợp vào SDK `react-native-adapty`. Để nâng cấp lên phiên bản 3.3.1, hãy thực hiện các bước sau: 1. Cập nhật gói `react-native-adapty` lên phiên bản 3.3.1. 2. Xóa gói `@adapty/react-native-ui` khỏi các dependency của dự án. 3. Đồng bộ hóa các dependency của dự án để áp dụng các thay đổi. ## Thay đổi trong các model \{#changes-in-models\} ### Model mới \{#new-models\} 1. [AdaptySubscriptionOffer](https://react-native.adapty.io/interfaces/adaptysubscriptionoffer): ```typescript showLineNumbers export interface AdaptySubscriptionOffer { readonly identifier: AdaptySubscriptionOfferId; phases: AdaptyDiscountPhase[]; android?: { offerTags?: string[]; }; } ``` 2. [AdaptySubscriptionOfferId](https://react-native.adapty.io/types/adaptysubscriptionofferid): ```typescript showLineNumbers export type AdaptySubscriptionOfferId = | { id?: string; type: 'introductory'; } | { id: string; type: 'promotional' | 'win_back'; }; ``` ### Model đã thay đổi \{#changed-models\} 1. [AdaptyPaywallProduct](https://react-native.adapty.io/interfaces/adaptypaywallproduct): - Đổi tên thuộc tính `subscriptionDetails` thành `subscription`. <p> </p> ```diff showLineNumbers - subscriptionDetails?: AdaptySubscriptionDetails; + subscription?: AdaptySubscriptionDetails; ``` 2. [AdaptySubscriptionDetails](https://react-native.adapty.io/interfaces/adaptysubscriptiondetails): - `promotionalOffer` đã bị xóa. Bây giờ ưu đãi chỉ được trả về trong thuộc tính `offer` nếu có. Trong trường hợp đó, `offer?.identifier?.type` sẽ là `'promotional'`. - `introductoryOfferEligibility` đã bị xóa (ưu đãi chỉ được trả về nếu người dùng đủ điều kiện). - `offerId` đã bị xóa. ID ưu đãi hiện được lưu trong `AdaptySubscriptionOffer.identifier`. - `offerTags` đã được chuyển sang `AdaptySubscriptionOffer.android`. <p> </p> ```diff showLineNumbers - introductoryOffers?: AdaptyDiscountPhase[]; + offer?: AdaptySubscriptionOffer; ios?: { - promotionalOffer?: AdaptyDiscountPhase; subscriptionGroupIdentifier?: string; }; android?: { - offerId?: string; basePlanId: string; - introductoryOfferEligibility: OfferEligibility; - offerTags?: string[]; renewalType?: 'prepaid' | 'autorenewable'; }; } ``` 3. [AdaptyDiscountPhase](https://react-native.adapty.io/interfaces/adaptydiscountphase): - Trường `identifier` đã bị xóa khỏi model `AdaptyDiscountPhase`. Identifier của ưu đãi hiện được lưu trong `AdaptySubscriptionOffer.identifier`. <p> </p> ```diff showLineNumbers - ios?: { - readonly identifier?: string; - }; ``` ### Xóa các model \{#remove-models\} 1. `AttributionSource`: - Chuỗi string hiện được sử dụng ở những nơi trước đây dùng `AttributionSource`. 2. `OfferEligibility`: - Model này đã bị xóa vì không còn cần thiết. Hiện tại, ưu đãi chỉ được trả về nếu người dùng đủ điều kiện. ## Xóa phương thức `getProductsIntroductoryOfferEligibility` \{#remove-getproductsintroductoryoffereligibility-method\} Trước Adapty SDK 3.3.1, các object sản phẩm luôn bao gồm ưu đãi, ngay cả khi người dùng không đủ điều kiện. Điều này yêu cầu bạn phải kiểm tra điều kiện thủ công trước khi sử dụng ưu đãi. Kể từ phiên bản 3.3.1, object sản phẩm chỉ bao gồm ưu đãi nếu người dùng đủ điều kiện. Điều này đơn giản hóa quy trình vì bạn có thể giả định người dùng đủ điều kiện nếu có ưu đãi. ## Cập nhật cách thực hiện giao dịch mua \{#update-making-purchase\} Trong các phiên bản trước, giao dịch bị hủy và đang chờ xử lý được coi là lỗi và trả về mã `2: 'paymentCancelled'` và `25: 'pendingPurchase'` tương ứng. Kể từ phiên bản 3.3.1, giao dịch bị hủy và đang chờ xử lý hiện được coi là kết quả thành công và nên được xử lý tương ứng: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` ## Cập nhật cách hiển thị paywall trong Paywall Builder \{#update-paywall-builder-paywall-presentation\} Để xem các ví dụ cập nhật, hãy tham khảo tài liệu [Hiển thị paywall mới từ Paywall Builder trong React Native](react-native-present-paywalls). ```diff showLineNumbers - import { createPaywallView } from '@adapty/react-native-ui'; + import { createPaywallView } from 'react-native-adapty/dist/ui'; const view = await createPaywallView(paywall); view.registerEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` ## Cập nhật cách triển khai timer do developer định nghĩa \{#update-developer-defined-timer-implementation\} Đổi tên tham số `timerInfo` thành `customTimers`: ```diff showLineNumbers - let timerInfo = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } + let customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } //and then you can pass it to createPaywallView as follows: - view = await createPaywallView(paywall, { timerInfo }) + view = await createPaywallView(paywall, { customTimers }) ``` ## Sửa đổi sự kiện mua hàng trong Paywall Builder \{#modify-paywall-builder-purchase-events\} Trước đây: - Giao dịch bị hủy kích hoạt callback `onPurchaseCancelled`. - Giao dịch đang chờ xử lý trả về mã lỗi `25: 'pendingPurchase'`. Bây giờ: - Cả hai trường hợp đều được xử lý bởi callback `onPurchaseCompleted`. #### Các bước migration: 1. Xóa callback `onPurchaseCancelled`. 2. Xóa phần xử lý mã lỗi `25: 'pendingPurchase'`. 3. Cập nhật callback `onPurchaseCompleted`: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ // ... other optional callbacks onPurchaseCompleted(purchaseResult, product) { switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; // highlight-start case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; // highlight-end } // highlight-start return purchaseResult.type !== 'user_cancelled'; // highlight-end }, }); ``` ## Sửa đổi sự kiện custom action trong Paywall Builder \{#modify-paywall-builder-custom-action-events\} Các callback đã bị xóa: - `onAction` - `onCustomEvent` Callback mới được thêm vào: - Callback `onCustomAction(actionId)` mới. Sử dụng nó cho các custom action. ## Sửa đổi callback `onProductSelected` \{#modify-onproductselected-callback\} Trước đây, `onProductSelected` yêu cầu object `product`. Bây giờ yêu cầu `productId` dưới dạng chuỗi string. ## Xóa các tham số tích hợp bên thứ ba khỏi phương thức `updateProfile` \{#remove-third-party-integration-parameters-from-updateprofile-method\} Các identifier tích hợp bên thứ ba hiện được thiết lập bằng phương thức `setIntegrationIdentifier`. Phương thức `updateProfile` không còn chấp nhận chúng nữa. ## 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 React Native SDK 3.3.1 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 đây như được mô tả trong các phần bên dưới. Ngoài ra, nếu bạn đã sử dụng `AttributionSource` để lấy attribution identifier, hãy thay đổi code của bạn để cung cấp identifier cần thiết dưới dạng chuỗi string. ### Adjust \{#adjust\} Cập nhật code ứng dụng di động của bạn như hướng dẫn 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 { Adjust, AdjustConfig } from "react-native-adjust"; import { adapty } from "react-native-adapty"; var adjustConfig = new AdjustConfig(appToken, environment); // Before submiting Adjust config... adjustConfig.setAttributionCallbackListener(attribution => { // Make sure Adapty SDK is activated at this point // You may want to lock this thread awaiting of `activate` adapty.updateAttribution(attribution, "adjust"); }); // ... Adjust.create(adjustConfig); + Adjust.getAdid((adid) => { + if (adid) + adapty.setIntegrationIdentifier("adjust_device_id", adid); + }); ``` ### AirBridge \{#airbridge\} Cập nhật code ứng dụng di động của bạn như hướng dẫn bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp AirBridge](airbridge#connect-your-app-to-airbridge). ```diff showLineNumbers import Airbridge from 'airbridge-react-native-sdk'; import { adapty } from 'react-native-adapty'; try { const deviceId = await Airbridge.state.deviceUUID(); - await adapty.updateProfile({ - airbridgeDeviceId: deviceId, - }); + await adapty.setIntegrationIdentifier("airbridge_device_id", deviceId); } catch (error) { // handle `AdaptyError` } ``` ### Amplitude \{#amplitude\} Cập nhật code ứng dụng di động của bạn như hướng dẫn 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 { adapty } from 'react-native-adapty'; try { - await adapty.updateProfile({ - amplitudeDeviceId: deviceId, - amplitudeUserId: userId, - }); + await adapty.setIntegrationIdentifier("amplitude_device_id", deviceId); + await adapty.setIntegrationIdentifier("amplitude_user_id", userId); } catch (error) { // handle `AdaptyError` } ``` ### AppMetrica \{#appmetrica\} Cập nhật code ứng dụng di động của bạn như hướng dẫn 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 { adapty } from 'react-native-adapty'; import AppMetrica, { DEVICE_ID_KEY, StartupParams, StartupParamsReason } from '@appmetrica/react-native-analytics'; // ... const startupParamsCallback = async ( params?: StartupParams, reason?: StartupParamsReason ) => { const deviceId = params?.deviceId if (deviceId) { try { - await adapty.updateProfile({ - appmetricaProfileId: 'YOUR_ADAPTY_CUSTOMER_USER_ID', - appmetricaDeviceId: deviceId, - }); + await adapty.setIntegrationIdentifier("appmetrica_profile_id", 'YOUR_ADAPTY_CUSTOMER_USER_ID'); + await adapty.setIntegrationIdentifier("appmetrica_device_id", deviceId); } catch (error) { // handle `AdaptyError` } } } AppMetrica.requestStartupParams(startupParamsCallback, [DEVICE_ID_KEY]) ``` ### AppsFlyer \{#appsflyer\} Cập nhật code ứng dụng di động của bạn như hướng dẫn 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 { adapty, AttributionSource } from 'react-native-adapty'; import appsFlyer from 'react-native-appsflyer'; appsFlyer.onInstallConversionData(installData => { try { - const networkUserId = appsFlyer.getAppsFlyerUID(); - adapty.updateAttribution(installData, AttributionSource.AppsFlyer, networkUserId); + const uid = appsFlyer.getAppsFlyerUID(); + adapty.setIntegrationIdentifier("appsflyer_id", uid); + adapty.updateAttribution(installData, "appsflyer"); } catch (error) { // handle the error } }); // ... appsFlyer.initSdk(/*...*/); ``` ### Branch \{#branch\} Cập nhật code ứng dụng di động của bạn như hướng dẫn 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 import { adapty, AttributionSource } from 'react-native-adapty'; import branch from 'react-native-branch'; branch.subscribe({ enComplete: ({ params, }) => { - adapty.updateAttribution(params, AttributionSource.Branch); + adapty.updateAttribution(params, "branch"); }, }); ``` ### Facebook Ads \{#facebook-ads\} Cập nhật code ứng dụng di động của bạn như hướng dẫn bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Facebook Ads](facebook-ads#connect-your-app-to-facebook-ads). ```diff showLineNumbers import { adapty } from 'react-native-adapty'; import { AppEventsLogger } from 'react-native-fbsdk-next'; try { const anonymousId = await AppEventsLogger.getAnonymousID(); - await adapty.updateProfile({ - facebookAnonymousId: anonymousId, - }); + await adapty.setIntegrationIdentifier("facebook_anonymous_id", anonymousId); } catch (error) { // handle `AdaptyError` } ``` ### Firebase and Google Analytics \{#firebase-and-google-analytics\} Cập nhật code ứng dụng di động của bạn như hướng dẫn bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Firebase and Google Analytics](firebase-and-google-analytics). ```diff showLineNumbers import analytics from '@react-native-firebase/analytics'; import { adapty } from 'react-native-adapty'; try { const appInstanceId = await analytics().getAppInstanceId(); - await adapty.updateProfile({ - firebaseAppInstanceId: appInstanceId, - }); + await adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId); } catch (error) { // handle `AdaptyError` } ``` ### Mixpanel \{#mixpanel\} Cập nhật code ứng dụng di động của bạn như hướng dẫn bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Mixpanel](mixpanel#sdk-configuration). ```diff showLineNumbers import { adapty } from 'react-native-adapty'; import { Mixpanel } from 'mixpanel-react-native'; // ... try { - await adapty.updateProfile({ - mixpanelUserId: mixpanelUserId, - }); + await adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelUserId); } catch (error) { // handle `AdaptyError` } ``` ### OneSignal \{#onesignal\} Cập nhật code ứng dụng di động của bạn như hướng dẫn 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). <Tabs groupId="current-os" queryString> <TabItem value="v5+" label="OneSignal SDK v5+ (current)" default> ```diff showLineNumbers import { adapty } from 'react-native-adapty'; import OneSignal from 'react-native-onesignal'; OneSignal.User.pushSubscription.addEventListener('change', (subscription) => { const subscriptionId = subscription.current.id; if (subscriptionId) { - adapty.updateProfile({ - oneSignalSubscriptionId: subscriptionId, - }); + adapty.setIntegrationIdentifier("one_signal_subscription_id", subscriptionId); } }); ``` </TabItem> <TabItem value="pre-v5" label="OneSignal SDK v. up to 4.x (legacy)" default> ```diff showLineNumbers import { adapty } from 'react-native-adapty'; import OneSignal from 'react-native-onesignal'; OneSignal.addSubscriptionObserver(event => { const playerId = event.to.userId; - adapty.updateProfile({ - oneSignalPlayerId: playerId, - }); + adapty.setIntegrationIdentifier("one_signal_player_id", playerId); }); ``` </TabItem> </Tabs> ### Pushwoosh \{#pushwoosh\} Cập nhật code ứng dụng di động của bạn như hướng dẫn 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 import { adapty } from 'react-native-adapty'; import Pushwoosh from 'pushwoosh-react-native-plugin'; // ... try { - await adapty.updateProfile({ - pushwooshHWID: hwid, - }); + await adapty.setIntegrationIdentifier("pushwoosh_hwid", hwid); } catch (error) { // handle `AdaptyError` } ``` ## Cập nhật cách triển khai Observer mode \{#update-observer-mode-implementation\} Cập nhật cách bạn liên kết paywall với giao dịch. Trước đây, bạn sử 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 hoàn chỉnh trong [Liên kết paywall với giao dịch mua trong Observer mode](report-transactions-observer-mode-react-native). :::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 ra 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. Đây là bước không thể bỏ qua! ::: :::note Hãy lưu ý rằng thứ tự các tham số của phương thức `reportTransaction` khác với phương thức `setVariationId`. ::: ```diff showLineNumbers const variationId = paywall.variationId; try { - await adapty.setVariationId(variationId, transactionId); + await adapty.reportTransaction(transactionId, variationId); } catch (error) { // handle the `AdaptyError` } ``` --- # File: migration-to-react-native-sdk-v3 --- --- title: "Migrate Adapty React Native SDK to v3.0" description: "Migrate to Adapty React Native SDK v3.0 for better performance and new monetization features." --- Adapty SDK v3.0 hỗ trợ [Adapty Paywall Builder](adapty-paywall-builder) mới — công cụ no-code thân thiện với người dùng để tạo paywall. Với tính linh hoạt tối đa và khả năng thiết kế phong phú, paywall của bạn sẽ trở nên hiệu quả và sinh lời hơn bao giờ hết. ## Nâng cấp lên phiên bản 3.0.1 \{#upgrade-to-version-301\} 1. Nâng cấp lên phiên bản 3.0.1 như bình thường. 2. Thay thế các tệp paywall dự phòng: 1. [Tải phiên bản mới nhất](fallback-paywalls) từ Adapty Dashboard. 2. Lưu chúng trên thiết bị của người dùng và truyền vào phương thức `.setFallbackPaywalls` như mô tả [tại đây](react-native-use-fallback-paywalls). --- # End of Documentation _Generated on: 2026-07-24T13:01:43.527Z_ _Successfully processed: 45/45 files_ # TUTORIAL - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.530Z Total files: 277 --- # File: is-adapty-right-for-me --- --- title: "Adapty có phù hợp với tôi không?" description: "Tìm hiểu xem Adapty có phù hợp với nhu cầu của bạn không. Dù bạn đang ra mắt ứng dụng mới, tối ưu doanh thu, hay migrate từ một công cụ khác — đây là nơi để bắt đầu." --- Adapty là nền tảng in-app purchase dành cho ứng dụng di động. Nó xử lý các gói đăng ký, sản phẩm mua một lần và consumable — từ xử lý giao dịch và xác thực receipt đến analytics, A/B test và tích hợp. Dưới đây là cách Adapty hoạt động trong từng tình huống cụ thể. ## Tôi đang ra mắt ứng dụng mới có in-app purchase \{#im-launching-a-new-app-with-in-app-purchases\} Dù bạn bán gói đăng ký, sản phẩm mua một lần hay consumable, Adapty đều hỗ trợ toàn bộ: - **SDK cho 7 nền tảng**: iOS, Android, React Native, Flutter, Unity, Kotlin Multiplatform và Capacitor. - **Xử lý giao dịch**: Gói đăng ký với gia hạn và logic thử lại, sản phẩm mua một lần, consumable và xác thực biên lai — tất cả đều được quản lý tự động. - **Paywall Builder không cần code**: Thiết kế và triển khai paywall mà không cần viết code UI. - **Analytics từ ngày đầu tiên**: Theo dõi doanh thu, dùng thử, chuyển đổi và nhiều hơn nữa ngay khi người dùng đầu tiên xuất hiện. Sẵn sàng bắt đầu? Hãy theo dõi [hướng dẫn Quickstart](quickstart). ## Tôi muốn A/B test, phân tích dữ liệu và tích hợp \{#i-want-ab-tests-analytics-and-integrations\} Adapty giúp bạn tối ưu hóa những gì đang hoạt động tốt: - **A/B testing**: Thử nghiệm các mức giá, thiết kế paywall, thời gian dùng thử và ưu đãi khác nhau để tìm ra phương án chuyển đổi tốt nhất. Dùng [AI Growth Advisor](autopilot) để nhận đề xuất A/B test được cá nhân hóa cho app của bạn, dựa trên dữ liệu từ hơn 20.000 ứng dụng đăng ký. - **Biểu đồ phân tích**: Theo dõi MRR, LTV, tỷ lệ rời bỏ, tỷ lệ giữ chân và hàng chục chỉ số khác. - **Phân khúc đối tượng**: Nhắm mục tiêu đến các nhóm người dùng cụ thể với paywall và ưu đãi phù hợp. - **Cấu hình paywall từ xa**: Cập nhật paywall mà không cần phát hành phiên bản app mới. - **Tích hợp bên thứ ba**: Gửi sự kiện mua hàng đến Amplitude, AppsFlyer, Adjust, Mixpanel và các công cụ khác mà nhóm của bạn đang dùng. Khám phá [A/B test](ab-tests), [Analytics](analytics), [Tích hợp dịch vụ analytics](analytics-integration), hoặc [Tích hợp dịch vụ attribution](attribution-integration). ## Tôi muốn triển khai in-app purchase với một LLM \{#i-want-to-implement-in-app-purchases-with-an-llm\} Tài liệu của Adapty được tối ưu hóa để dùng với các trợ lý lập trình AI như Cursor, Claude, ChatGPT và nhiều công cụ khác. Mọi trang đều có sẵn dưới dạng Markdown thuần, và chúng tôi cung cấp các hướng dẫn triển khai từng bước với sự hỗ trợ của LLM cho từng nền tảng: - **Hướng dẫn sẵn sàng để sao chép**: Gửi hướng dẫn cho LLM của bạn và để nó dẫn dắt bạn qua từng giai đoạn triển khai. - **Truy cập Markdown**: Thêm `.md` vào bất kỳ URL tài liệu nào hoặc nhấp vào **Copy for LLM** để nhận phiên bản văn bản sạch. - **Hỗ trợ Context7 MCP**: Kết nối tài liệu Adapty trực tiếp với IDE hỗ trợ LLM của bạn. Chọn nền tảng của bạn và bắt đầu: [Tích hợp Adapty với sự hỗ trợ của AI](adapty-cursor). ## Tôi muốn chạy và tối ưu hóa chiến dịch Apple Ads \{#i-want-to-run-and-optimize-apple-ads-campaigns\} Nếu bạn đang chạy Apple Search Ads, Adapty Ads Manager kết nối hiệu suất chiến dịch của bạn trực tiếp với các chỉ số doanh thu — không cần MMP: - **Dữ liệu hiệu suất theo thời gian thực**: Theo dõi chiến dịch, nhóm quảng cáo và từ khóa. - **Theo dõi doanh thu toàn trình**: Theo dõi chuỗi từ tìm kiếm → cài đặt → dùng thử → gói đăng ký → LTV. - **Dự đoán và đề xuất bằng AI**: Dự báo lợi nhuận và nhận gợi ý mở rộng quy mô. - **AI Agent**: Đặt câu hỏi bằng ngôn ngữ tự nhiên về tài khoản của bạn và nhận câu trả lời cùng đề xuất cho toàn bộ phễu. - **Tự động hóa theo quy tắc**: Giữ ổn định mục tiêu CPA và ROAS của bạn. Bắt đầu với [Adapty Ads Manager](adapty-ads-manager). ## Tôi muốn theo dõi nguồn gốc người dùng \{#i-want-to-track-where-my-users-come-from\} Adapty Attribution là giải pháp attribution tích hợp sẵn, giúp kết nối chi phí quảng cáo với lượt cài đặt ứng dụng và doanh thu gói đăng ký: - **Dashboard marketing hợp nhất**: Xem ROAS, lượt cài đặt và doanh thu từ tất cả các kênh của bạn trong một nơi. - **Attribution tích hợp sẵn**: Kết nối các chiến dịch quảng cáo với lượt cài đặt và doanh thu mà không cần dựa vào MMP bên ngoài. - **Tracking links**: Tạo link trong Adapty và thêm vào chiến dịch của bạn để attribution chính xác. - **Deferred deeplinks**: Điều hướng người dùng đến đúng nội dung sau khi cài đặt, kể cả khi họ chưa có ứng dụng lúc nhấn vào link. - **Phân tích cohort**: Phân tích hiệu quả acquisition và hành vi người dùng theo thời gian. Tìm hiểu thêm về [Adapty Attribution](adapty-user-acquisition). ## Tôi muốn chuyển đổi người dùng dùng thử và thu hút lại người đăng ký đã rời bỏ qua email \{#i-want-to-convert-trial-users-and-recover-churned-subscribers-via-email\} Adapty Mail biến dữ liệu người dùng Adapty của bạn thành các chiến dịch email do AI tạo ra, nhắm mục tiêu đến người dùng dùng thử, người đăng ký đã rời bỏ, và các sự kiện vòng đời khác: - **Chiến dịch do AI tạo**: Adapty tự động tạo nội dung và thiết kế cho từng chiến dịch dựa trên hồ sơ thương hiệu của bạn. - **Trigger theo vòng đời**: Tự động gửi chiến dịch dựa trên các sự kiện vòng đời quan trọng. - **Thanh toán qua web paywall**: Liên kết thanh toán được cá nhân hóa cho từng người nhận — các giao dịch mua được gán cho email đã dẫn đến chúng. - **Gửi từ tên miền của bạn**: Tất cả email được gửi từ tên miền đã xác minh của bạn — không cần nền tảng email riêng biệt. Tìm hiểu thêm về [Adapty Mail](adapty-mail). ## Tôi muốn iterate nhanh mà không cần phát hành app mới \{#i-want-to-iterate-fast-without-app-releases\} Sau khi tích hợp Adapty, hầu hết công việc hàng ngày đều thực hiện trên dashboard — không cần phát hành phiên bản app mới: - **Flows**: Thiết kế paywall và onboarding trong trình chỉnh sửa trực quan và xuất bản thay đổi ngay lập tức. - **A/B test từ dashboard**: Khởi chạy thử nghiệm, điều chỉnh giá, và thay đổi ưu đãi mà không cần chỉnh code. - **Analytics trên dashboard**: Theo dõi doanh thu, tỷ lệ hủy đăng ký, dùng thử, và chuyển đổi theo thời gian thực. - **Báo cáo qua Slack và email**: Nhận cập nhật tự động về các chỉ số quan trọng với team của bạn. Khám phá [Flows](adapty-flow-builder) hoặc xem [Analytics](charts). ## Tôi bán hàng trên web và cần ứng dụng di động \{#i-sell-on-the-web-and-need-a-mobile-app\} Nếu người dùng của bạn đã thanh toán qua website và bạn đang thêm ứng dụng di động, Adapty sẽ đồng bộ các giao dịch mua trên nhiều nền tảng: - **Tích hợp Stripe và Paddle**: Tự động đồng bộ các giao dịch mua trên web vào Adapty. - **Đồng bộ từ web sang di động**: Người dùng đã thanh toán trên web sẽ có quyền truy cập trong ứng dụng, và ngược lại. - **Phân tích thống nhất đa nền tảng**: Xem doanh thu từ web và di động trong một dashboard duy nhất. Thiết lập [tích hợp Stripe](stripe), [tích hợp Paddle](paddle), hoặc tìm hiểu cách [đồng bộ người dùng đăng ký từ web và di động](sync-subscribers-from-web). ## Tôi đang migrate từ công cụ khác \{#im-migrating-from-another-tool\} Adapty giúp bạn dễ dàng migrate từ các nền tảng gói đăng ký khác: - **Hướng dẫn migration**: Hướng dẫn từng bước để chuyển từ các nền tảng gói đăng ký khác. - **Observer Mode**: Giữ nguyên code thanh toán hiện có và áp dụng Adapty dần dần với [Observer mode](observer-vs-full-mode) — bắt đầu với analytics và A/B test, rồi mở rộng khi sẵn sàng. - **Import dữ liệu lịch sử**: Đưa lịch sử giao dịch hiện có vào Adapty để analytics của bạn luôn đầy đủ. Tìm hiểu về [migration sang Adapty](migrate-to-adapty-from-another-solutions) và [nhập dữ liệu lịch sử](importing-historical-data-to-adapty). --- Vẫn đang tìm hiểu? [Hướng dẫn Quickstart](quickstart) luôn là nơi tốt để bắt đầu. --- # File: integrate-payments --- --- title: "Tích hợp với cửa hàng hoặc nền tảng thanh toán" description: "Tích hợp Adapty với App Store, Google Play, cửa hàng tùy chỉnh, Stripe và Paddle." --- Để bắt đầu với Adapty, trước tiên hãy tích hợp với các cửa hàng nơi người dùng của bạn mua sản phẩm. Adapty kết nối với nhiều cửa hàng ứng dụng và nhà cung cấp thanh toán web, tập hợp tất cả các in-app purchase và dữ liệu phân tích của bạn vào một nơi. ## Tích hợp với cửa hàng và thanh toán web \{#integrate-with-stores-and-web-payments\} Chọn cửa hàng của bạn bên dưới để xem các bước tích hợp chi tiết: - [App Store](initial_ios) - [Google Play](initial-android) - Thanh toán web: - [Stripe](stripe) - [Paddle](paddle) - [Cửa hàng khác](custom-store) ## Các bước tiếp theo \{#next-steps\} Sau khi đã kết nối cửa hàng hoặc nền tảng thanh toán, bạn có thể tiến hành [thêm sản phẩm](quickstart-products). --- # File: quickstart-products --- --- title: "Thêm sản phẩm" description: "Thêm in-app purchase hoặc gói đăng ký vào Adapty và liên kết chúng với danh sách sản phẩm trên App Store, Google Play, Stripe, Paddle hoặc cửa hàng tùy chỉnh." --- :::tip Đang thiết lập Adapty theo phương thức lập trình? Bạn có thể hoàn thành bước này bằng [Developer CLI](developer-cli-quickstart). ::: Trước khi sử dụng các tính năng cốt lõi của Adapty, bạn cần thêm từng sản phẩm đang bán và liên kết chúng với mọi cửa hàng hoặc nền tảng thanh toán mà bạn hỗ trợ. Việc thiết lập này cho phép bạn phân phối sản phẩm đến thiết bị của người dùng và theo dõi chúng trong phân tích sau này. Trong Adapty, mọi thứ ứng dụng của bạn bán đều là một **sản phẩm**. Nếu cùng một mặt hàng tồn tại trên App Store, Google Play hoặc Stripe, bạn có thể gộp chúng thành một sản phẩm duy nhất trong Adapty. Thiết lập một lần và quản lý trên tất cả các nền tảng từ một nơi. Hãy thêm sản phẩm đầu tiên của bạn. <Tabs groupId="products" queryString> <TabItem value="no-products" label="Chưa có sản phẩm trong cửa hàng" default> <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/qUpC2XG-r5E?si=7Komyv4_PUQ4FaEH" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> </TabItem> <TabItem value="products-in-stores" label="Đã có sản phẩm trong cửa hàng"> <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/nlkdKCF0SwY?si=VVigzHcpv3waKJmI" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> </TabItem> </Tabs> ## Thêm sản phẩm đầu tiên \{#add-your-first-product\} :::tip Hướng dẫn nhanh này bao gồm những điều cơ bản bạn cần để tạo sản phẩm. Để biết thêm chi tiết, xem hướng dẫn về [tạo sản phẩm](create-product). ::: Giả sử bạn muốn thêm một gói đăng ký hàng tháng làm sản phẩm. 1. Vào [Products](https://app.adapty.io/products) từ menu chính của Adapty. 2. Nhấn **Create product** ở góc trên bên phải. <img src={require('./img/products-tab.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important **Các bước tiếp theo phụ thuộc vào việc bạn đã có sản phẩm trên App Store và/hoặc Google Play chưa:** ::: <Tabs groupId="products" queryString> <TabItem value="no-products" label="Chưa có sản phẩm trong cửa hàng" default> :::important Trước khi bắt đầu, hãy đảm bảo bạn đã cấu hình tích hợp với [App Store](initial_ios) và/hoặc [Google Play](initial-android). Đối với App Store, hãy đảm bảo bạn đã [thêm App Store Connect API key](app-store-connection-configuration#step-6-add-app-store-connect-api-key) để Adapty có thể đẩy sản phẩm lên. ::: 3. Chọn **Create a new product and push to stores**. 4. Điền thông tin sản phẩm: - **Product name**: Tên chỉ hiển thị với bạn trong toàn bộ Adapty dashboard. - **Access Level**: Định danh duy nhất xác định tính năng nào được mở khóa sau khi mua. Nếu tất cả người dùng trả phí trong ứng dụng của bạn đều truy cập vào các tính năng giống nhau, bạn có thể sử dụng mức độ truy cập mặc định: `premium`. Với các thiết lập phức tạp hơn, hãy tạo thêm [mức độ truy cập](access-level). - **Subscription duration**: Chọn thời hạn của gói đăng ký từ danh sách. - **Weekly/Monthly/2 Months/3 Months/6 Months/Annual**: Thời hạn gói đăng ký. - **Lifetime**: Sử dụng thời hạn trọn đời cho các sản phẩm mở khóa tính năng cao cấp của ứng dụng mãi mãi. - **Non-Subscriptions**: Đối với các sản phẩm không phải gói đăng ký và do đó không có thời hạn, hãy sử dụng non-subscriptions. Chúng có thể được dùng để mở khóa tính năng bổ sung, sản phẩm consumable, v.v. - **Consumables**: Các mặt hàng consumable có thể được mua nhiều lần. Chúng có thể được sử dụng hết trong vòng đời của ứng dụng. Ví dụ là tiền tệ trong game và các vật phẩm thêm. Lưu ý rằng sản phẩm consumable không ảnh hưởng đến mức độ truy cập. - **Price (USD)**: Giá sản phẩm bằng USD. Mức giá này sẽ được dùng làm cơ sở để tự động tính toán và thiết lập giá trên tất cả các quốc gia. Bạn có thể [tùy chỉnh giá cho từng quốc gia và khu vực](edit-product#set-country-specific-prices) sau. <img src={require('./img/create-product-push.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấn **Save & Continue** và chuyển sang tab **App Store** hoặc **Google Play** để điền thông tin sản phẩm cho cửa hàng. <Tabs> <TabItem value="App Store" label="App Store" default> - **Product ID**: Tạo một ID duy nhất và vĩnh viễn cho sản phẩm. - **Product group**: Chọn nhóm sản phẩm hiện có mà bạn đã tạo trong App Store Connect hoặc nhấn **Create new Product Group** và đặt tên cùng ID cho nhóm. Sau khi Adapty tạo xong, bạn có thể chọn từ danh sách thả xuống. - **Screenshot**: Tải lên ảnh chụp màn hình của in-app purchase hiển thị rõ mặt hàng hoặc dịch vụ được cung cấp. Ảnh chụp màn hình này chỉ dùng cho việc xét duyệt App Store và không hiển thị trên App Store. Xem yêu cầu về kích thước và định dạng ảnh chụp màn hình [tại đây](https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/). :::warning Nếu đây là sản phẩm đầu tiên của ứng dụng này, bạn phải tự tay gửi để xét duyệt trong App Store Connect. Điều này sẽ không cần thiết về sau. Sau khi xét duyệt xong, trạng thái sản phẩm trong Adapty sẽ tự động cập nhật. ::: </TabItem> <TabItem value="Google Play" label="Google Play" default> - **Base Product ID**: Tạo một ID duy nhất và vĩnh viễn cho sản phẩm. - **Subscription**: Chọn nhóm gói đăng ký hiện có mà bạn đã tạo trong Google Play Console hoặc nhấn **Create new Product Group** và đặt tên cùng ID. Sau khi Adapty tạo xong, bạn có thể chọn từ danh sách thả xuống. </TabItem> </Tabs> 6. Đối với iOS, hãy cấu hình ưu đãi giới thiệu – dùng thử miễn phí – bằng cách chọn **Free duration** từ danh sách thả xuống. Trong thiết lập ban đầu này, bạn có thể thêm bản dùng thử miễn phí giới thiệu. Sau khi sản phẩm chính được cửa hàng phê duyệt, bạn có thể [thêm các ưu đãi khác](offers) (ví dụ: ưu đãi, ưu đãi thu hút khách hàng cũ) bằng cách liên kết ID hiện có từ bảng điều khiển cửa hàng của bạn. :::important Ưu đãi giới thiệu không tự động đồng bộ với Google Play. Không giống App Store, Google Play không có loại "ưu đãi giới thiệu" riêng — các bản dùng thử miễn phí và ưu đãi giảm giá đều được cấu hình dưới dạng **offers** trên base plan. [Tạo ưu đãi trong Google Play Console và liên kết với sản phẩm Adapty của bạn](google-play-offers). ::: </TabItem> <TabItem value="products-in-stores" label="Đã có sản phẩm trong cửa hàng"> 3. Chọn **Connect an existing store product**. 4. Điền thông tin sản phẩm: - **Product name**: Tên chỉ hiển thị với bạn trong toàn bộ Adapty dashboard. - **Access level ID**: Định danh duy nhất xác định tính năng nào được mở khóa sau khi mua. Nếu tất cả người dùng trả phí trong ứng dụng của bạn đều truy cập vào các tính năng giống nhau, bạn có thể sử dụng mức độ truy cập mặc định: `premium`. Với các thiết lập phức tạp hơn, hãy tạo thêm [mức độ truy cập](access-level). - **Subscription duration**: Chọn thời hạn của gói đăng ký từ danh sách. - **Weekly/Monthly/2 Months/3 Months/6 Months/Annual**: Thời hạn gói đăng ký. - **Lifetime**: Sử dụng thời hạn trọn đời cho các sản phẩm mở khóa tính năng cao cấp của ứng dụng mãi mãi. - **Non-Subscriptions**: Đối với các sản phẩm không phải gói đăng ký và do đó không có thời hạn, hãy sử dụng non-subscriptions. Chúng có thể được dùng để mở khóa tính năng bổ sung, sản phẩm consumable, v.v. - **Consumables**: Các mặt hàng consumable có thể được mua nhiều lần. Chúng có thể được sử dụng hết trong vòng đời của ứng dụng. Ví dụ là tiền tệ trong game và các vật phẩm thêm. Lưu ý rằng sản phẩm consumable không ảnh hưởng đến mức độ truy cập. - **Price (USD)**: Giá sản phẩm bằng USD. Nếu sản phẩm của bạn đã có trong cửa hàng, giá trị này sẽ không ảnh hưởng đến giá thực tế trong cửa hàng; bạn có thể chọn bất kỳ giá trị nào từ danh sách. Sau đó, bạn có thể [tùy chỉnh giá cho từng khu vực](edit-product#set-country-specific-prices) ngay trong Adapty dashboard. <img src={require('./img/product-info.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <br /> 5. Thêm thông tin cửa hàng. Chọn cửa hàng của bạn: <Tabs> <TabItem value="App Store" label="App Store" default> - **App Store Product ID**: Định danh duy nhất dùng để truy cập sản phẩm của bạn trên các thiết bị. Nếu không tìm thấy, hãy đảm bảo ID chính xác và thuộc đúng ứng dụng. </TabItem> <TabItem value="Google Play" label="Google Play" default> - **Google Play Product ID**: Định danh sản phẩm từ Play Store. Chọn từ danh sách các ID sản phẩm hiện có. Nếu không tìm thấy, hãy đảm bảo ID chính xác và thuộc đúng ứng dụng. - **Base plan ID**: ID xác định base plan cho sản phẩm trong Play Store. - **Legacy fallback product**: Sản phẩm dự phòng này chỉ được dùng cho các ứng dụng sử dụng phiên bản SDK Adapty cũ hơn (phiên bản 2.5 trở xuống). Chỉ định giá trị theo định dạng `<subscription_id>:<base_plan_id>`. :::important Ưu đãi giới thiệu không tự động đồng bộ với Google Play. Không giống App Store, Google Play không có loại "ưu đãi giới thiệu" riêng — các bản dùng thử miễn phí và ưu đãi giảm giá đều được cấu hình dưới dạng **offers** trên base plan. [Tạo ưu đãi trong Google Play Console và liên kết với sản phẩm Adapty của bạn](google-play-offers). ::: <details> <summary>Nhấn vào đây để biết cách tìm Google Play Product ID và Base plan ID.</summary> 1. Vào **Monetize with Play > Products > Subscriptions** trong tài khoản [Google Play Console](https://play.google.com/console/developers/android/app) của bạn. 2. Mở **Subscription** cho giao dịch mua. 3. Bạn sẽ thấy Product ID trong phần **Subscription details** và Base plan ID trong cột **ID and duration** của phần **Base plans and offers**. <img src={require('./img/play-store-id.png').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </details> </TabItem> <TabItem value="Stripe" label="Stripe" default> - **Stripe Product ID**: Định danh sản phẩm duy nhất từ Stripe. - **Stripe Price ID**: Định danh duy nhất từ Stripe cho mức giá liên kết với sản phẩm. <details> <summary>Nhấn vào đây để biết cách tìm Stripe Product ID và Price ID.</summary> 1. Vào [Product Catalog](https://dashboard.stripe.com/products?active=true) trong Stripe của bạn. 2. Mở sản phẩm bạn cần. 3. Bạn sẽ thấy: - Stripe Product ID (có dạng `prod_...`) ở góc trên bên phải. - Stripe Price ID (có dạng `price_...`) trong cột **API ID** của phần **Pricing**. <img src={require('./img/product-stripe.png').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </details> </TabItem> <TabItem value="Paddle" label="Paddle" default> - **Paddle Product ID**: Định danh sản phẩm duy nhất từ Paddle. - **Paddle Price ID**: Định danh duy nhất từ Paddle cho mức giá liên kết với sản phẩm. <details> <summary>Nhấn vào đây để biết cách tìm Paddle Product ID và Price ID.</summary> 1. Vào [Product Catalog](https://vendors.paddle.com/products-v2) trong Paddle của bạn. 2. Mở sản phẩm bạn cần. 3. Bạn sẽ thấy: - Paddle Product ID (có dạng `pro_...`) trong phần **Additional details**. - Paddle Price ID (có dạng `pri_...`) trong cột **ID** của phần **Prices**. <img src={require('./img/paddle-product-price.webp').default} style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </details> </TabItem> <TabItem value="Custom" label="Custom store" default> Bạn có thể chọn một cửa hàng tùy chỉnh hiện có hoặc thêm mới và liên kết sản phẩm với nó. Lưu ý rằng Adapty chỉ theo dõi các giao dịch từ App Store, Google Play và Stripe. Đối với cửa hàng tùy chỉnh, bạn cần gửi giao dịch bằng Adapty server-side API [Set transaction method](api-adapty/operations/setTransaction). </TabItem> </Tabs> 6. Bạn có thể [tạo ưu đãi](create-offer) cho sản phẩm nếu cần. Để thêm ưu đãi, nhấn **Yes, add offers**. Nếu không, nhấn **No, thanks**. Sản phẩm của bạn sẽ xuất hiện trong danh sách sản phẩm. <img src={require('./img/created-product.png').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </TabItem> </Tabs> ## Các bước tiếp theo \{#next-steps\} Sau khi đã thêm sản phẩm vào Adapty, bạn có thể tiến hành [thiết lập paywall](quickstart-paywalls) vì đây là cách duy nhất để bắt đầu bán chúng. --- # File: quickstart-paywalls --- --- title: "Kích hoạt mua hàng" description: "Thêm flow hoặc paywall trong Adapty để hiển thị sản phẩm của bạn, sau đó gắn vào một placement." --- :::info Để tiếp tục với hướng dẫn này, hãy đảm bảo bạn đã hoàn thành [tích hợp cửa hàng](integrate-payments) và tạo ít nhất một sản phẩm như mô tả trong [hướng dẫn thêm sản phẩm](quickstart-products) trước đó. ::: Bây giờ bạn đã có sản phẩm, bạn cần một cách để hiển thị chúng cho người dùng. Adapty cung cấp cho bạn ba lựa chọn: - **Flow Builder (nên dùng)**: Trình chỉnh sửa trực quan không cần code cho toàn bộ hành trình mua hàng. Adapty SDK render kết quả trực tiếp, nên bạn không cần viết code UI. - **Paywall thủ công**: Bạn tự tạo paywall, gắn sản phẩm vào đó và tự render UI trong code ứng dụng. - **Adapty Paywall Builder (Legacy)**: Trình chỉnh sửa paywall không cần code. Cả hai lựa chọn đều kết thúc theo cùng một cách: bạn gắn thứ mình đã tạo vào một [placement](placements). Placement là thứ ứng dụng của bạn gọi tại runtime để lấy nội dung phù hợp cho đúng người dùng. <Tabs groupId="purchase-setup" queryString> <TabItem value="flow-builder" label="Sử dụng Flow Builder" default> :::important Flow Builder hiện hỗ trợ iOS, Android, React Native, Flutter và Capacitor SDK v4 trở lên. Hỗ trợ cho các nền tảng khác sẽ sớm ra mắt. ::: Một flow là một hoặc nhiều màn hình có sản phẩm được nhúng trực tiếp. Bạn thiết kế nó trong [Flow Builder](adapty-flow-builder) — không cần viết code. SDK Adapty render flow ngay trên từng nền tảng. App của bạn gọi `getFlow`, và SDK sẽ hiển thị các màn hình, xử lý giao dịch mua, và báo cáo các sự kiện. Không cần viết code UI riêng, không cần duy trì paywall song song. ## 1. Tạo flow \{#1-build-the-flow\} 1. Vào [**Flows**](https://app.adapty.io/flows) trong menu chính của Adapty. 2. Nhấn **Create flow** và thiết kế flow của bạn. Tìm hiểu thêm về [Adapty Flow Builder](adapty-flow-builder). Các hướng dẫn theo mẫu dưới đây sẽ hướng dẫn từng bước cho những pattern phổ biến nhất: <CustomDocCardList ids={['basic-paywall-screen', 'show-plans-bottom-sheet', 'paywall-with-tabs', 'paywall-features-per-product', 'onboarding-flow-tutorial']} /> Sau khi flow đã được lưu và xuất bản, hãy tiến hành kết nối nó vào một placement. :::warning Đừng quên xuất bản flow! Nếu bạn không xuất bản, bạn sẽ không thể thêm flow vào placement. ::: ### 2. Thêm flow vào placement \{#2-add-the-flow-to-a-placement\} Tạo một <InlineTooltip tooltip="placement">Placement là một điểm cụ thể trong ứng dụng của bạn nơi bạn hiển thị flow, paywall, onboarding hoặc A/B test. Placement cho phép bạn nhắm mục tiêu đến các [đối tượng](audience) cụ thể với nội dung của mình. Tìm hiểu thêm về [placements](placements).</InlineTooltip> để ứng dụng của bạn có thể yêu cầu flow trong thời gian chạy. Hãy bắt đầu với placement quan trọng nhất — placement onboarding. Sau đó, bạn có thể thêm nhiều [placement có ý nghĩa hơn](choose-meaningful-placements) trong suốt hành trình người dùng. 1. Truy cập [**Placements**](https://app.adapty.io/placements) trong menu chính của Adapty và chuyển sang tab **Flows**. 2. Nhấp **Create placement**. 3. Nhập **Placement name** (ví dụ: `main` hoặc `onboarding`). Đây là tên định danh nội bộ trong Adapty Dashboard. 4. Nhập **Placement ID**. Bạn sẽ dùng ID này trong Adapty SDK để tải flow của placement. 5. Nhấp **Run flow** và chọn flow bạn vừa tạo. 6. Nhấp **Save & publish**. Trong code ứng dụng của bạn, bạn chỉ cần hardcode các placement ID. Mọi thứ khác — flow nào chạy, sản phẩm nào được bán, giao diện trông như thế nào — đều được cấu hình trên Adapty Dashboard và có thể thay đổi bất cứ lúc nào mà không cần cập nhật ứng dụng. :::tip Adapty cho phép bạn hiển thị các flow khác nhau cho các nhóm người dùng khác nhau và phân tích hiệu suất. Tìm hiểu thêm về [đối tượng](audience) và [A/B test](ab-tests). ::: </TabItem> <TabItem value="manual-paywall" label="Implement paywall manually"> <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/e4o7Z2tUGL8?si=ipwbW3VVN0fIg0R0" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Một paywall là container được cấu hình từ xa chứa một hoặc nhiều sản phẩm. Adapty cung cấp danh sách sản phẩm và một JSON payload [Remote Config](customize-paywall-with-remote-config) tùy chọn — code ứng dụng của bạn đọc chúng và tự dựng giao diện. :::tip Muốn thiết lập Adapty bằng lập trình? Bạn có thể hoàn thành bước này bằng [Developer CLI](developer-cli-quickstart). ::: ### 1. Tạo một paywall \{#create-a-paywall\} 1. Vào [**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty. 2. Nhấn **Create paywall**. 3. Nhập **Paywall name**. Đây là tên định danh nội bộ trong Adapty Dashboard. 4. Nhấn **Add product** và chọn các sản phẩm muốn hiển thị trên paywall. 5. (Tùy chọn) Mở tab **Remote config** và thêm JSON payload mà ứng dụng cần (tiêu đề, nội dung, feature flag). Xem [Thiết kế paywall với Remote Config](customize-paywall-with-remote-config) để biết thêm chi tiết. 6. Nhấn **Create as a draft**, sau đó xuất bản khi sẵn sàng. <img src="/assets/shared/img/quickstart-paywall.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bạn sẽ render paywall này trong code ứng dụng của mình. <InlineTooltip tooltip="tự triển khai paywall">Xem hướng dẫn cho nền tảng của bạn: [iOS](ios-implement-paywalls-manually), [Android](android-implement-paywalls-manually), [React Native](react-native-implement-paywalls-manually), [Flutter](flutter-implement-paywalls-manually), [Unity](unity-implement-paywalls-manually).</InlineTooltip> ### 2. Thêm paywall vào một placement \{#2-add-the-paywall-to-a-placement\} Tạo một <InlineTooltip tooltip="placement">Placement là một vị trí cụ thể trong ứng dụng của bạn, nơi bạn hiển thị flow, paywall, onboarding hoặc A/B test. Placement cho phép bạn nhắm mục tiêu đến các [đối tượng](audience) cụ thể với nội dung của mình. Tìm hiểu thêm về [placements](placements).</InlineTooltip> để ứng dụng có thể yêu cầu paywall khi chạy. Hãy bắt đầu với placement cơ bản nhất — placement onboarding. Sau đó, bạn có thể thêm nhiều [placement có ý nghĩa hơn](choose-meaningful-placements) trong suốt hành trình người dùng. 1. Vào [**Placements**](https://app.adapty.io/placements) trong menu chính của Adapty và chuyển sang tab **Paywalls**. 2. Nhấn **Create placement**. 3. Nhập **Placement name** (ví dụ: `main` hoặc `onboarding`). Đây là định danh nội bộ trong Adapty Dashboard. 4. Nhập **Placement ID**. Bạn sẽ dùng ID này trong SDK để tải paywall của placement. 5. Nhấn **Run paywall** và chọn paywall vừa tạo. 6. Nhấn **Save & publish**. <img src="/assets/shared/img/add-placement.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trong code ứng dụng, bạn chỉ cần hardcode các placement ID. Mọi thứ còn lại — paywall nào sẽ chạy, sản phẩm nào được bán, Remote Config — đều được cấu hình trong Adapty Dashboard và có thể thay đổi bất kỳ lúc nào mà không cần cập nhật ứng dụng. :::tip Adapty cho phép bạn hiển thị các paywall khác nhau cho từng nhóm người dùng và phân tích hiệu suất. Tìm hiểu thêm về [đối tượng](audience) và [A/B test](ab-tests). ::: </TabItem> <TabItem value="paywall-builder" label="Adapty Paywall Builder (Legacy)"> Paywall được xây dựng trong [Paywall Builder](adapty-paywall-builder) là màn hình không cần code với các sản phẩm được nhúng trực tiếp. Adapty SDK render natively, vì vậy bạn không cần viết code UI. :::warning Paywall Builder vẫn hoạt động đầy đủ, nhưng Adapty sẽ không bổ sung tính năng mới hay cập nhật cho nó nữa. Với các dự án mới, hãy sử dụng [Flow Builder](adapty-flow-builder) thay thế. ::: ### 1. Xây dựng paywall \{#1-build-the-paywall\} 1. Vào [**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty. 2. Nhấp **Create paywall**. 3. Nhập **Paywall name**. Đây là tên định danh nội bộ trong Adapty Dashboard. 4. Nhấp **Add product** và chọn các sản phẩm để hiển thị trên paywall. 5. Mở tab **Builder & Generator**. Tạo paywall từ template hoặc tạo bằng AI. 6. Bật toggle **Show on device** để SDK có thể render nó. ### 2. Thêm paywall vào một placement \{#2-add-the-paywall-to-a-placement\} Tạo một <InlineTooltip tooltip="placement">Placement là một vị trí cụ thể trong ứng dụng của bạn, nơi bạn hiển thị flow, paywall, onboarding hoặc A/B test. Placement cho phép bạn nhắm mục tiêu đến các [đối tượng](audience) cụ thể với nội dung của mình. Tìm hiểu thêm về [placement](placements).</InlineTooltip> để ứng dụng của bạn có thể yêu cầu paywall trong thời gian chạy. 1. Truy cập [**Placements**](https://app.adapty.io/placements) trong menu chính của Adapty và chuyển sang tab **Paywalls**. 2. Nhấp vào **Create placement**. 3. Nhập **Placement name** (ví dụ: `main` hoặc `onboarding`). Đây là mã định danh nội bộ trong Adapty Dashboard. 4. Nhập **Placement ID**. Bạn sẽ dùng ID này trong SDK để tải paywall của placement. 5. Nhấp vào **Run paywall** và chọn paywall bạn đã tạo. 6. Nhấp vào **Save & publish**. Trong code ứng dụng, bạn chỉ cần hardcode các placement ID. Tất cả những thứ còn lại — paywall nào đang chạy, sản phẩm nào được bán, giao diện trông như thế nào — đều được cấu hình trong Adapty Dashboard và có thể thay đổi bất kỳ lúc nào mà không cần cập nhật ứng dụng. </TabItem> </Tabs> ## Các bước tiếp theo \{#next-steps\} Giờ bạn đã có nội dung để SDK phân phối. Tiếp theo, hãy [tích hợp Adapty SDK](quickstart-sdk) vào ứng dụng của bạn và bắt đầu lấy placement. --- # File: quickstart-sdk --- --- title: "Tích hợp Adapty SDK vào code ứng dụng của bạn" description: "Tích hợp Adapty với App Store, Google Play, cửa hàng tùy chỉnh, Stripe và Paddle." --- Tích hợp Adapty SDK vào ứng dụng để: - Xử lý giao dịch mua, xác thực receipt và quản lý gói đăng ký một cách tự động - Tạo và kiểm tra paywall mà không cần cập nhật ứng dụng - Nhận phân tích mua hàng chi tiết mà không cần thiết lập thêm - bao gồm cohort, LTV, churn và phân tích funnel - Luôn cập nhật trạng thái gói đăng ký của người dùng trên các phiên sử dụng và thiết bị - Tích hợp ứng dụng với các dịch vụ attribution marketing và phân tích chỉ bằng một dòng code ## Cách thức hoạt động \{#how-does-it-work\} Để triển khai cơ bản Adapty SDK, bạn chỉ cần quan tâm đến ba việc: 1. Cài đặt và khởi tạo SDK. 2. Giao việc xử lý in-app purchase cho Adapty. 3. Theo dõi trạng thái gói đăng ký trong hồ sơ người dùng. Adapty xác định trạng thái, loại và thời hạn hết gói đăng ký – SDK chỉ cần tiêu thụ thông tin này. Thứ tự và chi tiết có thể khác nhau tùy từng ứng dụng, nhưng về cơ bản là vậy. ## Bắt đầu \{#get-started\} Chọn nền tảng của bạn và bắt đầu ngay: **iOS** - **[SDK Quickstart](ios-sdk-overview)** - **[Ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples)** **Android** - **[SDK Quickstart](android-sdk-overview)** - **[Ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app)** **React Native** - **[SDK Quickstart](react-native-sdk-overview)** - **[Ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/)** **Flutter** - **[SDK Quickstart](flutter-sdk-overview)** - **[Ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example)** **Unity** - **[SDK Quickstart](unity-sdk-overview)** - **[Ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Assets)** **Capacitor** - **[SDK Quickstart](capacitor-sdk-overview)** - **[Ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples)** **Kotlin Multiplatform**: - **[SDK Quickstart](kmp-sdk-overview)** - **[Ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example)** ## Các bước tiếp theo \{#next-steps\} Sau khi đã cấu hình Adapty SDK trong code ứng dụng, bạn có thể chuyển sang [kiểm tra triển khai](quickstart-test). --- # File: quickstart-test --- --- title: "Kiểm tra tích hợp Adapty của bạn" description: "Nhanh chóng xác minh tích hợp Adapty bằng cách kiểm tra kích hoạt SDK, tải paywall và in-app purchase trên App Store, Google Play, Stripe và Paddle." --- Bạn đã sẵn sàng! Bây giờ hãy đảm bảo rằng tích hợp của bạn hoạt động đúng như mong đợi và bạn có thể thấy các giao dịch mua trong Adapty Dashboard. Thực hiện một giao dịch mua thử nghiệm là cách tốt nhất để xác minh tích hợp của bạn hoạt động từ đầu đến cuối. Bắt đầu với một in-app purchase, sau đó kiểm tra kết quả. ## 1. Kiểm tra in-app purchase \{#1-test-in-app-purchases\} Làm theo hướng dẫn dựa trên cửa hàng hoặc nền tảng thanh toán của bạn. ### App store \{#app-store\} Chúng tôi khuyên bạn nên sử dụng tài khoản thử nghiệm (Sandbox Apple ID) và tiến hành kiểm tra trên thiết bị thật. Để tìm hiểu thêm về tất cả các bước kiểm tra, hãy xem bài viết chi tiết về [kiểm tra Sandbox trên App Store](test-purchases-in-sandbox). :::warning Hãy kiểm tra trên thiết bị thật để có kết quả đáng tin cậy nhất. Bạn có thể tùy chọn kiểm tra bằng trình giả lập, nhưng chúng tôi không khuyến nghị vì độ tin cậy thấp hơn. ::: ### Google Play Store \{#google-play-store\} Tạo người dùng thử nghiệm và kiểm tra ứng dụng của bạn trên thiết bị thật. Để tìm hiểu thêm về tất cả các bước kiểm tra, hãy xem bài viết chi tiết về [kiểm tra trên Google Play Store](testing-on-android). :::note Google [khuyến nghị](https://support.google.com/googleplay/android-developer/answer/14316361) sử dụng thiết bị thật để kiểm tra. Nếu bạn quyết định sử dụng trình giả lập, hãy đảm bảo rằng nó đã cài đặt Google Play để ứng dụng hoạt động đúng cách. ::: ### Stripe \{#stripe\} Kiểm tra giao dịch mua trên Stripe yêu cầu kết nối Stripe với Adapty bằng API key của chế độ Stripe Test. Các giao dịch bạn thực hiện từ chế độ Test của Stripe sẽ được coi là Sandbox trong Adapty. Để tìm hiểu thêm về tất cả các bước kết nối, hãy xem [bài viết tích hợp Stripe](stripe#6-test-your-integration). ### Paddle \{#paddle\} Kiểm tra giao dịch mua trên Paddle yêu cầu kết nối Paddle với Adapty bằng API key của môi trường thử nghiệm Paddle. Các giao dịch bạn thực hiện từ môi trường thử nghiệm của Paddle sẽ được coi là Test trong Adapty. Để tìm hiểu thêm về tất cả các bước kết nối, hãy xem [bài viết tích hợp Paddle](paddle#4-test-your-integration). ## 2. Xác minh giao dịch mua thử nghiệm \{#2-validate-test-purchases\} Sau khi thực hiện giao dịch mua thử nghiệm, hãy kiểm tra giao dịch tương ứng trong [**Event Feed**](https://app.adapty.io/event-feed) trên Adapty Dashboard. Nếu giao dịch mua không xuất hiện trong **Event Feed**, Adapty chưa ghi nhận nó. Tìm hiểu thêm trong hướng dẫn chi tiết về [xác minh giao dịch mua thử nghiệm](validate-test-purchases). <img src="/assets/shared/img/test-event-feed.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Các bước tiếp theo \{#next-steps\} Chúc mừng bạn đã tích hợp Adapty thành công! Bây giờ bạn đã sẵn sàng phát triển doanh thu từ in-app purchase. Chuẩn bị cho bản phát hành chính thức: <Button id="release-checklist"> Danh sách kiểm tra trước khi phát hành </Button> Hoặc bạn có thể tiếp tục với các nội dung sau: - **[A/B testing](ab-tests)**: Thử nghiệm với các mức giá, thời hạn gói đăng ký, thời gian dùng thử và các yếu tố hình ảnh khác nhau để tìm ra những kết hợp hiệu quả nhất. - **[Phân tích](how-adapty-analytics-works)**: Khám phá các chỉ số kiếm tiền chi tiết để hiểu hành vi người dùng và tối ưu hóa hiệu suất doanh thu. - **Tích hợp**: Adapty gửi [sự kiện gói đăng ký](events) đến các công cụ phân tích và attribution của bên thứ ba, như [Amplitude](amplitude), [AppsFlyer](appsflyer), [Adjust](adjust), [Branch](branch), [Mixpanel](mixpanel), [Facebook Ads](facebook-ads), [AppMetrica](appmetrica) và [Webhook](webhook) tùy chỉnh. :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: --- # File: release-checklist --- --- title: "Danh sách kiểm tra trước khi phát hành" description: "Làm theo danh sách kiểm tra của Adapty để đảm bảo quá trình cập nhật ứng dụng diễn ra suôn sẻ." --- Chúng tôi rất vui khi bạn quyết định sử dụng Adapty! Hy vọng quá trình tích hợp diễn ra suôn sẻ. Hướng dẫn này sẽ đưa bạn qua các bước để đảm bảo ứng dụng sẵn sàng phát hành lên cửa hàng, và bạn có thể yên tâm rằng flow thanh toán hoạt động đúng. ## Các yêu cầu cần có trước \{#pre-flight-essentials\} Những gì bạn cần trước khi bắt đầu kiểm tra: - Thiết bị thật với tài khoản sandbox - Quyền truy cập vào Adapty Dashboard - Quyền truy cập vào App Store Connect / Google Play Console :::note Mặc dù có thể thực hiện mua hàng sandbox trên máy ảo, nhưng bạn cần thiết bị thật để kiểm tra đầy đủ tất cả các flow, bao gồm cả hộp thoại thanh toán và xác thực sinh trắc học. ::: <Button id="test-purchases-in-sandbox"> Hướng dẫn kiểm tra cho App Store </Button> <Button id="testing-on-android"> Hướng dẫn kiểm tra cho Google Play </Button> ## Kiểm tra chung \{#universal-validations\} - [ ] **Kết nối cửa hàng**: Đảm bảo bạn đã kết nối Adapty với App Store và/hoặc Google Play: - [ ] [App Store](initial_ios) - [ ] [Google Play](initial-android) - [ ] **Gửi sự kiện gói đăng ký**: Xác nhận rằng thông báo từ máy chủ đã được thiết lập: - [ ] [Thông báo máy chủ App Store](enable-app-store-server-notifications) - [ ] [Thông báo nhà phát triển theo thời gian thực (RTDN)](enable-real-time-developer-notifications-rtdn) - [ ] **Nhận diện hồ sơ người dùng**: Kiểm tra logic nhận diện người dùng và đảm bảo các giao dịch mua được gắn đúng hồ sơ người dùng: - [ ] [Kiểm tra logic nhận diện trong code ứng dụng của bạn có khớp với trường hợp sử dụng không](ios-quickstart-identify) - [ ] [Đảm bảo bạn hiểu logic cha/kế thừa khi chia sẻ quyền truy cập trả phí giữa các hồ sơ người dùng](sharing-paid-access-between-user-accounts) - [ ] **Ưu đãi**: Nếu ứng dụng có ưu đãi cho App Store, hãy đảm bảo bạn đã [thêm In-app purchase key](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers) vào cả trường chính lẫn phần **App Store promotional offers**. - [ ] **Thu thập dữ liệu**: Đảm bảo tuân thủ quyền riêng tư: - [ ] Nếu bạn cần tuân thủ các quy định về quyền riêng tư như GDPR hoặc CCPA, hoặc ứng dụng dành cho trẻ em, hãy kiểm soát việc [bật IDFA và thu thập/chia sẻ IP](sdk-installation-ios#data-policies). - [ ] Nếu ứng dụng sử dụng AppTrackingTransparency, hãy đảm bảo bạn đang [gửi trạng thái ủy quyền cho Adapty](ios-deal-with-att). - [ ] **Nhãn quyền riêng tư**: [Tìm hiểu thêm](apple-app-privacy) về dữ liệu Adapty thu thập và các flag bạn cần thiết lập để kiểm duyệt. ## Kiểm tra giao dịch mua \{#purchase-validations\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Trước khi ra mắt, hãy đảm bảo rằng các giao dịch mua trong ứng dụng hoạt động chính xác và paywall của bạn sẵn sàng để kiểm duyệt cửa hàng. Cách kiểm tra in-app purchase phụ thuộc vào cách bạn triển khai: - Bạn hiển thị paywall được tạo trong Adapty Paywall Builder - Bạn đã tự triển khai paywall và sử dụng phương thức `makePurchase` bên trong để xử lý giao dịch - Bạn sử dụng Adapty ở chế độ observer (với Adapty Paywall Builder hoặc paywall tùy chỉnh của bạn) <Tabs groupId="paywall" queryString> <TabItem value="builder" label="Adapty Paywall Builder" default> **Mục tiêu**: Adapty render paywall, người dùng có thể mua sản phẩm, quyền truy cập được mở khóa và flow khôi phục hoạt động đúng. - [ ] Ứng dụng của bạn [hiển thị paywall](ios-present-paywalls) từ đúng placement bạn sẽ phát hành. - [ ] Paywall hiển thị trên màn hình. Nếu tải mất quá nhiều thời gian (ví dụ: khi bạn hoặc người dùng gặp kết nối internet không ổn định), hãy cân nhắc [điều chỉnh fetch policy](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). - [ ] Paywall hiển thị đúng biến thể (đối tượng/ngôn ngữ nếu có). Bạn có thể [thay đổi mức độ ưu tiên đối tượng](change-audience-priority) nếu cần. - [ ] Sản phẩm và giá hiển thị trên paywall. Lưu ý rằng API của Apple đôi khi có thể cung cấp giá không chính xác trong quá trình kiểm tra (đặc biệt với các cấu hình khu vực khác nhau), vì vậy hãy ưu tiên kiểm tra chức năng flow mua hàng hơn là độ chính xác của giá vì Adapty không ảnh hưởng đến giá của cửa hàng. - [ ] Giao dịch mua sandbox hoàn tất thành công. Callback mua hàng thành công được nhận. - [ ] Quyền truy cập được mở khóa và duy trì. Xác nhận rằng [quyền truy cập trả phí được cấp dựa trên hồ sơ người dùng Adapty hiện tại](ios-check-subscription-status#connect-profile-with-paywall-logic). - [ ] Sau khi mua, hồ sơ người dùng Adapty có mức độ truy cập đang hoạt động. - [ ] Các tính năng trả phí được mở khóa khi hồ sơ người dùng chứa mức độ truy cập đó (không chỉ dựa vào callback mua hàng). - [ ] Khôi phục giao dịch hoạt động. Khi bạn cài đặt lại ứng dụng hoặc cài đặt trên thiết bị mới, tính năng khôi phục giao dịch tự động hoạt động theo cài đặt [Chia sẻ quyền truy cập trả phí](sharing-paid-access-between-user-accounts). Nếu bạn không có xác thực backend, giao dịch sẽ được khôi phục tự động bất kể cài đặt. Trong các trường hợp khác, hãy đảm bảo người dùng có thể khôi phục giao dịch sau khi cài đặt lại ứng dụng. - [ ] Yêu cầu kiểm duyệt cửa hàng: - [ ] Nút **Restore purchases** có trên paywall. Bạn có thể thêm nút này trong paywall builder, và nó sẽ tự động xử lý việc khôi phục giao dịch khi được nhấn. - [ ] Điều khoản sử dụng + Chính sách bảo mật có thể truy cập từ màn hình paywall, và nhấp vào các liên kết này sẽ mở chúng trong trình duyệt. </TabItem> <TabItem value="makepurchase" label="Custom paywall (makePurchase)" default> **Mục tiêu**: Bạn render giao diện; Adapty xử lý giao dịch, cập nhật hồ sơ người dùng và khôi phục. - [ ] ID sản phẩm không được hardcode trong code ứng dụng. Bạn chỉ hardcode ID [placement](placements). - [ ] Ứng dụng của bạn [lấy sản phẩm](fetch-paywalls-and-products) từ đúng placement bạn sẽ phát hành. - [ ] Danh sách sản phẩm tải thành công. Nếu tải mất quá nhiều thời gian (ví dụ: khi bạn hoặc người dùng gặp kết nối internet không ổn định), hãy cân nhắc [điều chỉnh fetch policy](fetch-paywalls-and-products#fetch-paywall-information). - [ ] Các sản phẩm được lấy về khớp với biến thể mong đợi (đối tượng/ngôn ngữ nếu có). Bạn có thể [thay đổi mức độ ưu tiên đối tượng](change-audience-priority) nếu cần. - [ ] Sản phẩm và giá hiển thị trên paywall. Lưu ý rằng API của Apple đôi khi có thể cung cấp giá không chính xác trong quá trình kiểm tra (đặc biệt với các cấu hình khu vực khác nhau), vì vậy hãy ưu tiên kiểm tra chức năng flow mua hàng hơn là độ chính xác của giá vì Adapty không ảnh hưởng đến giá của cửa hàng. - [ ] Giao dịch mua sandbox với [makePurchase](making-purchases) hoàn tất thành công: - [ ] Kết quả mua hàng thành công được xử lý. - [ ] Các kết quả đang chờ/thất bại/đã hủy được xử lý một cách hợp lý. - [ ] Nếu bạn [sử dụng Remote Config](present-remote-config-paywalls), các giá trị của nó được lấy đúng cho paywall của bạn. - [ ] Khi paywall được hiển thị, phương thức [`logShowFlow` (iOS SDK v4+) / `logShowPaywall`](present-remote-config-paywalls#track-paywall-view-events) được gọi. - [ ] Giao dịch mua sandbox hoàn tất thành công. Callback mua hàng thành công được nhận. - [ ] Quyền truy cập được mở khóa và duy trì. Xác nhận rằng [quyền truy cập trả phí được cấp dựa trên hồ sơ người dùng Adapty hiện tại](ios-check-subscription-status#connect-profile-with-paywall-logic). - [ ] Sau khi mua, hồ sơ người dùng Adapty có mức độ truy cập đang hoạt động. - [ ] Các tính năng trả phí được mở khóa khi hồ sơ người dùng chứa mức độ truy cập đó (không chỉ dựa vào callback mua hàng). - [ ] Khôi phục giao dịch hoạt động. Khi bạn cài đặt lại ứng dụng hoặc cài đặt trên thiết bị mới, tính năng khôi phục giao dịch tự động hoạt động theo cài đặt [Chia sẻ quyền truy cập trả phí](sharing-paid-access-between-user-accounts). Nếu bạn không có xác thực backend, giao dịch sẽ được khôi phục tự động bất kể cài đặt. Trong các trường hợp khác, hãy đảm bảo người dùng có thể khôi phục giao dịch sau khi cài đặt lại ứng dụng. - [ ] Yêu cầu kiểm duyệt cửa hàng: - [ ] Nút **Restore purchases** có thể truy cập và [xử lý việc khôi phục](restore-purchase). - [ ] Điều khoản sử dụng + Chính sách bảo mật có thể truy cập từ màn hình paywall, và nhấp vào các liên kết này sẽ mở chúng trong trình duyệt. </TabItem> <TabItem value="observer" label="Observer mode"> **Mục tiêu**: Bạn tự xử lý giao dịch, cập nhật hồ sơ người dùng và khôi phục; Adapty nhận báo cáo giao dịch. - [ ] **Ứng dụng của bạn hoàn tất giao dịch bằng flow mua hàng của riêng mình** (StoreKit / BillingClient / backend): - [ ] Giao dịch mua sandbox thành công trong giao diện cửa hàng. - [ ] Các kết quả đang chờ/thất bại/đã hủy được xử lý hợp lý trong ứng dụng. - [ ] **Giao dịch được báo cáo cho Adapty**. - [ ] Chế độ observer được [bật trong code ứng dụng](implement-observer-mode). - [ ] Giao dịch mua hiển thị trong Event Feed của Adapty. - [ ] Gia hạn, hủy và hoàn tiền được phản ánh theo thời gian (khi áp dụng). - [ ] **Lượt xem paywall được theo dõi**. Phương thức [`logShowFlow` (iOS SDK v4+) / `logShowPaywall`](present-remote-config-paywalls#track-paywall-view-events) được gọi khi paywall được hiển thị. - [ ] **Khôi phục giao dịch hoạt động cho triển khai của bạn**. Cài đặt lại ứng dụng hoặc chuyển sang thiết bị khác sẽ khôi phục quyền truy cập đúng cách. - [ ] **Yêu cầu kiểm duyệt cửa hàng**: - [ ] Hành động **Restore purchases** có thể truy cập và kích hoạt flow khôi phục của bạn. - [ ] Điều khoản sử dụng + Chính sách bảo mật có thể truy cập từ màn hình paywall hoặc mua hàng và mở trong trình duyệt. </TabItem> </Tabs> Nếu bạn có bất kỳ câu hỏi nào về việc tích hợp Adapty SDK, hãy sử dụng chatbot AI ở góc dưới bên phải hoặc liên hệ với chúng tôi tại [support@adapty.io](mailto:support@adapty.io). --- # File: observer-vs-full-mode --- --- title: "Chế độ Observer" description: "So sánh Chế độ Observer và Chế độ Full trong Adapty để quản lý gói đăng ký." --- Adapty là một nền tảng in-app purchase mạnh mẽ và linh hoạt, được thiết kế để tăng doanh thu và mở rộng tệp người dùng đăng ký của bạn. Với các tính năng như paywall tùy chỉnh theo từng phân khúc người dùng, A/B test cho giá, thời hạn, thời gian dùng thử và các yếu tố hiển thị, cùng với bộ công cụ phân tích toàn diện về monetization và tích hợp bên thứ ba, Adapty hỗ trợ chiến lược tăng trưởng của bạn. Tuy nhiên, nếu bạn đã có hạ tầng mua hàng riêng và chưa sẵn sàng chuyển sang hệ thống của Adapty, bạn có thể thử Chế độ Observer của Adapty. Chế độ giới hạn này không sử dụng paywall của Adapty, không nhắm mục tiêu paywall theo đối tượng người dùng, không quản lý gói đăng ký (bao gồm gia hạn và thử lại thanh toán), mà chỉ tập trung vào phân tích. Dù có những hạn chế này, Chế độ Observer vẫn cung cấp khả năng phân tích mạnh mẽ, bao gồm tích hợp hệ thống attribution, phân tích nâng cao, messaging và hồ sơ người dùng CRM. Cả hai chế độ có cùng mức giá và đều yêu cầu cập nhật ứng dụng di động, nên lựa chọn thực chất là: chuyển hoàn toàn sang hạ tầng của Adapty để có đầy đủ tính năng, hoặc giữ hạ tầng hiện tại trong khi chỉ bổ sung tích hợp bên thứ ba và khả năng phân tích. | Tính năng | Chế độ Observer | Chế độ Full | |-------------|-------------|---------| | **Phân tích toàn diện** | ✅ | ✅ | | **Tích hợp bên thứ ba** | ✅ | ✅ | | **Phản hồi sự kiện mua hàng để cấp/hạn chế quyền truy cập nội dung trả phí** | ❌ | ✅ | | **Bên quản lý hạ tầng mua hàng** | Bạn | Adapty | | **A/B Testing** | <p>Khả thi, nhưng cần viết thêm nhiều code và cấu hình hơn so với Chế độ Full.</p> | ✅ | | **Thời gian triển khai** | <p>Cho analytics và tích hợp: Dưới một giờ</p><p>Kèm A/B test: Đến một tuần khi kiểm thử kỹ lưỡng</p> | Vài giờ | ## Chế độ Observer hoạt động như thế nào \{#how-observer-mode-works\} Ở Chế độ Observer, bạn báo cáo các giao dịch mới từ Apple/Google lên Adapty SDK, và Adapty SDK sẽ chuyển tiếp chúng đến backend của Adapty. Bạn có trách nhiệm quản lý quyền truy cập nội dung trả phí trong ứng dụng, hoàn tất giao dịch, xử lý gia hạn, giải quyết các vấn đề thanh toán, v.v. ## Cách thiết lập Chế độ Observer \{#how-to-set-up-observer-mode\} 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android) và [với App Store](initial_ios). 2. 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 thiết lập cho [iOS](sdk-installation-ios#activate-adapty-module-of-adapty-sdk), [Android](sdk-installation-android#activate-adapty-module-of-adapty-sdk), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform#activate-adapty-sdk), và [Unity](sdk-installation-unity#activate-adapty-module-of-adapty-sdk). 3. [Báo cáo giao dịch](report-transactions-observer-mode) từ hạ tầng mua hàng hiện có của bạn lên Adapty cho iOS và các framework đa nền tảng dựa trên iOS. 4. (Tùy chọn) Nếu bạn muốn sử dụng tích hợp bên thứ ba, hãy thiết lập theo hướng dẫn trong phần [Cấu hình tích hợp bên thứ ba](configuration). :::warning Khi hoạt động ở Chế độ Observer, Adapty SDK không hoàn tất giao dịch, vì vậy hãy đảm bảo bạn tự xử lý phần này. ::: ## Cách sử dụng paywall và A/B test trong Chế độ Observer \{#how-to-use-paywalls-and-ab-tests-in-observer-mode\} Ở Chế độ Observer, 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 thực hiện chúng trong hạ tầng của riêng mình. Do đó, nếu bạn muốn sử dụng paywall và/hoặc A/B test trong Chế độ Observer, bạn cần liên kết giao dịch đến từ app store với paywall tương ứng trong code ứng dụng di động khi báo cáo giao dịch. Ngoài ra, các paywall được thiết kế bằng Paywall Builder cần được hiển thị theo cách đặc biệt khi sử dụng Chế độ Observer: - Hiển thị paywall trong Chế độ Observer cho [iOS](implement-observer-mode) hoặc [Android](android-present-paywall-builder-paywalls-in-observer-mode). - [Liên kết paywall với giao dịch mua hàng](report-transactions-observer-mode) khi báo cáo giao dịch trong Chế độ Observer. --- # File: migration-from-revenuecat --- --- title: "Migration từ RevenueCat" description: "Migrate từ RevenueCat sang Adapty với hướng dẫn từng bước của chúng tôi." --- Kế hoạch migration của bạn gồm 5 bước logic và mất trung bình 2 giờ. 90% tất cả các migration hoàn thành trong chưa đầy một ngày làm việc. 1. Tìm hiểu các điểm khác biệt cốt lõi; tạo và chuẩn bị tài khoản Adapty _(5 phút)_; 2. Cài đặt Adapty SDK cho nền tảng của bạn ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform), [Unity](sdk-installation-unity)) thay thế RevenueCat SDK _(1 giờ)_; 3. Thiết lập [Apple App Store server notifications](enable-app-store-server-notifications) cho Adapty và (tùy chọn) [chuyển tiếp raw events](enable-app-store-server-notifications#raw-events-forwarding) _(5 phút)_; 4. Kiểm tra và phát hành bản cập nhật ứng dụng _(30 phút);_ 5. (Tùy chọn) Yêu cầu bộ phận hỗ trợ RevenueCat cung cấp dữ liệu lịch sử dưới dạng CSV _(5 phút);_ 6. (Tùy chọn) Import dữ liệu lịch sử qua bộ phận hỗ trợ Adapty _(30 phút)_. :::info Người dùng của bạn sẽ được migrate tự động Tất cả người dùng đã từng kích hoạt gói đăng ký sẽ được chuyển sang Adapty ngay khi họ mở phiên bản mới của ứng dụng có tích hợp Adapty SDK. Trạng thái xác thực gói đăng ký và quyền truy cập premium sẽ được khôi phục tự động. ::: Trước khi phát hành phiên bản mới của ứng dụng có tích hợp Adapty SDK, hãy kiểm tra [danh sách kiểm tra trước khi phát hành](release-checklist) của chúng tôi. ## Tìm hiểu các điểm khác biệt cốt lõi; tạo và chuẩn bị tài khoản Adapty \{#learn-the-core-differences-create-and-prepare-an-adapty-account\} Adapty và RevenueCat SDK được thiết kế tương tự nhau. Sự khác biệt lớn nhất nằm ở mức sử dụng mạng và tốc độ: Adapty SDK được thiết kế để cung cấp thông tin theo yêu cầu nhanh nhất có thể khi bạn cần. Ví dụ, khi yêu cầu một paywall, bạn nhận được [Remote Config](customize-paywall-with-remote-config) trước để xây dựng sẵn onboarding hoặc paywall, sau đó mới gọi lấy sản phẩm trong một request riêng. Tên gọi có một số điểm khác nhau: | RevenueCat | Adapty | | :---------- | :-------------- | | Package | Product | | Offering | Paywall | | Paywall | Paywall Builder | | Entitlement | Access level | Adapty có khái niệm [placement](placements) — một vị trí logic bên trong ứng dụng nơi người dùng có thể thực hiện mua hàng. Trong hầu hết các trường hợp, bạn có một hoặc hai placement: - Onboarding (vì 80% tất cả các giao dịch mua diễn ra ở đây); - General (hiển thị trong phần cài đặt hoặc bên trong ứng dụng sau onboarding). <img src="/assets/shared/img/2406d97-image.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '300px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Cài đặt Adapty SDK và thay thế RevenueCat SDK \{#install-adapty-sdk-and-replace-revenuecat-sdk\} Cài đặt Adapty SDK cho nền tảng của bạn ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform), [Unity](sdk-installation-unity)) vào ứng dụng. Bạn cần thay thế một số phương thức SDK ở phía ứng dụng. Hãy cùng xem các hàm phổ biến nhất và cách thay thế chúng bằng Adapty SDK. ### Khởi động SDK \{#sdk-activation\} Thay `Purchases.configure` bằng `Adapty.activate`. ### Lấy paywall (offerings) \{#getting-paywalls-offerings\} Thay `Purchases.shared.getOfferings` bằng [`Adapty.getPaywall`](fetch-paywalls-and-products#fetch-paywall-information). Trong Adapty, bạn luôn yêu cầu paywall thông qua [placement id](placements). Trên thực tế, bạn chỉ lấy tối đa 1-2 paywall, vì vậy chúng tôi thiết kế như vậy để tăng tốc SDK và giảm mức sử dụng mạng. ### Lấy thông tin người dùng (customer profile) \{#getting-a-user-customer-profile\} Thay `Purchases.shared.getCustomerInfo` bằng `Adapty.getProfile`. ### Lấy sản phẩm \{#getting-products\} Trong RevenueCat, bạn dùng cấu trúc sau: `Purchases.shared.getOfferings` rồi `self.offering?.availablePackages`. Trong Adapty, bạn trước tiên yêu cầu paywall (xem ở trên) để có ngay [Remote Config](customize-paywall-with-remote-config) của Adapty, sau đó gọi lấy sản phẩm bằng [`Adapty.getPaywallProducts`](fetch-paywalls-and-products#fetch-products). ### Thực hiện mua hàng \{#making-a-purchase\} Thay `Purchases.shared.purchase` bằng [`Adapty.makePurchase`](making-purchases#make-purchase). ### Kiểm tra mức độ truy cập (entitlement) \{#checking-access-level-entitlement\} Lấy hồ sơ người dùng (xem ở trên trước) rồi thay thế `customerInfo?.entitlements["premium"]?.isActive == true` bằng [`profile.accessLevels["premium"]?.isActive == true`](subscription-status#retrieving-the-access-level-from-the-server). ### Khôi phục mua hàng \{#restore-purchase\} Thay `Purchases.shared.restorePurchases` bằng [`Adapty.restorePurchases`](restore-purchase). ### Kiểm tra người dùng đã đăng nhập chưa \{#check-if-the-user-is-logged-in\} Thay `Purchases.shared.isAnonymous` bằng `if profile.customerUserId == nil`. ### Đăng nhập người dùng \{#log-in-user\} Thay `Purchases.shared.logIn` bằng [`Adapty.identify`](identifying-users#set-customer-user-id-after-configuration). ### Đăng xuất người dùng \{#log-out-user\} Thay `Purchases.shared.logOut` bằng [`Adapty.logout`](identifying-users#logging-out-and-logging-in). ## Chuyển App Store server-side notifications sang Adapty \{#switch-app-store-server-side-notifications-to-adapty\} Đọc hướng dẫn thực hiện [tại đây](migrate-to-adapty-from-another-solutions#changing-apple-server-notifications). ## Kiểm tra và phát hành phiên bản mới của ứng dụng \{#test-and-release-a-new-version-of-your-app\} Nếu bạn đang đọc phần này, bạn đã: - [x] Cấu hình Adapty Dashboard - [x] Cài đặt Adapty SDK - [x] Thay thế logic SDK bằng các hàm Adapty - [x] Chuyển App Store server-side notifications sang Adapty và tùy chọn bật chuyển tiếp raw events sang RevenueCat - [ ] Thực hiện mua hàng thử trong Sandbox - [ ] Phát hành phiên bản ứng dụng mới Nếu bạn đã hoàn thành các bước trên, hãy thực hiện mua hàng thử trong Sandbox rồi phát hành ứng dụng. :::info Xem qua [danh sách kiểm tra trước khi phát hành](release-checklist). Thực hiện kiểm tra cuối cùng bằng danh sách của chúng tôi để xác nhận tích hợp hiện tại hoặc thêm các tính năng bổ sung như tích hợp [attribution](attribution-integration) hay [analytics](analytics-integration). ::: ## (Tùy chọn) Xuất dữ liệu lịch sử RevenueCat dưới dạng CSV \{#optional-export-your-revenuecat-historical-data-in-csv-format\} :::warning Đừng vội import dữ liệu lịch sử Bạn nên chờ ít nhất một tuần sau khi phát hành với SDK trước khi import dữ liệu lịch sử. Trong thời gian đó, chúng tôi sẽ thu thập đầy đủ thông tin về giá giao dịch từ SDK, giúp dữ liệu bạn import chính xác hơn. ::: Xuất dữ liệu lịch sử từ RevenueCat dưới dạng CSV theo hướng dẫn trong [tài liệu chính thức của RevenueCat](https://www.revenuecat.com/docs/integrations/scheduled-data-exports). ## (Tùy chọn) Yêu cầu bộ phận hỗ trợ RevenueCat cung cấp Google Purchase Tokens \{#optional-ask-revenuecat-support-for-google-purchase-tokens\} Nếu bạn cần import giao dịch Google Play, hãy liên hệ bộ phận hỗ trợ RevenueCat để lấy file CSV chứa Google Purchase Tokens qua [trang hỗ trợ](https://app.revenuecat.com/settings/support) của họ. Google Purchase Token là mã định danh duy nhất do Google Play cung cấp cho mỗi giao dịch, cần thiết để theo dõi và xác minh giao dịch chính xác trong Adapty. Thông tin này không có trong file xuất tiêu chuẩn. File chứa ba cột sau: - `user_id` - `google_purchase_token` - `google_product_id` ## Liên hệ chúng tôi để import dữ liệu lịch sử \{#write-us-to-import-your-historical-data\} Liên hệ với chúng tôi qua tin nhắn trên website hoặc gửi email đến [support@adapty.io](mailto:support@adapty.io) kèm theo file CSV của bạn. 1. Gửi trực tiếp file CSV bạn đã xuất từ RevenueCat cho đội ngũ hỗ trợ của chúng tôi. 2. Nếu import giao dịch Google Play, hãy đính kèm file CSV chứa Google Purchase Tokens mà bạn nhận được từ bộ phận hỗ trợ RevenueCat. 3. Cho chúng tôi biết ID người dùng nào sẽ được dùng làm Customer User ID (định danh người dùng chính của Adapty): `rc_original_app_user_id` hay `rc_last_seen_app_user_id_alias`. Đội ngũ hỗ trợ của chúng tôi sẽ import các giao dịch của bạn vào Adapty. Dữ liệu sau sẽ được import vào Adapty cho mỗi giao dịch: | Tham số | Mô tả | | ----------------------------- | ------------------------------------------------------------ | | user_id | Customer User ID, định danh chính của người dùng trong Adapty và hệ thống của bạn. | | apple_original_transaction_id | Đối với chuỗi gói đăng ký, đây là ngày mua của giao dịch gốc, được liên kết qua `store_original_transaction_id`. | | google_product_id | ID sản phẩm trên Google Play Store. | | google_purchase_token | Mã định danh duy nhất do Google Play cung cấp cho mỗi giao dịch, cần thiết để xác thực. | | country | Quốc gia của người dùng. | | created_at | Ngày và giờ tạo người dùng. | | subscription_expiration_date | Ngày và giờ gói đăng ký hết hạn. | | email | Email của người dùng cuối. | | phone_number | Số điện thoại của người dùng cuối. | | idfa | Identifier for Advertisers (IDFA), được Apple gán cho thiết bị của người dùng. | | idfv | Identifier for Vendors (IDFV), mã được gán cho tất cả ứng dụng của một nhà phát triển và dùng chung trên các ứng dụng đó trong cùng một thiết bị. | | advertising_id | Mã định danh duy nhất do hệ điều hành Android cung cấp, các nhà quảng cáo có thể dùng để theo dõi. | | attribution_channel | Tên kênh marketing. | | attribution_campaign | Tên chiến dịch marketing. | | attribution_ad_group | Nhóm quảng cáo attribution. | | attribution_ad_set | Bộ quảng cáo attribution. | | attribution_creative | Từ khóa creative của attribution. | Ngoài ra, các định danh tích hợp cho các tích hợp sau cũng sẽ được import: Amplitude, Mixpanel, AppsFlyer, Adjust và FacebookAds. ## Câu hỏi thường gặp \{#faq\} ### Tôi đã cài đặt thành công Adapty SDK và phát hành phiên bản ứng dụng mới. Điều gì sẽ xảy ra với những người dùng cũ chưa cập nhật lên phiên bản có Adapty SDK? \{#i-successfully-installed-adapty-sdk-and-released-a-new-app-version-with-it-what-will-happen-to-my-legacy-subscribers-who-did-not-update-to-a-version-with-adapty-sdk\} Hầu hết người dùng sạc điện thoại qua đêm — đó là lúc App Store thường tự động cập nhật tất cả ứng dụng, vì vậy đây không phải vấn đề đáng lo ngại. Vẫn có thể có một số ít người dùng trả phí chưa cập nhật, nhưng họ vẫn có quyền truy cập vào nội dung premium. Bạn không cần lo lắng và không cần ép họ cập nhật. ### Tôi có cần xuất dữ liệu lịch sử từ RevenueCat càng sớm càng tốt không, hay tôi sẽ mất dữ liệu đó? \{#do-i-need-to-export-my-historical-data-from-revenuecat-as-quickly-as-possible-or-will-i-lose-it\} Bạn không cần phải vội; hãy phát hành bản cập nhật với Adapty SDK trước, sau đó mới cung cấp dữ liệu lịch sử cho chúng tôi. Chúng tôi sẽ khôi phục lịch sử thanh toán của người dùng và điền vào [hồ sơ người dùng](profiles-crm) và [biểu đồ](charts). ### Tôi đang dùng MMP (AppsFlyer, Adjust, v.v.) và analytics (Mixpanel, Amplitude, v.v.). Làm sao để đảm bảo mọi thứ hoạt động bình thường? \{#i-use-mmp-appsflyer-adjust-etc-and-analytics-mixpanel-amplitude-etc-how-do-i-make-sure-that-everything-will-work\} Trước tiên bạn cần truyền cho chúng tôi các ID của các dịch vụ bên thứ ba mà bạn muốn chúng tôi gửi dữ liệu đến, thông qua SDK của chúng tôi. Đọc hướng dẫn về [tích hợp attribution](attribution-integration) và [tích hợp analytics](analytics-integration). Đối với dữ liệu lịch sử và người dùng cũ, **hãy đảm bảo bạn truyền cho chúng tôi các ID đó từ dữ liệu bạn đã xuất từ RevenueCat.** --- # File: migration-from-superwall --- --- title: "Migration from Superwall" description: "Migrate từ Superwall sang Adapty với hướng dẫn từng bước ánh xạ mọi lệnh gọi SDK và khái niệm." --- Hầu hết các migration từ Superwall sang Adapty mất khoảng hai tiếng. Bạn thay thế SDK, trỏ thông báo server của cửa hàng về Adapty, và phát hành bản app mới. Người dùng đã mua gói đăng ký vẫn giữ quyền truy cập — Adapty khôi phục từ receipt của App Store và Google Play ngay lần khởi chạy đầu tiên. :::info Người dùng đăng ký sẽ được migrate tự động Tất cả người dùng đã từng kích hoạt gói đăng ký sẽ chuyển sang Adapty ngay khi họ mở phiên bản mới của app có tích hợp Adapty SDK. Trạng thái xác thực gói đăng ký và quyền truy cập premium được khôi phục tự động. ::: ## Cách tổ chức hướng dẫn này \{#how-this-guide-is-organized\} Migration gồm sáu bước: 1. [Ánh xạ các khái niệm Superwall sang Adapty](#map-your-superwall-concepts-to-adapty) 2. [Cài đặt Adapty SDK](#install-the-adapty-sdk) 3. [Thay thế các lệnh gọi SDK](#replace-sdk-calls) 4. [Chuyển thông báo server App Store và Google Play](#switch-app-store-and-google-play-server-notifications) 5. [Kiểm tra và phát hành](#test-and-release) 6. [(Tùy chọn) Import dữ liệu lịch sử](#optional-import-historical-data) ## Ánh xạ các khái niệm Superwall sang Adapty \{#map-your-superwall-concepts-to-adapty\} Hầu hết các khái niệm trong Superwall đều có khái niệm tương đương trong Adapty: | Superwall | Adapty | Thay đổi gì | | :------------------- | :------------------------------------------------ | :--------------------------------------------------------------------------- | | Campaign | [Placement](placements) + [Audience](audience) | Logic Campaign được tách thành placement (vị trí) và audience (quy tắc). | | Placement | [Placement](placements) | Cùng khái niệm, cùng tên. | | Audience filter | [Audience](audience) | Các bộ quy tắc nằm bên trong một placement. | | Entitlement | [Access level](access-level) | Tên định danh (ví dụ: `premium`). | | WebView paywall | [Paywall Builder paywall](adapty-paywall-builder) | Được Adapty SDK render theo native thay vì dùng `WKWebView`. | | `PurchaseController` | Built-in | Không cần implement protocol — Adapty xử lý mua hàng. | | Feature gating | Kiểm tra [mức độ truy cập](access-level) | Kiểm tra `profile.accessLevels["premium"]?.isActive`. | Có hai điểm thay đổi tư duy đáng lưu ý trước khi bạn động đến code: - **Fetch và present là hai bước riêng biệt**: `register` của Superwall gộp việc fetch paywall, đánh giá campaign, và hiển thị UI vào một lệnh gọi duy nhất. Adapty tách hai bước này — bạn fetch paywall, lấy cấu hình của nó, rồi mới present. Cách này thêm vài dòng code nhưng cho phép bạn pre-load cấu hình, hiển thị trạng thái loading tùy chỉnh, hoặc hủy việc hiển thị theo logic riêng. - **Trạng thái gói đăng ký theo từng mức độ truy cập**: Superwall cung cấp một `subscriptionStatus` dạng published property duy nhất. Adapty trả về một [`AdaptyProfile`](https://swift.adapty.io/documentation/adapty/adaptyprofile) với các mức độ truy cập có tên, nên một người dùng có thể sở hữu cả mức độ truy cập `sports` và `science` độc lập với nhau. Để đọc đồng bộ, hãy cache profile từ `AdaptyDelegate` thay vì gọi `getProfile()` ở mỗi lần load view. ## Cài đặt Adapty SDK \{#install-the-adapty-sdk\} Cài đặt Adapty SDK cho nền tảng của bạn — [iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform), [Unity](sdk-installation-unity), hoặc [Capacitor](sdk-installation-capacitor) — và đồng thời gỡ SuperwallKit khỏi dự án. ## Thay thế các lệnh gọi SDK \{#replace-sdk-calls\} Đi qua từng phần trong tích hợp của bạn và thay thế lệnh gọi Superwall bằng lệnh tương đương của Adapty. Các liên kết ở cuối mỗi mục bao gồm cả bảy nền tảng SDK — hãy chọn đúng nền tảng của app bạn. ### Khởi tạo SDK \{#initialize-the-sdk\} Thay `Superwall.configure` bằng `Adapty.activate`. Xem hướng dẫn cài đặt cho nền tảng của bạn — [iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform), [Unity](sdk-installation-unity), hoặc [Capacitor](sdk-installation-capacitor). ### Xác định và đăng xuất người dùng \{#identify-and-log-out-users\} Thay `Superwall.shared.identify` bằng `Adapty.identify` và `Superwall.shared.reset` bằng `Adapty.logout`. Cả hai SDK đều tạo hồ sơ người dùng ẩn danh ngay lần khởi chạy đầu tiên, vì vậy các lệnh gọi này chỉ cần thiết khi người dùng đăng nhập hoặc đăng xuất. Hãy fetch lại paywall sau khi xác định người dùng — người dùng mới có thể thuộc về một đối tượng khác. Xem hướng dẫn xác định người dùng cho nền tảng của bạn — [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), [Kotlin Multiplatform](kmp-identifying-users), [Unity](unity-identifying-users), hoặc [Capacitor](capacitor-identifying-users). ### Fetch và hiển thị paywall \{#fetch-and-present-a-paywall\} Thay `Superwall.shared.register` bằng flow hai bước: fetch paywall với `Adapty.getPaywall`, load cấu hình view của nó với `AdaptyUI.getPaywallConfiguration`, rồi mới present. Hai điểm khác biệt cần lưu ý: - **Feature gating thay thế closure `feature:`**: Sau khi paywall đóng, kiểm tra mức độ truy cập đang hoạt động trên profile được trả về (hoặc trên `Adapty.getProfile`) rồi phân nhánh từ đó. - **Paywall được render bởi SDK**: Superwall render paywall bên trong `WKWebView`. Adapty render Paywall Builder paywall theo native — font chữ, thông tin sản phẩm, và các nút được SDK vẽ trực tiếp. Xem hướng dẫn nhanh về paywall cho nền tảng của bạn — [iOS](ios-quickstart-paywalls), [Android](android-quickstart-paywalls), [React Native](react-native-quickstart-paywalls), [Flutter](flutter-quickstart-paywalls), [Kotlin Multiplatform](kmp-quickstart-paywalls), [Unity](unity-quickstart-paywalls), hoặc [Capacitor](capacitor-quickstart-paywalls). ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Thay `Superwall.shared.subscriptionStatus` bằng cách kiểm tra mức độ truy cập có tên trên profile: `profile.accessLevels["premium"]?.isActive`. Theo dõi thay đổi qua `AdaptyDelegate.didLoadLatestProfile(_:)` thay vì dùng pattern `@Published` property, và cache profile ở phía bạn để đọc đồng bộ. Xem hướng dẫn kiểm tra trạng thái gói đăng ký cho nền tảng của bạn — [iOS](ios-check-subscription-status), [Android](android-check-subscription-status), [React Native](react-native-check-subscription-status), [Flutter](flutter-check-subscription-status), [Kotlin Multiplatform](kmp-check-subscription-status), [Unity](unity-check-subscription-status), hoặc [Capacitor](capacitor-check-subscription-status). ### Xử lý mua hàng và khôi phục \{#handle-purchases-and-restores\} Với Paywall Builder, cả hai SDK đều tự động xử lý giao dịch mua bên trong giao diện paywall — **bạn có thể bỏ qua bước này**. Với paywall tùy chỉnh, Superwall yêu cầu implement `PurchaseController`. Adapty thì không: thay `PurchaseController.purchase` bằng `Adapty.makePurchase` và `PurchaseController.restorePurchases` bằng `Adapty.restorePurchases`. SDK tự xử lý việc xác thực. Xem hướng dẫn nhanh về custom paywall cho nền tảng của bạn — [iOS](ios-quickstart-manual), [Android](android-quickstart-manual), [React Native](react-native-quickstart-manual), [Flutter](flutter-quickstart-manual), [Kotlin Multiplatform](kmp-quickstart-manual), [Unity](unity-quickstart-manual), hoặc [Capacitor](capacitor-quickstart-manual). ### Thiết lập thuộc tính người dùng \{#set-user-attributes\} Thay `Superwall.shared.setUserAttributes` bằng `Adapty.updateProfile`. Xem hướng dẫn thuộc tính người dùng cho nền tảng của bạn — [iOS](setting-user-attributes), [Android](android-setting-user-attributes), [React Native](react-native-setting-user-attributes), [Flutter](flutter-setting-user-attributes), [Kotlin Multiplatform](kmp-setting-user-attributes), [Unity](unity-setting-user-attributes), hoặc [Capacitor](capacitor-setting-user-attributes). ## Chuyển thông báo server App Store và Google Play \{#switch-app-store-and-google-play-server-notifications\} Trỏ thông báo server của cửa hàng về Adapty. Adapty vẫn hoạt động mà không cần chúng, nhưng analytics, các tích hợp bên thứ ba, và chỉ số A/B test đều phụ thuộc vào đây: - **App Store**: Làm theo [Bật thông báo server App Store](enable-app-store-server-notifications). - **Google Play**: Làm theo [Bật thông báo thời gian thực cho nhà phát triển](enable-real-time-developer-notifications-rtdn). Nếu bạn muốn chạy song song Superwall và Adapty trong quá trình triển khai, hãy dùng [raw events forwarding](enable-app-store-server-notifications#raw-events-forwarding) — Adapty sẽ proxy các sự kiện từ cửa hàng về lại Superwall trong khi bạn xác minh tích hợp mới. ## Kiểm tra và phát hành \{#test-and-release\} Trước khi phát hành, hãy kiểm tra từng mục: - [x] Đã cấu hình Adapty Dashboard (sản phẩm, paywall, placement, mức độ truy cập) - [x] Đã cài đặt Adapty SDK - [x] Đã thay thế các lệnh gọi Superwall SDK bằng các lệnh tương đương của Adapty - [x] Đã trỏ thông báo server App Store và Google Play về Adapty - [ ] Đã thực hiện giao dịch mua trong sandbox - [ ] Đã nộp bản phát hành app mới Xem qua [danh sách kiểm tra trước khi phát hành](release-checklist) để xác thực lần cuối. ## (Tùy chọn) Import dữ liệu lịch sử \{#optional-import-historical-data\} Superwall không sở hữu trạng thái gói đăng ký của bạn — App Store và Google Play mới là nơi lưu trữ. Adapty xác thực receipt ngay lần khởi chạy đầu tiên, nên người dùng đã mua vẫn giữ quyền truy cập mà không cần import gì. Nếu bạn muốn backfill các giao dịch lịch sử vào Adapty analytics, hãy làm theo [Import dữ liệu lịch sử vào Adapty](importing-historical-data-to-adapty). Đợi ít nhất một tuần sau khi phát hành SDK để SDK có thời gian thu thập giá mua hàng mới. ## Câu hỏi thường gặp \{#faq\} ### Điều gì xảy ra với người dùng đã mua gói đăng ký nhưng chưa cập nhật app? \{#what-happens-to-subscribers-who-dont-update-the-app\} Hầu hết người dùng tự động cập nhật app qua đêm, nên tỷ lệ người dùng còn dùng phiên bản cũ giảm nhanh. Người dùng đăng ký trên phiên bản cũ vẫn giữ quyền truy cập trực tiếp qua App Store hoặc Google Play — bạn không cần ép buộc cập nhật. ### Audience của Superwall campaign có chuyển sang được không? \{#do-my-superwall-campaign-audiences-carry-over\} Không. Bộ lọc audience của Superwall và đối tượng trong Adapty được cấu hình ở hai dashboard khác nhau và dùng các định danh khác nhau. Hãy tạo lại targeting của bạn dưới dạng [đối tượng](audience) bên trong [placement](placements) Adapty. Hầu hết các app chỉ có một hoặc hai placement (onboarding và một trigger trong app), nên việc tái tạo thường khá nhanh. ### Adapty có tương đương với `getPresentationResult` không? \{#does-adapty-have-an-equivalent-to-getpresentationresult\} Không có lệnh gọi đơn lẻ nào tương đương. Để kiểm tra xem một placement có hiển thị paywall không, hãy gọi `Adapty.getPaywall(placementId:)` và phân nhánh dựa trên kết quả. Nếu lệnh gọi thành công, một paywall đã được gán cho đối tượng của người dùng đó. Nếu thất bại vì không có paywall nào được cấu hình, hãy bỏ qua việc hiển thị và chạy logic dự phòng của bạn. --- # File: importing-historical-data-to-adapty --- --- title: "Nhập dữ liệu lịch sử vào Adapty" description: "Nhập dữ liệu lịch sử vào Adapty để có phân tích chi tiết." --- Sau khi cài đặt SDK Adapty và phát hành ứng dụng, bạn có thể xem người dùng và người đăng ký trong phần [Profiles](profiles-crm). Nhưng nếu bạn có hạ tầng cũ và cần migrate sang Adapty, hoặc chỉ đơn giản muốn xem dữ liệu hiện có trong Adapty thì sao? :::note Nhập dữ liệu không bắt buộc Adapty sẽ tự động cấp mức độ truy cập cho người dùng cũ và khôi phục các sự kiện mua hàng của họ khi họ mở ứng dụng đã tích hợp SDK Adapty. Với trường hợp này, việc nhập dữ liệu lịch sử là không cần thiết. Tuy nhiên, nhập dữ liệu sẽ đảm bảo phân tích chính xác nếu bạn có số lượng lớn giao dịch lịch sử, dù nhìn chung thì không bắt buộc cho migration. ::: Để nhập dữ liệu vào Adapty: 1. Xuất các giao dịch của bạn ra file CSV (cần cung cấp file riêng cho iOS, Android và Stripe). Vui lòng tham khảo [phần định dạng file nhập](importing-historical-data-to-adapty#import-file-format) bên dưới để biết yêu cầu chi tiết. 2. Nếu file nào vượt quá 1 GB, hãy chuẩn bị một mẫu dữ liệu khoảng 100 dòng. 3. Tải tất cả các file lên Google Drive (bạn có thể nén lại, nhưng giữ chúng tách biệt). 4. Đối với giao dịch iOS, hãy đảm bảo phần **In-app purchase API** trong [**App settings**](https://app.adapty.io/settings/ios-sdk) đã được điền đầy đủ **Issuer ID**, **Key ID** và **Private key** (file .P8) dù bạn sử dụng StoreKit 1. Xem hướng dẫn chi tiết tại phần [Provide Issuer ID and Key ID](app-store-connection-configuration#step-2-provide-issuer-id-and-key-id) và [Upload In-App Purchase Key file](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file). 5. Chia sẻ các đường dẫn với đội ngũ của chúng tôi qua [email](mailto:support@adapty.io) hoặc qua chat trực tuyến trong Adapty Dashboard. Đừng lo, việc nhập dữ liệu lịch sử sẽ không tạo ra bản trùng lặp, ngay cả khi dữ liệu đó trùng với các mục đã có trong Adapty. ## Giới hạn đã biết đối với Android \{#known-limitations-for-android\} 1. Chỉ các gói đăng ký đang hoạt động mới được khôi phục; các giao dịch đã hết hạn sẽ không được khôi phục. 2. Chỉ lần gia hạn gần nhất trong một gói đăng ký mới được khôi phục; toàn bộ chuỗi mua hàng sẽ không được khôi phục. 3. Nếu giá sản phẩm đã thay đổi kể từ lần mua, giá hiện tại sẽ được sử dụng, điều này có thể dẫn đến giá không chính xác. :::note Nếu bạn có lượng lớn giao dịch Android, bạn có thể cần [yêu cầu tăng hạn mức Google Play Developer API](google-play-quota-increase) trước khi bắt đầu nhập để tránh vượt quá giới hạn API mặc định. ::: ## Định dạng file nhập \{#import-file-format\} :::tip Nếu bạn đang migrate từ RevenueCat, bạn có thể gửi thẳng file xuất từ RevenueCat — không cần chuyển đổi. Xem [tài liệu của RevenueCat](https://www.revenuecat.com/docs/integrations/scheduled-data-exports) để biết hướng dẫn xuất file. ::: Hãy chuẩn bị dữ liệu trong một hoặc nhiều file đáp ứng các yêu cầu sau: - [ ] Định dạng file là .CSV. - [ ] File riêng cho Android, iOS và Stripe. - [ ] Mỗi file nhập chứa tất cả [các cột bắt buộc](importing-historical-data-to-adapty#required-fields). - [ ] Các cột trong file nhập có tiêu đề. - [ ] Tiêu đề cột phải khớp chính xác với cột **Column name** trong bảng bên dưới. Vui lòng kiểm tra lỗi chính tả. - [ ] Các cột không bắt buộc có thể vắng mặt trong file. Đừng thêm cột trống cho dữ liệu bạn không có. - [ ] File nhập không được có cột thừa không được đề cập trong bảng. Nếu có, hãy xóa chúng đi. - [ ] Các giá trị được phân tách bằng dấu phẩy. - [ ] Các giá trị không được đặt trong dấu ngoặc kép. - [ ] Nếu có nhiều **apple_original_transaction_id** cho một người dùng, hãy thêm tất cả chúng thành các dòng riêng biệt cho mỗi **apple_original_transaction_id**. Nếu không, chúng tôi có thể không khôi phục được các giao dịch mua consumable. Vui lòng sử dụng các file sau làm mẫu cho [iOS](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/adapty_import_ios_sample.csv) và [Android](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/adapty_import_android_sample.csv). ### Các cột có thể dùng trong file nhập \{#available-import-file-columns\} | Column name | Presence | Mô tả | |-----------|--------|-----------| | **user_id** | bắt buộc | ID người dùng của bạn | | **apple_original_transaction_id** | bắt buộc cho iOS | <p>ID giao dịch gốc hoặc OTID ([tìm hiểu thêm](https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid)), được dùng trong cơ chế nhập StoreKit 2. Vì một người dùng có thể có nhiều OTID, chỉ cần cung cấp ít nhất một OTID để nhập thành công.</p><p></p><p>**Lưu ý:** Chúng tôi yêu cầu thông tin xác thực In-app purchase API cho việc nhập này phải được thiết lập trong Adapty Dashboard của bạn. Tìm hiểu cách thực hiện [tại đây](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file).</p> | | **google_product_id** | bắt buộc cho Google | ID sản phẩm trong Google Play Store. | | **google_purchase_token** | bắt buộc cho Google | Mã định danh duy nhất đại diện cho người dùng và ID sản phẩm của in-app purchase mà họ đã mua | | **google_is_subscription** | bắt buộc cho Google | Các giá trị có thể là `1` \| `0` | | **stripe_token** | bắt buộc cho Stripe | Token của đối tượng Stripe đại diện cho một giao dịch mua duy nhất. Có thể là token của Stripe Subscription (`sub_...`) hoặc Payment Intent (`pi_...`). | | **subscription_expiration_date** | tùy chọn | Ngày hết hạn gói đăng ký, tức là ngày tính phí tiếp theo, ngày và giờ có múi giờ (2020-12-31T23:59:59-06:00) | | **created_at** | tùy chọn | Ngày và giờ tạo hồ sơ người dùng (2019-12-31 23:59:59-06:00) | | **birthday** | tùy chọn | Ngày sinh của người dùng theo định dạng 2000-12-31 | | **email** | tùy chọn | Email của người dùng | | **gender** | tùy chọn | Giới tính của người dùng | | **phone_number** | tùy chọn | Số điện thoại của người dùng | | **country** | tùy chọn | định dạng [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) | | **first_name** | tùy chọn | Tên của người dùng | | **last_name** | tùy chọn | Họ của người dùng | | **last_seen** | tùy chọn | Ngày và giờ có múi giờ (2020-12-31T23:59:59-06:00) | | **idfa** | tùy chọn | Mã định danh cho nhà quảng cáo (IDFA) là mã định danh thiết bị ngẫu nhiên do Apple gán cho thiết bị của người dùng. Chỉ áp dụng cho ứng dụng iOS | | **idfv** | tùy chọn | Mã định danh cho nhà cung cấp (IDFV) là mã duy nhất được gán cho tất cả ứng dụng do một nhà phát triển tạo ra, trong trường hợp này là ứng dụng của bạn. Chỉ áp dụng cho ứng dụng iOS | | **advertising_id** | tùy chọn | Advertising ID là mã duy nhất do Hệ điều hành Android gán mà nhà quảng cáo có thể dùng để định danh thiết bị của người dùng | | **amplitude_user_id** | tùy chọn | ID người dùng từ Amplitude | | **amplitude_device_id** | tùy chọn | ID thiết bị từ Amplitude | | **mixpanel_user_id** | tùy chọn | ID người dùng từ Mixpanel | | **appmetrica_profile_id** | tùy chọn | ID hồ sơ người dùng từ AppMetrica | | **appmetrica_device_id** | tùy chọn | ID thiết bị từ AppMetrica | | **appsflyer_id** | tùy chọn | Mã định danh duy nhất từ AppsFlyer | | **adjust_device_id** | tùy chọn | ID thiết bị từ Adjust | | **facebook_anonymous_id** | tùy chọn | Mã định danh duy nhất do Facebook tạo ra cho người dùng tương tác với ứng dụng hoặc trang web của bạn theo cách ẩn danh, tức là họ không đăng nhập vào Facebook | | **branch_id** | tùy chọn | Mã định danh duy nhất từ Branch | | **attribution_source** | tùy chọn | Nguồn tích hợp attribution, ví dụ: appsflyer | | **attribution_status** | tùy chọn | organic | | **attribution_channel** | tùy chọn | Kênh attribution đã mang lại giao dịch | | **attribution_campaign** | tùy chọn | Chiến dịch attribution đã mang lại giao dịch | | **attribution_ad_group** | tùy chọn | Nhóm quảng cáo attribution đã mang lại giao dịch | | **attribution_ad_set** | tùy chọn | Bộ quảng cáo attribution đã mang lại giao dịch | | **attribution_creative** | tùy chọn | Các yếu tố hình ảnh hoặc văn bản cụ thể được sử dụng trong quảng cáo hoặc chiến dịch marketing, được theo dõi để xác định hiệu quả trong việc thúc đẩy các hành động mong muốn như click, chuyển đổi hoặc cài đặt | | **custom_attributes** | tùy chọn | Định nghĩa tối đa 30 thuộc tính tùy chỉnh dưới dạng từ điển JSON theo định dạng key-value: <ul><li>**key**: (string) Tên của thuộc tính tùy chỉnh</li><li> **value**: (string, integer, float hoặc boolean) Giá trị của thuộc tính tùy chỉnh.</li></ul><p> Định dạng: `"{'string_value': 'some_value', 'float_value': 123.0, 'int_value': 456}"`.</p><p>Lưu ý cách dùng dấu nháy đơn và nháy kép trong định dạng. Lưu ý rằng các giá trị boolean và integer sẽ được chuyển thành float.</p> | ### Các trường bắt buộc \{#required-fields\} Có 2 nhóm trường bắt buộc cho mỗi nền tảng: **user_id** và dữ liệu xác định giao dịch mua hàng cụ thể của nền tảng tương ứng. Tham khảo bảng bên dưới để biết các trường bắt buộc theo từng nền tảng. | Nền tảng | Các trường bắt buộc | |--------|---------------| | iOS | <p>user_id</p><p>apple_original_transaction_id</p> | | Android | <p>user_id</p><p>google_product_id</p><p>google_purchase_token</p><p>google_is_subscription</p> | | Stripe | <p>user_id</p><p>stripe_token</p> | Nếu thiếu các trường này, Adapty sẽ không thể lấy được giao dịch. Để có phân tích cohort chính xác, hãy chỉ định `created_at`. Nếu không cung cấp, chúng tôi sẽ lấy ngày cài đặt bằng với ngày mua hàng đầu tiên. ### Nhập dữ liệu vào Adapty \{#import-data-to-adapty\} Vui lòng liên hệ với chúng tôi và chia sẻ file nhập qua [support@adapty.io](mailto:support@adapty.io) hoặc qua chat trực tuyến trong [Adapty Dashboard](https://app.adapty.io/overview). --- # File: migrate-integrations-to-adapty --- --- title: "Migrate tích hợp sang Adapty" description: "Chuyển đổi các tích hợp analytics và attribution từ giải pháp cũ sang Adapty mà không bị trùng lặp sự kiện hay gián đoạn chiến dịch." --- Migrate sang Adapty không chỉ đơn giản là đổi SDK. Các tích hợp analytics và attribution của bên thứ ba — như Amplitude và Adjust — cũng cần được chuyển đổi đồng bộ. Nếu thực hiện cẩn thận, quá trình chuyển đổi sẽ có rất ít sự kiện bị trùng hoặc bị thiếu và không làm gián đoạn chiến dịch của bạn. ## Ánh xạ sự kiện \{#map-your-events\} Tên sự kiện có thể tùy chỉnh trong hầu hết các tích hợp Adapty. Bạn có thể cấu hình chúng để khớp với tên đã dùng trong dashboard và chiến dịch hiện tại. Báo cáo analytics và chiến dịch của bạn sẽ tiếp tục hoạt động với cùng tên sự kiện sau khi chuyển đổi. Để xem danh sách đầy đủ các sự kiện trong Adapty, hãy xem [Sự kiện](events). Với Adjust, tích hợp sử dụng event ID thay vì tên sự kiện tùy chỉnh. Hãy chuyển các event ID hiện có từ Adjust dashboard sang cấu hình tích hợp Adapty. Xem [hướng dẫn tích hợp Adjust](adjust) để biết thêm chi tiết. ## Cách Adapty tạo sự kiện tích hợp \{#how-adapty-creates-integration-events\} Để gửi sự kiện đến một tích hợp, Adapty cần có hồ sơ người dùng. Hồ sơ được tạo theo một trong hai cách: - **Import lịch sử**: Hồ sơ được tạo khi bạn [import dữ liệu giao dịch lịch sử](importing-historical-data-to-adapty) trước khi SDK hoạt động. - **Tương tác qua SDK**: Hồ sơ được tạo tự động khi người dùng mở ứng dụng có tích hợp Adapty SDK lần đầu tiên. Adapty nhận biết các giao dịch mua được thực hiện trong hệ thống cũ theo thời gian thực. Nhưng nó chỉ có thể gửi sự kiện tích hợp khi hồ sơ của người mua đã tồn tại. Hồ sơ được tạo khi người dùng mở ứng dụng có Adapty SDK. Những người dùng không cập nhật lên phiên bản mới sẽ không tạo ra sự kiện tích hợp. ## Chuẩn bị trước ngày migration \{#prepare-before-migration-day\} ### Loại trừ sự kiện lịch sử \{#exclude-historical-events\} Bật **Exclude Historical Events** trong [cài đặt tích hợp](configuration) của bạn. Điều này ngăn các sự kiện xảy ra trước phiên Adapty SDK đầu tiên của người dùng không bị gửi đến tích hợp. Cài đặt này đặc biệt quan trọng trong quá trình [import lịch sử](importing-historical-data-to-adapty), khi Adapty xử lý một lượng lớn giao dịch quá khứ cùng một lúc. Nếu không có nó, các giao dịch đó sẽ tạo ra một lượng lớn sự kiện trong công cụ analytics của bạn. ### Thiết lập tích hợp trước \{#set-up-the-integration-in-advance\} Adapty cho phép bạn cấu hình và kiểm tra tích hợp trong khi vẫn giữ nó ở trạng thái tắt. Bạn có thể thiết lập thông tin xác thực, ánh xạ sự kiện và bộ lọc mà không cần kích hoạt tích hợp cho đến khi sẵn sàng. Cài đặt được lưu lại khi bạn bật, nên không mất gì khi giữ nó tắt đến ngày migration. Để tìm tích hợp của bạn, xem [Tích hợp attribution](attribution-integration), [Tích hợp analytics](analytics-integration), [Tích hợp dịch vụ nhắn tin](messaging), hoặc [Tích hợp Webhook và ETL](webhook-and-etl). ## Chuyển đổi vào ngày migration \{#switch-on-migration-day\} Tắt tích hợp trong giải pháp cũ và bật nó trong Adapty cùng một lúc. Chạy song song cả hai sẽ tạo ra sự kiện trùng lặp. Tạm dừng các chiến dịch mua lớn vào ngày migration. Điều này giảm thiểu rủi ro lỗi tối ưu hóa chiến dịch do các sự kiện trong khoảng thời gian chuyển giao. ## Điều cần lưu ý \{#what-to-expect\} Một số sự kiện tích hợp bị thiếu hoặc trùng lặp trong quá trình migration là không thể tránh khỏi. Khi việc chuyển đổi được thực hiện đúng cách, số lượng sự kiện bị ảnh hưởng là không đáng kể. Nguyên nhân chính gây ra khoảng trống là thời điểm đã đề cập ở trên: Adapty chỉ có thể gửi sự kiện tích hợp cho một giao dịch mua sau khi hồ sơ người dùng tồn tại. Các giao dịch thực hiện trong hệ thống cũ sẽ không tạo ra sự kiện tích hợp Adapty cho đến khi người mua mở ứng dụng có Adapty SDK. ## Tích hợp vs. thông báo server-to-server \{#integrations-vs-server-to-server-notifications\} Adapty khuyến nghị sử dụng tích hợp thay vì chuyển tiếp thông báo server-to-server thô từ cửa hàng trực tiếp đến công cụ analytics hoặc attribution của bạn. Với tích hợp: - **Định dạng thống nhất**: Sự kiện từ tất cả các cửa hàng — App Store, Google Play, Stripe — sử dụng cùng một định dạng sự kiện. - **Dữ liệu phong phú hơn**: Sự kiện bao gồm dữ liệu mà Adapty thu thập, chẳng hạn như trạng thái gói đăng ký và thuộc tính người dùng. Thông báo thô không bao gồm điều này. --- # File: whats-new --- --- title: "Có gì mới" description: "Cập nhật các tính năng và cải tiến mới nhất trong Adapty" --- Khám phá các tính năng mới nhất, cải tiến, cập nhật SDK và nâng cấp tài liệu giúp bạn tối ưu hóa chiến lược kiếm tiền từ ứng dụng. Trang này tổng hợp những bản phát hành quan trọng nhất mỗi tháng. :::note Có phản hồi về các tính năng mới? Chúng tôi rất muốn nghe ý kiến từ bạn! Liên hệ với chúng tôi qua [Bảng phản hồi sản phẩm](https://adapty.featurebase.app/en?b=69831ba5e82e7a3391632ec2). ::: ## Tháng 7 năm 2026 \{#july-2026\} - **Tiền tệ ảo**: Định nghĩa các loại tiền tệ trong ứng dụng như token, coin hoặc gem, cấp và theo dõi số dư cho từng người dùng, đồng thời đọc các số dư đó từ máy chủ của bạn thông qua server-side API. [Tìm hiểu thêm](virtual-currencies) - **AI Agent trong Apple Ads Manager**: Hỏi một chat agent tư vấn về hiệu suất Apple Ads của bạn và nhận câu trả lời được rút ra từ dữ liệu chiến dịch của bạn, mà không cần tự tay xây dựng báo cáo. [Tìm hiểu thêm](ads-manager-ai-agent) - **Tự động hóa mới trong Apple Ads Manager**: Tự động thay đổi ở cấp độ chiến dịch và nhóm quảng cáo với hai loại quy tắc mới, bên cạnh các tự động hóa từ khóa và cụm từ tìm kiếm hiện có. [Quy tắc chiến dịch](ads-manager-automations-campaign-rules) | [Quy tắc nhóm quảng cáo](ads-manager-automations-ad-group-rules) - **Hồ sơ trong Adapty Mail**: Chế độ xem theo từng người đăng ký, hiển thị hành trình của mỗi người dùng, trạng thái gói đăng ký hiện tại và trạng thái hủy đăng ký trong một nơi duy nhất. [Tìm hiểu thêm](mail-profiles) - **SDK v4 dành cho React Native, Flutter, Capacitor, và Kotlin Multiplatform**: Các SDK v4 hỗ trợ Flows đã ra mắt. React Native, Flutter, và Capacitor đã đạt mức phát hành chính thức (general availability), và Kotlin Multiplatform v4 cũng được ra mắt — mỗi nền tảng đều có hướng dẫn migration riêng. [React Native](migration-to-react-native-sdk-v4) | [Flutter](migration-to-flutter-sdk-v4) | [Capacitor](migration-to-capacitor-sdk-v4) | [Kotlin Multiplatform](migration-to-kmp-sdk-v4) - **Trường webhook mới**: Webhook payload giờ đây bao gồm giá gốc và mức giảm giá cho từng giao dịch, giúp bạn theo dõi ưu đãi và ưu đãi giới thiệu ở các hệ thống downstream. Các trường này chỉ có trong webhook. [Tìm hiểu thêm](webhook-event-types-and-fields) - **Giá gạch ngang trong flow**: Hiển thị giá gốc bị gạch ngang bên cạnh giá ưu đãi, kèm huy hiệu giảm giá, trực tiếp trong Flow Builder. [Tìm hiểu thêm](strikethrough-price) - **Thư viện mẫu flow**: Bắt đầu một flow mới từ mẫu thiết kế chuyên nghiệp thay vì canvas trống, rồi tùy chỉnh theo ứng dụng của bạn. [Tìm hiểu thêm](paywall-builder-templates) - **Nút Install tools**: Mỗi bài viết tài liệu hiện có nút **Install tools** ở phần header. Nút này mở một modal với các lệnh sẵn sàng sao chép để cài đặt kỹ năng tích hợp Adapty SDK trong Claude Code, Copilot CLI, Gemini CLI, Codex và các trợ lý lập trình AI khác. [Tìm hiểu thêm](adapty-sdk-integration-skill) - **Phương thức cài đặt Unity SDK mới**: Giờ đây bạn có thể cài đặt Unity SDK thông qua Swift Package Manager, kèm theo hướng dẫn khắc phục sự cố cho các vấn đề thiết lập thường gặp. [Tìm hiểu thêm](sdk-installation-unity) - **Footer container trong Flow Builder**: Một thanh cố định ở cuối màn hình luôn hiển thị trong khi phần còn lại của màn hình cuộn — phù hợp cho nút CTA, văn bản pháp lý và các liên kết. [Tìm hiểu thêm](builder-containers#footer) - **Video hướng dẫn Flow Builder mới**: Danh sách phát YouTube với các video hướng dẫn từng bước xây dựng flow, hiện đã được nhúng vào các hướng dẫn Flow Builder. [Tìm hiểu thêm](adapty-flow-builder) ## Tháng 6 năm 2026 \{#june-2026\} - **Flows nay đã hỗ trợ Android**: Trình tạo no-code trực quan cho paywall và onboarding giờ đây hoạt động trên Android SDK v4 trở lên, cùng với iOS. Màn hình hiển thị native, không dùng web view. [Tìm hiểu thêm](adapty-flow-builder) - **CPP A/B Test trong Apple Ads Manager**: So sánh các trang sản phẩm tùy chỉnh với nhau trong Apple Ads. Chọn từ 2 đến 4 trang — bao gồm cả trang mặc định hiện tại — và Apple Ads sẽ phân phối lưu lượng giữa các trang đó rồi báo cáo trang nào có tỷ lệ chuyển đổi tốt nhất. [Tìm hiểu thêm](ads-manager-cpp-ab-tests) - **Adapty Mail API**: Gửi hồ sơ người dùng và giao dịch tới Adapty Mail trực tiếp từ máy chủ của bạn, không cần định tuyến dữ liệu qua Adapty SDK. Dùng để khởi tạo danh sách người đăng ký, tái sử dụng người đăng ký từ các ứng dụng khác của bạn, hoặc giữ backend làm nguồn dữ liệu chính. [Tìm hiểu thêm](mail-send-data-via-api) - **Hiển thị paywall nhắm mục tiêu Apple Ads ngay lần khởi chạy đầu tiên**: Attribution của Apple Ads đến sau khi SDK kích hoạt, nên nếu yêu cầu paywall quá sớm sẽ bỏ lỡ đối tượng Apple Ads của bạn. Dùng `AdaptyProfile.appliedAttributionSources` để hiển thị paywall nhắm mục tiêu Apple Ads ngay khi dữ liệu attribution về. [iOS](ios-show-aa-targeted-paywall) | [React Native](react-native-show-aa-targeted-paywall) | [Capacitor](capacitor-show-aa-targeted-paywall) - **Tự động lưu trong Flow Builder**: Flow Builder giờ tự động lưu tiến trình của bạn mỗi phút một lần, nên bạn sẽ không mất công chỉnh sửa chưa lưu khi rời khỏi trang. Bạn vẫn có thể lưu nháp thủ công bằng **Cmd/Ctrl + S**. [Tìm hiểu thêm](builder-save-publish) - **Video hướng dẫn Flow Builder mới**: Hai video hướng dẫn mới bao gồm cách xây dựng điều hướng giữa các màn hình flow và thiết kế trạng thái phần tử như đã chọn, đang hoạt động và bị vô hiệu hóa. [Điều hướng trong flow](onboarding-navigation-branching) | [Trạng thái phần tử](builder-element-states) - **Tài liệu bằng tiếng Nhật và tiếng Việt**: Tài liệu Adapty hiện đã có bằng tiếng Nhật (日本語) và tiếng Việt (Tiếng Việt). Chuyển đổi ngôn ngữ bằng bộ chọn ngôn ngữ ở thanh điều hướng phía trên. ## Tháng 5 năm 2026 \{#may-2026\} - **Flows (Beta)**: Tạo toàn bộ chuỗi màn hình trong một trình xây dựng no-code trực quan — paywall một màn hình, onboarding nhiều bước, và mọi thứ ở giữa, tất cả trong một flow. Các màn hình hiển thị natively mà không cần web view, và bạn có thể cập nhật nội dung, thiết kế, và logic mà không cần phát hành bản cập nhật ứng dụng. Hiện hỗ trợ iOS, Android, React Native, Flutter, và Capacitor SDK v4 trở lên. [Tìm hiểu thêm](adapty-flow-builder) - **Autopilot giờ đây tự điều chỉnh theo kết quả test của bạn**: Hoạt động như một AI growth manager, nó cập nhật kế hoạch tăng trưởng sau mỗi vòng hoàn thành. Hypothesis tiếp theo được xây dựng dựa trên những thí nghiệm bạn đã chạy, thí nghiệm nào thắng, và hướng nào còn đáng khám phá — thay vì tuân theo một trình tự cố định. [Tìm hiểu thêm](autopilot-how-it-works#how-ai-growth-advisor-decides-what-to-recommend) - **Activation ARPU trong Autopilot Market Insights**: Một biểu đồ mới so sánh doanh thu trung bình trên mỗi lượt cài đặt mới của ứng dụng với mức trung bình của danh mục. Kết hợp với phễu chuyển đổi — tỷ lệ chuyển đổi cao kết hợp với Activation ARPU thấp có thể cho thấy sản phẩm đang được định giá quá thấp. [Tìm hiểu thêm](autopilot-analysis#activation-arpu) - **Analytics trong Adapty Mail**: So sánh chỉ số phân phối và doanh thu được ghi nhận từ email cho từng chiến dịch trong một giao diện. Nhóm, phân tích chi tiết và lọc theo chiến dịch, phân khúc, biến thể A/B test, nội dung tin nhắn hoặc trigger, rồi xem chi tiết từng hàng. [Tìm hiểu thêm](mail-analytics) - **Hồ sơ thương hiệu trong Adapty Mail**: Hồ sơ tổng hợp giúp định hướng nội dung email, giọng điệu, hình ảnh và nội dung paywall trên web. Adapty xây dựng hồ sơ này từ trang danh sách ứng dụng trên cửa hàng, trang đích, trang pháp lý và các hồ sơ mạng xã hội của bạn — bạn có thể xem lại hoặc chỉnh sửa từng phần ngay trực tiếp. [Tìm hiểu thêm](mail-brand) - **Dự đoán trong Adapty UA**: Dự đoán doanh thu, ROAS, lợi nhuận quảng cáo, ARPU và ARPPU cho từng cohort, giúp bạn so sánh các chiến dịch trước khi chúng đủ thời gian để đánh giá. Dự đoán được xây dựng từ dữ liệu cohort lịch sử của chính ứng dụng, cập nhật hàng ngày, và có sẵn cho các khoảng thời gian cohort từ D0 đến D360 hoặc một ngày tùy chỉnh. [Tìm hiểu thêm](ua-predicted-metrics) - **Các trường mới trong Adapty UA custom S3 export**: Custom S3 export hiện bao gồm `bundle_id`, `device_brand`, `device_model`, `os_version`, `app_version` và `sdk_version`. Phân tách và kết hợp dữ liệu attribution theo thiết bị và phiên bản ứng dụng ở phía downstream. [Tìm hiểu thêm](ua-custom-s3) - **Đối tượng placement trong CLI**: Các lệnh `adapty placements create` và `adapty placements update` hiện hỗ trợ flag `--audiences` — một mảng JSON gồm các mục `{segment_ids, paywall_id, priority}` — cho phép bạn nhắm paywall khác nhau đến các phân khúc khác nhau ngay từ terminal. Lệnh `adapty paywalls placements` mới liệt kê tất cả các placement đang sử dụng một paywall nhất định, giúp bạn xem trước tác động trước khi thay đổi. [Tìm hiểu thêm](developer-cli-reference#placements) - **Tài liệu bằng tiếng Tây Ban Nha**: Tài liệu Adapty hiện đã có phiên bản tiếng Tây Ban Nha (Español). Chuyển ngôn ngữ bằng bộ chọn ngôn ngữ ở thanh điều hướng phía trên. ## Tháng 4 năm 2026 \{#april-2026\} - **Adapty Mail**: Chiến dịch email được tạo bởi AI, giúp chuyển đổi người dùng dùng thử thành người đăng ký trả phí. Xây dựng, gửi và attribution chiến dịch ngay trong dự án Adapty của bạn — không cần nền tảng email riêng. [Tìm hiểu thêm](adapty-mail) - **Chẩn đoán Paywall trong Autopilot**: Biết cần sửa gì trên paywall trước khi tạo test. Tải lên ảnh chụp màn hình và Autopilot sẽ trả về các đề xuất dựa trên benchmark từ các ứng dụng hoạt động tốt nhất trong danh mục của bạn, cùng với gợi ý bố cục và nội dung do AI tạo ra. Các đề xuất có benchmark sẽ trở thành các vòng A/B test trong kế hoạch tăng trưởng của bạn. [Tìm hiểu thêm](autopilot-analysis#paywall-analysis) - **Hướng dẫn rõ ràng hơn cho từng gợi ý của Autopilot**: Mỗi giả thuyết giờ đây đều giải thích rõ lý do tại sao nó quan trọng (giải thích dựa trên dữ liệu về cách paywall của bạn lệch khỏi các mẫu đã được xác lập), cần thay đổi gì và cách thiết lập A/B test, cũng như các chỉ số cần theo dõi trong phần mới "Cách diễn giải kết quả của bạn". [Tìm hiểu thêm](autopilot-execute-plan#step-1-view-the-hypothesis) - **Giữ cho kế hoạch tăng trưởng Autopilot luôn cập nhật**: Làm mới phân tích để lấy dữ liệu thị trường mới nhất và các đề xuất mới, đồng thời xem lại các đề xuất trước đó trong lịch sử phiên bản nếu các đề xuất mới chưa phù hợp. Các giả thuyết được nhóm theo các tab Top priority, All, Pricing, Visual, Geo-pricing và Archived. [Tìm hiểu thêm](autopilot-growth-plan) - **Phân bổ doanh thu theo thời hạn trong Autopilot**: Xem liệu doanh thu của bạn có đang tập trung quá mức vào một thời hạn gói đăng ký hay không. Một biểu đồ Market Insights mới hiển thị tỷ lệ doanh thu theo thời hạn của bạn cùng với mức trung bình ngành cho danh mục và quốc gia của bạn. [Tìm hiểu thêm](autopilot-analysis#revenue-distribution-by-duration) - **Cập nhật dự đoán LTV và doanh thu**: Dự đoán LTV và doanh thu giờ đây sử dụng dữ liệu giữ chân cohort của chính ứng dụng bạn khi có đủ lịch sử, và dùng mức trung bình trên nhiều ứng dụng trong trường hợp còn lại — vì vậy ngay cả các ứng dụng mới cũng có được dự đoán khả dụng trong analytics và A/B test. [Tìm hiểu thêm](predicted-ltv-and-revenue) - **Gửi tất cả sự kiện trong Adapty UA**: Cung cấp cho Meta và TikTok bức tranh đầy đủ hơn về các lượt chuyển đổi để tối ưu hóa mô hình đối tượng. Adapty hiện hỗ trợ chuyển tiếp lượt cài đặt và giao dịch từ người dùng organic và không có attribution đến pixel của bạn, không chỉ những người dùng được khớp với một chiến dịch. [Meta](ua-facebook#send-all-events) | [TikTok](ua-tiktok#send-all-events) - **Tài liệu bằng tiếng Nga và tiếng Thổ Nhĩ Kỳ**: Tài liệu Adapty hiện đã có sẵn bằng tiếng Nga (Русский) và tiếng Thổ Nhĩ Kỳ (Türkçe). Chuyển đổi ngôn ngữ bằng bộ chọn ngôn ngữ trên thanh điều hướng phía trên. ## Tháng 3 năm 2026 \{#march-2026\} - **Developer CLI**: Quản lý tài khoản Adapty của bạn ngay từ terminal mà không cần mở Dashboard. CLI cho phép bạn tạo ứng dụng, định nghĩa mức độ truy cập, thiết lập sản phẩm, tạo paywall và cấu hình placement — tất cả đều có thể viết script cho các môi trường tự động hóa. Ngoài ra còn có [Adapty CLI skill](https://github.com/adaptyteam/adapty-cli/tree/main/skills/adapty-cli) để hỗ trợ các công cụ AI coding làm việc với CLI. [Tìm hiểu thêm](developer-cli) - **Trang Overview trong Apple Ads Manager**: Xem tất cả các chỉ số quan trọng của Apple Ads tại một nơi, mỗi chỉ số kèm theo biểu đồ xu hướng. Lọc theo ứng dụng bằng dropdown ở header, tùy chỉnh các chỉ số hiển thị, và điều chỉnh loại biểu đồ cũng như cách hiển thị doanh thu. [Tìm hiểu thêm](ads-manager-overview) - **Market Intelligence trong Apple Ads Manager**: Xem các từ khóa mà đối thủ của bạn đang chạy quảng cáo trên hơn 50 quốc gia, và thêm trực tiếp các từ khóa hiệu quả nhất vào chiến dịch của bạn. [Tìm hiểu thêm](ads-manager-market-intelligence) - **Tự động hóa keyword toàn chu trình trong Apple Ads Manager**: Tự động điều chỉnh giá thầu, tạm dừng hoặc kích hoạt keyword, và di chuyển chúng giữa các nhóm quảng cáo dựa trên các quy tắc hiệu suất bạn định nghĩa. [Tìm hiểu thêm](ads-manager-automations-keyword-rules) - **Lịch sử giá thầu trong Apple Ads Manager**: Xem toàn bộ lịch sử thay đổi cho giá thầu CPT của bất kỳ keyword nào — thời điểm mỗi thay đổi xảy ra, giá trị trước và sau, và quy tắc tự động hóa nào đã kích hoạt nó. [Tìm hiểu thêm](ads-manager-manage-keywords#bid-history) - **Các vòng thiết kế trong Autopilot**: Các gợi ý thiết kế paywall giờ đây là các vòng chính thức trong kế hoạch tăng trưởng của bạn — được liệt kê trong thanh bên cùng với các vòng monetization. Mỗi vòng thiết kế bao gồm bản phác thảo thiết kế, mô tả về thời điểm phù hợp để áp dụng mẫu đó, và các chỉ số quan trọng mà nó hướng đến. [Tìm hiểu thêm](autopilot-growth-plan#view-the-growth-plan) - **Thêm giả thuyết của riêng bạn vào Autopilot**: Mở rộng kế hoạch tăng trưởng với các vòng tùy chỉnh. Thêm tiêu đề, mô tả, loại vòng (kiếm tiền hoặc giao diện), chỉ số mục tiêu và — đối với các vòng kiếm tiền — các sản phẩm liên quan. [Tìm hiểu thêm](autopilot-growth-plan#add-your-own-hypothesis) - **Sắp xếp lại các vòng Autopilot**: Kéo và sắp xếp lại các giai đoạn trong kế hoạch tăng trưởng để chạy các thử nghiệm theo thứ tự phù hợp nhất với chiến lược của bạn. [Tìm hiểu thêm](autopilot) - **Định giá theo địa lý trong Autopilot**: Thử nghiệm thay đổi giá theo từng quốc gia như một loại round mới trong kế hoạch tăng trưởng của bạn. Dựa trên dữ liệu Market Insights, Autopilot đề xuất nên tăng, giảm hay giữ nguyên giá ở từng quốc gia. Thêm một đề xuất dưới dạng geo-pricing round để chạy nó như một A/B test — có thể chạy đồng thời tối đa 5 round. [Tìm hiểu thêm](autopilot-growth-plan#geo-pricing-hypotheses) - **Tự động hóa cụm từ tìm kiếm trong Apple Ads Manager**: Tự động đưa các cụm từ tìm kiếm hiệu quả lên thành từ khóa khớp chính xác và loại trừ chúng tại nguồn — không cần tải xuống báo cáo thủ công. Có thể tạo quy tắc từ các mẫu có sẵn hoặc xây dựng từ đầu với các điều kiện và lịch trình tùy chỉnh. [Tìm hiểu thêm](ads-manager-automations-search-terms) - **Maximize Conversions bidding trong Apple Ads Manager**: Khi tạo chiến dịch, bạn có thể chọn Maximize Conversions làm chiến lược đặt giá thầu. Thuật toán của Apple tối đa hóa lượt tải xuống trong ngân sách của bạn, dựa trên CPA mục tiêu tùy chọn. [Tìm hiểu thêm](ads-manager-create-campaign) - **Tích hợp FunnelFox trong Adapty UA**: Tích hợp mới với FunnelFox hiện đã có trong Adapty UA. [FunnelFox](ua-funnelfox) - **Tài liệu bằng tiếng Trung**: Tài liệu Adapty hiện đã có bằng tiếng Trung (中文). Chuyển đổi ngôn ngữ bằng bộ chọn ngôn ngữ trên thanh điều hướng trên cùng. ## Tháng 2 năm 2026 \{#february-2026\} - **Giá sản phẩm theo từng quốc gia**: Đặt mức giá khác nhau cho từng quốc gia trực tiếp trên Adapty Dashboard — Adapty tự động đồng bộ thay đổi với App Store Connect và Google Play. Mọi cập nhật giá đều được ghi lại trong nhật ký kiểm tra, đảm bảo không có thay đổi nào bị bỏ sót. [Tìm hiểu thêm](edit-product) - **Giá của đối thủ cạnh tranh theo quốc gia trong Autopilot**: So sánh giá gói đăng ký của bạn với đối thủ cạnh tranh tại các thị trường trọng điểm. [Tìm hiểu thêm](autopilot-analysis#market-and-competitor-analysis) - **Kiểm soát phiên bản onboarding**: Theo dõi và quản lý các phiên bản onboarding với lịch sử phiên bản đầy đủ. Xem lại các thay đổi và khôi phục khi cần. - **Biểu đồ chuyển đổi paywall trong analytics**: Hai biểu đồ chuyển đổi mới — Paywall view → Trial và Paywall view → Paid — cho thấy paywall của bạn chuyển đổi người xem thành người đăng ký như thế nào. [Tìm hiểu thêm](analytics-conversion) - **Nhân bản phân khúc**: Sao chép một phân khúc hiện có cùng với toàn bộ bộ lọc thay vì xây dựng lại từ đầu. Hữu ích khi chạy nhiều chiến dịch hoặc A/B test với các đối tượng trùng lặp. [Tìm hiểu thêm](segments#duplicate-segments) - **Thông báo đẩy trong ứng dụng di động Adapty**: Cấu hình thông báo đẩy cho 14 loại sự kiện trực tiếp trong ứng dụng Adapty iOS để theo dõi hoạt động gói đăng ký mà không cần mở dashboard. [Tìm hiểu thêm](push-notifications) - **Kotlin Multiplatform SDK 3.15**: Thêm hỗ trợ onboarding, web paywall, và cải tiến API. [Tìm hiểu thêm](migration-to-kmp-315) - **Capacitor SDK 3.16**: Thêm hỗ trợ Capacitor 8. Các dự án đang dùng Capacitor 7 nên ở lại SDK v3.15. [Tìm hiểu thêm](migration-to-capacitor-316) - **Hướng dẫn tích hợp SDK có sự hỗ trợ của LLM**: Hướng dẫn từng bước tích hợp Adapty với sự hỗ trợ của các AI coding assistant. Mỗi hướng dẫn sẽ dẫn dắt LLM của bạn qua toàn bộ quá trình triển khai, từ thiết lập dashboard đến xử lý mua hàng. [iOS](adapty-cursor) | [Android](adapty-cursor-android) | [React Native](adapty-cursor-react-native) | [Flutter](adapty-cursor-flutter) | [Unity](adapty-cursor-unity) | [Kotlin Multiplatform](adapty-cursor-kmp) | [Capacitor](adapty-cursor-capacitor). Để có flow tự động chỉ với một lệnh, hãy thử **adapty-sdk-integration skill** mới (beta): [iOS](adapty-sdk-integration-skill) | [Android](adapty-sdk-integration-skill-android) | [React Native](adapty-sdk-integration-skill-react-native) | [Flutter](adapty-sdk-integration-skill-flutter) | [Unity](adapty-sdk-integration-skill-unity) | [Kotlin Multiplatform](adapty-sdk-integration-skill-kmp) | [Capacitor](adapty-sdk-integration-skill-capacitor) ## Tháng 1 năm 2026 \{#january-2026\} - **Capacitor SDK chính thức phát hành**: Capacitor SDK hiện đã sẵn sàng cho môi trường production sau quá trình kiểm thử toàn diện. Xây dựng ứng dụng gói đăng ký cho iOS và Android bằng Capacitor với hỗ trợ tích hợp Adapty đầy đủ. [Tìm hiểu thêm](capacitor-sdk-overview) - **Autopilot cho ứng dụng mới**: Phân tích Autopilot hiện khả dụng ngay cả khi ứng dụng của bạn chưa có lịch sử giao dịch phong phú. Nhận các đề xuất tối ưu hóa giá dựa trên dữ liệu và xây dựng kế hoạch tăng trưởng ngay từ ngày đầu. [Tìm hiểu thêm](autopilot) - **Cơ hội định giá toàn cầu trong Autopilot**: Xác định tiềm năng doanh thu trên các thị trường hoạt động tốt nhất của bạn với các đề xuất định giá theo từng quốc gia. Autopilot phân tích tỷ lệ chuyển đổi và sức mua cho 5 quốc gia hàng đầu tiếp theo của bạn, cung cấp thông tin dựa trên dữ liệu về việc nên tăng, giảm hay duy trì giá dựa trên Chỉ số Giá Adapty. [Tìm hiểu thêm](autopilot) - **Chỉ số chuyển đổi khôi phục thanh toán**: Các biểu đồ phân tích mới theo dõi doanh thu được khôi phục từ các vấn đề thanh toán và thời gian ân hạn. Theo dõi "Billing issue converted", "Billing issue converted revenue", "Grace period converted" và "Grace period converted revenue" để đo lường hiệu quả khôi phục giữ chân người dùng của bạn. - **Quản lý quảng cáo trực tiếp trong Apple Ads Manager**: Tạo và quản lý các chiến dịch Apple Ads của bạn ngay trong Adapty mà không cần chuyển đổi giữa các nền tảng. [Tìm hiểu thêm](ads-manager-manage-ads) - **Phân tích Apple Ads Manager**: Truy cập các chỉ số hiệu suất chi tiết ở cấp độ quảng cáo và dữ liệu attribution trong Adapty. Xem hiệu suất chiến dịch, phân tích nhóm quảng cáo và thông tin attribution trong một dashboard thống nhất. [Tìm hiểu thêm](adapty-ads-manager-analytics) - **Biểu đồ attribution Apple Ads**: Kết hợp nhiều chỉ số attribution trong các biểu đồ tùy chỉnh để phân tích hiệu suất Apple Ads của bạn cùng với dữ liệu gói đăng ký. [Tìm hiểu thêm](adapty-ads-manager-analytics#charts) - **Phân khúc attribution Apple Ads**: Tạo phân khúc người dùng dựa trên dữ liệu attribution từ Apple Ads với quy trình chỉ hai bước. Nhắm mục tiêu người dùng theo chiến dịch, nhóm quảng cáo hoặc từ khóa để phân tích và thử nghiệm chính xác hơn. [Tìm hiểu thêm](ads-manager-create-segments) - **Nền tảng tài liệu mới**: Trang tài liệu đã được migrate sang nền tảng mới, cho phép cập nhật tính năng nhanh hơn và cải thiện trải nghiệm người dùng với tính năng tìm kiếm, điều hướng và tổ chức nội dung được nâng cao. ## Tháng 12 năm 2025 \{#december-2025\} - **Tài liệu Apple Ads Manager**: Kết hợp dữ liệu chiến dịch Apple Search Ads với các chỉ số doanh thu trong một dashboard phân tích duy nhất. Tài liệu mới bao gồm việc tạo chiến dịch, quản lý nhóm quảng cáo và cách theo dõi ROI chi tiêu quảng cáo cùng với hiệu suất gói đăng ký. [Tìm hiểu thêm](ads-manager) - **Paywall web trong ứng dụng**: Hiển thị paywall dạng web ngay trong ứng dụng của bạn thông qua trình duyệt in-app, mang lại trải nghiệm liền mạch mà không cần chuyển hướng ra ngoài. [iOS](ios-web-paywall#open-web-paywalls-in-an-in-app-browser) | [Android](android-web-paywall#open-web-paywalls-in-an-in-app-browser) | [React Native](react-native-web-paywall#open-web-paywalls-in-an-in-app-browser) | [Flutter](flutter-web-paywall#open-web-paywalls-in-an-in-app-browser) - **Phân khúc cuộn (Rolling segments)**: Tạo các phân khúc đối tượng động tự động cập nhật dựa trên cửa sổ thời gian di chuyển. Ví dụ: tạo phân khúc "người dùng đã cài đặt ứng dụng trong 7 ngày qua" và phân khúc này sẽ liên tục làm mới để luôn hiển thị những khách hàng mới nhất của bạn. [Tìm hiểu thêm](segments#available-attributes) - **Hướng dẫn thiết lập chiến dịch Meta và TikTok**: Tài liệu từng bước để tạo và theo dõi chiến dịch trên Meta (Facebook & Instagram) và TikTok, tích hợp theo dõi chuyển đổi và phân tích. [Meta](meta-create-campaign) | [TikTok](tiktok-create-campaign) - **Hướng dẫn nhanh triển khai paywall thủ công**: Tích hợp in-app purchase nhanh hơn với các hướng dẫn từng bước, chỉ cách tích hợp Adapty SDK vào giao diện paywall tùy chỉnh của bạn. [iOS](ios-implement-paywalls-manually) | [Android](android-implement-paywalls-manually) | [React Native](react-native-implement-paywalls-manually) | [Flutter](flutter-implement-paywalls-manually) | [Unity](unity-implement-paywalls-manually) | [Kotlin Multiplatform](kmp-quickstart-manual) | [Capacitor](capacitor-quickstart-manual) - **Trình duyệt trong ứng dụng cho liên kết onboarding**: Các liên kết bên ngoài trong onboarding giờ đây mặc định mở trong trình duyệt tích hợp, giữ người dùng ở lại ứng dụng của bạn. Bạn có thể tùy chỉnh hành vi này để sử dụng trình duyệt ngoài nếu cần. [iOS](ios-present-onboardings#customize-how-links-open-in-onboardings) | [Android](android-present-onboardings#customize-how-links-open-in-onboardings) | [React Native](react-native-present-onboardings#customize-how-links-open-in-onboardings) - **Cải thiện gợi ý Autopilot**: Autopilot giờ đây đưa ra khuyến nghị tối ưu hóa giá tốt hơn nhờ phân tích dữ liệu gói đăng ký của bạn sâu hơn. [Thử Autopilot](autopilot) - **Chế độ tối cho tài liệu**: Tài liệu giờ hỗ trợ chế độ tối với tính năng tự động nhận diện cài đặt hệ thống hoặc chuyển đổi thủ công ở góc trên bên phải. --- # File: adapty-ecosystem --- --- title: "Hệ sinh thái Adapty" description: "Adapty là nền tảng in-app purchase dành cho ứng dụng di động. Tìm hiểu từng sản phẩm làm gì và cách chúng kết nối với nhau." --- Adapty là nền tảng in-app purchase dành cho ứng dụng di động, được xây dựng với một sứ mệnh: giúp ứng dụng tăng trưởng doanh thu. Adapty cung cấp mọi thứ bạn cần để phát triển doanh thu: thu hút người dùng, chuyển đổi họ, giữ họ tiếp tục đăng ký, và lấy lại những người đã rời đi. Một tài khoản là đủ để truy cập toàn bộ hệ sinh thái Adapty ngay từ ngày đầu. Chỉ cần nhấp vào logo Adapty để chuyển đổi giữa các sản phẩm: - **Core** — xử lý giao dịch mua mà không cần động đến StoreKit hay Google Play Billing, thiết kế paywall không cần code, và theo dõi doanh thu theo thời gian thực. Các sản phẩm khác đều được xây dựng trên nền tảng này. - **Adapty Ads Manager** — chạy và tối ưu Apple Ads, đo lường dựa trên doanh thu gói đăng ký thực tế. - **Adapty Attribution** — xem kênh quảng cáo nào thực sự tạo ra doanh thu, không cần MMP. - **Adapty Mail** — chuyển đổi người dùng dùng thử và thu hút lại người dùng đã rời đi bằng email tự động. Hai sản phẩm nữa bổ sung bên cạnh bốn sản phẩm cốt lõi: **FunnelFox** (funnel web-to-app và hosted checkout) và **Adapty Finance** (ứng trước doanh thu gói đăng ký trong tương lai). ## Các sản phẩm kết hợp với nhau như thế nào \{#how-the-products-fit-together\} Mỗi sản phẩm tiếp cận khách hàng ở một giai đoạn khác nhau trong vòng đời. Di chuột qua bất kỳ tính năng nào có liên kết để xem định nghĩa nhanh, hoặc nhấp vào để xem tài liệu chi tiết. <ProductMap /> :::link Xem thêm: [Adapty có phù hợp với tôi không?](is-adapty-right-for-me) ::: ## Được xây dựng cho quy trình làm việc với AI \{#built-for-ai-workflows\} Chạy Adapty trực tiếp từ AI coding agent của bạn — tích hợp, quản lý và tham chiếu mà không cần rời khỏi editor. Trỏ agent vào [SDK integration skill](adapty-sdk-integration-skill) cho nền tảng của bạn và nó sẽ hoàn tất toàn bộ quá trình cài đặt chỉ trong một lệnh, hoặc làm theo [hướng dẫn LLM từng bước](adapty-cursor) để tự xem xét từng bước một. Công cụ AI của bạn có thể kéo tài liệu theo bất kỳ cách nào phù hợp. Sao chép bất kỳ trang nào dưới dạng Markdown bằng nút **Copy for LLM**, hoặc trỏ nó đến [`llms.txt`](https://adapty.io/docs/vi/llms.txt) — một bản đồ của toàn bộ tài liệu. Để truy cập trực tiếp, máy chủ MCP [Context7](https://context7.com/adaptyteam/adapty-docs) cung cấp các đoạn code phù hợp nhất từ tài liệu trong Cursor, Claude Code và các IDE khác. Xem [Quản lý Adapty với AI](manage-adapty-with-ai) để biết tất cả các điểm truy cập. ## Core \{#core\} Core là nền tảng Adapty cơ bản. Nó hiển thị các paywall của bạn, quản lý các giao dịch mua và đo lường kết quả sau đó. ### SDK và các cửa hàng \{#sdks-and-stores\} Không cần lo về phần thanh toán phức tạp. Adapty xử lý toàn bộ in-app purchase, xác thực receipt và gia hạn cho bạn, đồng thời cập nhật trạng thái gói đăng ký của từng người dùng theo thời gian thực — giúp bạn luôn biết ai đang có quyền truy cập và vì sao. Bên trong ứng dụng của bạn, các [SDK](installation-of-adapty-sdks) sẽ xử lý toàn bộ luồng mua hàng, hoặc [theo dõi hệ thống thanh toán sẵn có của bạn](observer-vs-full-mode) nếu bạn đã có một hệ thống. Chúng hỗ trợ 7 nền tảng: [iOS](ios-sdk-overview), [Android](android-sdk-overview), [React Native](react-native-sdk-overview), [Flutter](flutter-sdk-overview), [Unity](unity-sdk-overview), [Kotlin Multiplatform](kmp-sdk-overview) và [Capacitor](capacitor-sdk-overview). Về phía server, Adapty kết nối trực tiếp với các cửa hàng, nên mọi lần gia hạn, hoàn tiền, và vấn đề thanh toán đều đến tay bạn theo thời gian thực — kể cả khi ứng dụng đang đóng. Hỗ trợ: [App Store](initial_ios), [Google Play](initial-android), [Stripe](stripe), và [Paddle](paddle), cùng với [tích hợp tùy chỉnh](custom-store) cho bất kỳ nhà cung cấp nào khác. ### Sản phẩm, ưu đãi và mức độ truy cập \{#products-offers-and-access-levels\} Adapty tách biệt những gì bạn bán khỏi những gì người dùng được mở khóa. Nhờ sự tách biệt này, bạn có thể thay đổi giá, hoán đổi sản phẩm hoặc tung ra các ưu đãi mà không cần phát hành bản cập nhật ứng dụng. Ba thành phần tạo nên điều đó: - **[Sản phẩm](product)** — một sản phẩm hợp nhất các SKU từ App Store, Play Store và web — gói đăng ký, sản phẩm mua một lần hoặc consumable — giúp bạn quản lý danh mục ở một nơi duy nhất. - **[Ưu đãi](offers)** — các đòn bẩy tăng tỷ lệ chuyển đổi: ưu đãi giới thiệu, ưu đãi và ưu đãi thu hút khách hàng cũ. - **[Mức độ truy cập](access-level)** — cho phép bạn tách biệt quyền truy cập khỏi từng sản phẩm cụ thể. ### Flow và placement \{#flows-and-placements\} Xây dựng các màn hình tạo doanh thu — paywall, onboarding, quiz — mà không cần viết code. [Flow](adapty-flow-builder) chạy trực tiếp trên thiết bị thông qua SDK, vì vậy bạn có thể thay đổi nội dung, thiết kế và giá cả bất cứ lúc nào mà không cần phát hành lại ứng dụng. Bắt đầu từ một [template](paywall-builder-templates) hoặc trang trắng. Gắn mỗi flow vào một [placement](placements), sau đó nhắm mục tiêu đến các [đối tượng](audience) khác nhau được xây dựng từ các [phân khúc](segments). ### Hồ sơ người dùng và phân khúc \{#profiles-and-segments\} Tra cứu bất kỳ người đăng ký nào để xem toàn bộ lịch sử của họ — [hồ sơ người dùng](profiles-crm) lưu trữ timeline sự kiện, trạng thái gói đăng ký, doanh thu và các thuộc tính tùy chỉnh của từng người dùng. Phân chia người dùng thành các [phân khúc](segments) theo bất kỳ thuộc tính nào, sau đó cá nhân hóa nội dung hiển thị, lọc số liệu phân tích và giới hạn phạm vi A/B test. [Event feed](event-feed) phát trực tiếp mọi sự kiện gói đăng ký ngay khi xảy ra. ### A/B test và AI Growth Advisor \{#ab-tests-and-ai-growth-advisor\} Tăng doanh thu bằng cách tìm ra điều gì có tỷ lệ chuyển đổi tốt nhất: - **[A/B test](ab-tests)** — thử nghiệm các mức giá, thời gian dùng thử và thiết kế flow khác nhau. - **[AI Growth Advisor](autopilot)** — cho bạn biết chính xác nên A/B test gì tiếp theo. So sánh paywall của bạn với hơn 20.000 ứng dụng đăng ký và xếp hạng các thí nghiệm theo mức tăng doanh thu dự kiến. Xem [cách hoạt động](autopilot-how-it-works). ### Phân tích và Dự đoán \{#analytics-and-predictions\} [Analytics](analytics) chuyển đổi dữ liệu từ cửa hàng, SDK và attribution thành một dashboard doanh thu thời gian thực với [hàng chục chỉ số](metric-comparison-table) — nhiều hơn hẳn so với những gì App Store và Google Play hiển thị. Đi sâu hơn với các phân tích [cohort](analytics-cohorts), [funnel](analytics-funnels), [retention](analytics-retention) và [conversion](analytics-conversion). [Dự đoán](predicted-ltv-and-revenue) dự báo LTV của từng cohort hàng tháng trước và xác định [người chiến thắng trong A/B test](predictions-in-ab-tests) trước khi đạt mức ý nghĩa thống kê. [Báo cáo](reports) được lên lịch sẽ gửi thẳng vào hộp thư của bạn. ### Adapty Developer CLI \{#developer-cli\} [Adapty Developer CLI](developer-cli-quickstart) giúp bạn thiết lập sản phẩm, placement và mức độ truy cập ngay từ dòng lệnh — một lựa chọn thay thế cho dashboard dành cho các developer quen làm việc với terminal. ## Adapty Ads Manager \{#adapty-ads-manager\} [Adapty Ads Manager](adapty-ads-manager) là một nền tảng Apple Ads. Nó thay thế Apple Ads console gốc bằng tính năng tối ưu hóa dựa trên AI, attribution doanh thu theo thời gian thực và thông tin cạnh tranh. Vì Core đã theo dõi mọi lượt cài đặt, dùng thử, gói đăng ký và gia hạn, Ads Manager kết nối trực tiếp chi phí quảng cáo với LTV. Không cần MMP. Các tính năng chính: - **[Chiến dịch và từ khóa](ads-manager)** — xây dựng và quản lý chiến dịch, nhóm quảng cáo và giá thầu ngay từ Adapty dashboard. - **[AI Agent](ads-manager-ai-agent)** — truy vấn toàn phễu và nhận gợi ý bằng ngôn ngữ thông thường. - **[Market Intelligence](ads-manager-market-intelligence)** — chiến lược từ khóa của đối thủ trên 50+ quốc gia. - **[CPP A/B Tests](ads-manager-cpp-ab-tests)** — so sánh trực tiếp các trang sản phẩm tùy chỉnh. - **[Automations](ads-manager-automations)** — chiến dịch tự tối ưu hóa. Giá thầu, từ khóa và cụm từ tìm kiếm tự động điều chỉnh khi chỉ số vượt ngưỡng đã đặt. ## Adapty Attribution \{#adapty-attribution\} [Adapty Attribution](adapty-user-acquisition) kết nối lượt cài đặt ứng dụng và doanh thu gói đăng ký với các chiến dịch quảng cáo đã tạo ra chúng. Nó kết hợp chi tiêu từ nền tảng quảng cáo, lượt click từ link theo dõi và sự kiện cài đặt từ SDK thành các góc nhìn về ROAS, LTV và cohort trên tất cả các kênh có trả phí của bạn. Không cần MMP bên ngoài. Tính năng chính: - **[Tích hợp nền tảng quảng cáo](ua-integrations)** — Meta Ads, TikTok for Business, FunnelFox, và các kết nối S3/GCS. - **[Liên kết theo dõi](ua-tracking-links)** — được tạo trong Adapty, thêm vào chiến dịch của bạn và khớp với lượt cài đặt ngay lần khởi chạy đầu tiên. - **[Deeplink hoãn](ua-deferred-data)** — điều hướng người dùng mới đến đúng nội dung trong ứng dụng ngay lần khởi chạy đầu tiên, kể cả khi họ đã nhấp vào trước khi cài đặt. - **[Dữ liệu attribution](ua-attribution-data)** — nhận payload attribution trong ứng dụng của bạn để xử lý theo logic tùy chỉnh. ## Adapty Mail \{#adapty-mail\} [Adapty Mail](adapty-mail) biến dữ liệu người dùng thành các chiến dịch email do AI tạo ra. Nó xây dựng [hồ sơ thương hiệu](mail-brand) từ trang listing của ứng dụng trên cửa hàng, trang landing page và các trang mạng xã hội, sau đó tạo ra một chuỗi email hoàn chỉnh chỉ trong vài phút. Email được gửi từ tên miền đã được xác minh của bạn, và mỗi lần mua hàng đều được gán attribution về email đã dẫn đến giao dịch đó. Không cần nền tảng email riêng. Các tính năng chính: - **[Campaigns](mail-email-campaigns)** — một chuỗi nhiều email hoàn chỉnh, được tạo trong một lần. Chuỗi này bắt đầu gửi khi bạn gắn nó vào một flow. - **[Flows](mail-flows)** — liên kết một campaign với một phân khúc và một trigger sự kiện gói đăng ký như *chưa từng mua* hoặc *sự cố thanh toán*, để chuỗi email tự động gửi đi. - **[Web paywall](mail-checkout)** — các trang thanh toán được cá nhân hóa, mỗi người nhận một trang riêng, giúp các giao dịch mua hàng được attribution về đúng email. - **[Segments](mail-segments)** và **[profiles](mail-profiles)** — tiếp cận những người dùng có khả năng chuyển đổi cao nhất. Xây dựng phân khúc dựa trên trạng thái mua hàng, quốc gia hoặc doanh thu, rồi kích hoạt campaign hoặc flow từ đó. Dữ liệu đến từ Adapty Core, giới hạn ở các hồ sơ người dùng đã được xác định có email. ## Kết nối Adapty với hệ thống hiện tại của bạn \{#connect-adapty-to-your-existing-stack\} [Tích hợp bên thứ ba](configuration) chuyển tiếp các [sự kiện](events) gói đăng ký đến các nền tảng phân tích, attribution và nhắn tin mà team bạn đang dùng: - **Analytics**: [Amplitude](amplitude), [Mixpanel](mixpanel), [PostHog](posthog), [Firebase / Google Analytics](firebase-and-google-analytics), [AppMetrica](appmetrica), [SplitMetrics Acquire](splitmetrics). - **Attribution**: [AppsFlyer](appsflyer), [Adjust](adjust), [Branch](branch), [Airbridge](airbridge), [Apple Ads](apple-search-ads), [Singular](singular), [Tenjin](tenjin), [Asapty](asapty), [Facebook Ads](facebook-ads). - **Nhắn tin**: [Braze](braze), [OneSignal](onesignal), [Pushwoosh](pushwoosh), [Slack](slack). - **Webhook và ETL**: [webhook](webhook) tùy chỉnh, [Amazon S3](s3-exports), [Google Cloud Storage](google-cloud-storage). ## Hệ sinh thái mở rộng \{#the-wider-ecosystem\} Thêm hai sản phẩm kết nối với dữ liệu Adapty của bạn nhưng phục vụ các nhu cầu nằm ngoài vòng đời cốt lõi. ### FunnelFox [FunnelFox](https://funnelfox.com/docs/integrations/subscription-management/adapty) là một công cụ xây dựng funnel từ web đến ứng dụng. Nó tạo ra các trang đích và bài quiz để dẫn dắt người dùng đến ứng dụng của bạn. [Hệ thống thanh toán](https://funnelfox.com/docs/billing/integration-billing-funnelfox) của nó thu tiền từ người dùng trực tiếp trên web. Kết nối FunnelFox với Adapty để theo dõi gói đăng ký và attribution doanh thu. ### Adapty Finance [Adapty Finance](https://adapty.io/blog/introducing-adapty-finance/) ứng trước doanh thu gói đăng ký trong tương lai của bạn, giúp bạn không phải chờ đợi thanh toán từ cửa hàng. ## Bước tiếp theo \{#next-steps\} - **[Adapty có phù hợp với tôi không?](is-adapty-right-for-me)** — tổng quan nền tảng theo từng trường hợp sử dụng. - **[Hướng dẫn bắt đầu nhanh](quickstart)** — kết nối cửa hàng, thêm sản phẩm và tích hợp SDK. - **[Quản lý Adapty bằng AI](manage-adapty-with-ai)** — tất cả các cách sử dụng Adapty cùng với công cụ lập trình AI. --- # File: generate-in-app-purchase-key --- --- title: "Tạo In-App Purchase Key trong App Store Connect" description: "Tạo in-app purchase key để xác thực giao dịch an toàn." --- **In-App Purchase Key** là một API key chuyên dụng được tạo trong App Store Connect để xác thực các giao dịch mua hàng bằng cách xác nhận tính xác thực của chúng. :::note Để tạo API key cho App Store Server API, bạn phải có vai trò Admin hoặc Account Holder trong App Store Connect. Bạn cũng có thể đọc thêm về cách tạo API Key trong [Tài liệu Apple Developer](https://developer.apple.com/documentation/appstoreserverapi/creating-api-keys-to-authorize-api-requests). ::: 1. Mở **App Store Connect**. Truy cập vào mục [**Users and Access** → **Integrations** → **In-App Purchase**](https://appstoreconnect.apple.com/access/integrations/api/subs). 2. Sau đó nhấn vào nút thêm **(+)** bên cạnh tiêu đề **Active**. <img src="/assets/shared/img/6d737db-generate_in-app_key.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Generate In-App Purchase Key** vừa mở, nhập tên cho key để bạn dễ nhận biết sau này. Tên này sẽ không được dùng trong Adapty. 4. Nhấn nút **Generate**. Sau khi cửa sổ **Generate in-App Purchase Key** đóng lại, bạn sẽ thấy key vừa tạo xuất hiện trong danh sách **Active**. <img src="/assets/shared/img/fac066b-download_inapp_file.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Sau khi đã tạo API key, nhấn nút **Download In-App Purchase Key** để tải key về dưới dạng file. <img src="/assets/shared/img/d59faff-download_in-app_purchase_key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Trong cửa sổ **Download in-App Purchase Key**, nhấn nút **Download**. File sẽ được lưu vào máy tính của bạn. Điều quan trọng là phải giữ file này an toàn để tải lên Adapty Dashboard sau này. Lưu ý rằng file được tạo ra chỉ có thể tải xuống một lần duy nhất, vì vậy hãy đảm bảo lưu trữ an toàn cho đến khi bạn tải lên. File .p8 key được tạo từ mục **In-App Purchase** sẽ được sử dụng khi [cấu hình tích hợp ban đầu của Adapty với App Store](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file). **Tiếp theo:** - [Cấu hình tích hợp App Store](app-store-connection-configuration) --- # File: app-store-connection-configuration --- --- title: "Cấu hình tích hợp App Store" description: "Cấu hình kết nối App Store của bạn để theo dõi gói đăng ký liền mạch." --- <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/VJQbzoTCkqs?si=l7BPX9mIu6GVGZ0Z" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Phần này mô tả cách thiết lập kết nối giữa App Store và Adapty cho ứng dụng iOS của bạn. Đây là yêu cầu bắt buộc để chúng tôi có thể hiển thị số liệu phân tích gói đăng ký và xác thực các giao dịch mua. Bạn có thể hoàn tất tích hợp trong quá trình onboarding ban đầu hoặc sau này trong **App Settings** trên Adapty Dashboard. Mặc dù bạn có thể đã cấu hình tích hợp giữa ứng dụng và Adapty trong quá trình onboarding, bạn vẫn có thể chỉnh sửa các cài đặt này sau trong **App settings**. :::danger Các thay đổi cấu hình có thể được thực hiện an toàn trong giai đoạn Sandbox, trước khi ứng dụng của bạn ra mắt với SDK Adapty được cài đặt. Các thay đổi sau khi phát hành có thể làm gián đoạn luồng mua hàng trong ứng dụng của bạn. ::: ## Bước 1. Cung cấp Bundle ID và Apple app ID \{#step-1-provide-bundle-id-and-apple-app-id\} Cả **Bundle ID** và **Apple app ID** đều bắt buộc. **Bundle ID** là mã định danh duy nhất của ứng dụng trong App Store, cho phép các chức năng cốt lõi của Adapty hoạt động, chẳng hạn như xử lý gói đăng ký. **Apple app ID** cũng cần thiết để [tạo sản phẩm mới và đẩy lên cửa hàng](create-product#create-product-and-push-to-store) từ trang **Products**. :::note Nếu không có **Apple app ID**, tùy chọn **Create a new product and push to stores** trên trang **Products** sẽ bị vô hiệu hóa mà dashboard không hiển thị lý do. ::: 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**. <Zoom> <img src="/docs/img/afd5012-bundle_id_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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**. <Zoom> <img src="/docs/img/2d64163-bundle_id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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. Cung cấp Issuer ID và Key ID \{#step-2-provide-issuer-id-and-key-id\} **In-app purchase Issuer ID** (được gọi là **Issuer ID** trong App Store Connect) là một ID đặc biệt dùng để xác định tổ chức phát hành đã tạo token xác thực. **In-App Purchase Key ID** (được gọi là **Key ID** trong App Store Connect) là mã định danh duy nhất gắn với khóa mã hóa bạn đã tạo trong phần [Tạo In-App Purchase Key trong App Store Connect](generate-in-app-purchase-key). 1. Mở **App Store Connect**. Điều hướng đến mục [**Users and Access** → **Integrations** → **In-App Purchase**](https://appstoreconnect.apple.com/access/integrations/api/subs). 2. Trong danh sách **Active**, tìm khóa bạn đã tạo ở phần [Tạo In-App Purchase Key trong App Store Connect](generate-in-app-purchase-key). <img src="/assets/shared/img/19a2868-issuer_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Sao chép **Issuer ID** và dán vào trường **In-app purchase Issuer ID** trên Adapty Dashboard. <img src="/assets/shared/img/c2b42e7-issuer_id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Sao chép **Key ID** và dán vào trường **In-app purchase Key ID** trên Adapty Dashboard. ## Bước 3. Tải lên file In-App Purchase Key \{#step-3-upload-in-app-purchase-key-file\} Tải lên file **In-App Purchase Key** bạn đã tải xuống ở phần [Tạo In-App Purchase Key trong App Store Connect](generate-in-app-purchase-key) <img src="/assets/shared/img/88cdfff-download_inapp_file.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> vào trường **Private key (.p8 file)** trên Adapty Dashboard. <img src="/assets/shared/img/253b840-in-app_file_upload.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 4. Đối với trial và ưu đãi đặc biệt – thiết lập promotional offers \{#step-4-for-trials-and-special-offers--set-up-promotional-offers\} :::important Bước này bắt buộc nếu ứng dụng của bạn có [trial hoặc các ưu đãi khác](offers). ::: 1. Sao chép key ID bạn đã dùng ở [Bước 2](#step-2-provide-issuer-id-and-key-id) vào trường **Subscription key ID** trong mục **App Store promotional offers**. 2. Tải lên file **In-App Purchase Key** bạn đã dùng ở [Bước 3](#step-3-upload-in-app-purchase-key-file) vào khu vực **Subscription key (.p8 file)** trong mục **App Store promotional offers**. <img src="/assets/shared/img/promo-key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 5. Nhập App Store shared secret \{#step-5-enter-app-store-shared-secret\} **App Store shared secret** (còn được gọi là App Store Connect Shared Secret) là một chuỗi thập lục phân 32 ký tự dùng để xác thực in-app purchase và receipt của gói đăng ký. 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 mục **General** → **App Information**. 2. Cuộn xuống phần **App-Specific Shared Secret**. <img src="/assets/shared/img/2bd112a-shared_secret_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::info Nếu không thấy phần **App-Specific Shared Secret**, hãy đảm bảo bạn có vai trò Account Holder hoặc Admin. Nếu bạn có vai trò Admin nhưng vẫn không thấy phần này, hãy yêu cầu Account Holder của ứng dụng (người đã tạo ứng dụng trong App Store Connect) tạo App Store shared secret. Sau đó, phần này sẽ hiển thị với các Admin khác. ::: 3. Nhấp vào nút **Manage**. <img src="/assets/shared/img/2d8b4c0-shared_secret_apple_copy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trong cửa sổ **App-Specific Shared Secret** vừa mở, sao chép **Shared Secret**. Nếu chưa thấy shared secret, hãy nhấp vào nút **Manage** hoặc **Generate** (tùy nút nào đang hiển thị), sau đó sao chép **Shared Secret**. 5. Dán **Shared Secret** vừa sao chép vào trường **App Store shared secret** trên Adapty Dashboard. <img src="/assets/shared/img/4f9624d-shared_secret.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Nhấp vào nút **Save** trên Adapty Dashboard để xác nhận các thay đổi. ## Bước 6. Thêm App Store Connect API key \{#step-6-add-app-store-connect-api-key\} Tạo App Store Connect API key và thêm vào Adapty để có thể [quản lý sản phẩm trên App Store từ Adapty dashboard](create-product#create-product-and-push-to-store): 1. Trong App Store Connect, đi tới [**Users and Access > Integrations > Team keys**](https://appstoreconnect.apple.com/access/integrations/api) và nhấp **+**. <img src="/assets/shared/img/app-store-connect-api.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong cửa sổ **Generate API key**, nhập tên cho khóa và cấp quyền truy cập **Admin**. <img src="/assets/shared/img/generate-api-key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp **Download** bên cạnh khóa của bạn. Lưu ý rằng bạn chỉ có thể tải xuống một lần. <img src="/assets/shared/img/download-api-key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trên Adapty dashboard, đi tới [**App settings > iOS SDK**](https://app.adapty.io/settings/ios-sdk) và nhấp **Connect API key**. <img src="/assets/shared/img/connect-api-key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Điền vào các trường trong cửa sổ: - **Issuer ID**: Sao chép từ [**Users and Access > Integrations > Team keys**](https://appstoreconnect.apple.com/access/integrations/api). Nó nằm phía trên bảng **API keys**. <img src="/assets/shared/img/issuer-id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> - **Key ID**: Sao chép từ [**Users and Access > Integrations > Team keys**](https://appstoreconnect.apple.com/access/integrations/api). Nó nằm trong bảng **API keys** bên cạnh khóa của bạn. <img src="/assets/shared/img/key-id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> - **API key**: Tải lên file API key bạn đã tải xuống từ App Store Connect. <img src="/assets/shared/img/app-store-connect-key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Nhấp **Connect**. **Bước tiếp theo** - [Bật thông báo máy chủ App Store](enable-app-store-server-notifications) --- # File: enable-app-store-server-notifications --- --- title: "Bật thông báo máy chủ App Store" description: "Bật thông báo máy chủ App Store để theo dõi các sự kiện gói đăng ký theo thời gian thực." --- Việc thiết lập thông báo máy chủ App Store rất quan trọng để đảm bảo độ chính xác của dữ liệu, vì nó cho phép bạn nhận cập nhật tức thì từ App Store, bao gồm thông tin về hoàn tiền và các sự kiện khác. :::important Cần có Adapty iOS SDK 2.10.0 trở lên để hỗ trợ đầy đủ App Store Server Notifications V2. ::: 1. Sao chép **URL for App Store server notification** trong Adapty Dashboard. <img src="/assets/shared/img/2901185-app_server_notifications.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và vào mục **General** → **App Information**, phần **App Store Server Notifications**. 3. Dán **URL for App Store server notification** đã sao chép vào các trường **Production Server URL** và **Sandbox Server URL**. <img src="/assets/shared/img/86fb3d2-app_server_notifications_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Chuyển tiếp sự kiện thô \{#raw-events-forwarding\} Đôi khi bạn vẫn muốn nhận các sự kiện S2S thô từ Apple. Để tiếp tục nhận chúng khi đang dùng Adapty, chỉ cần thêm endpoint của bạn vào trường **URL for forwarding raw Apple events**, và chúng tôi sẽ gửi các sự kiện thô nguyên bản từ Apple đến bạn. <img src="/assets/shared/img/e9f4bba-CleanShot_2021-03-16_at_19.30.272x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> **Tiếp theo** Thiết lập Adapty SDK cho: - [iOS](sdk-installation-ios) - [React Native](sdk-installation-reactnative) - [Flutter](sdk-installation-flutter) - [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform) - [Unity](sdk-installation-unity) --- # File: troubleshoot-app-store-integration --- --- title: "Xử lý sự cố tích hợp App Store" description: "Giải quyết các sự cố thiết lập Apple App Store phổ biến — thỏa thuận chờ xử lý, độ trễ thông báo máy chủ và không khớp giá." --- Bài viết này đề cập đến các sự cố tích hợp App Store phổ biến. Mỗi phần dưới đây liệt kê triệu chứng, nguyên nhân gốc rễ và cách giải quyết. ## Sản phẩm không hiển thị \{#products-dont-appear\} Hai triệu chứng bề mặt chỉ đến cùng một nguyên nhân gốc rễ: - Khóa API App Store Connect được thiết lập đúng, nhưng Adapty không thể lấy sản phẩm. - Sản phẩm tồn tại trong App Store Connect nhưng không hiển thị trong Adapty, hoặc hiển thị ít hơn dự kiến. SDK báo lỗi "Product Id not found" khi thực hiện mua hàng. Nguyên nhân phổ biến nhất là **thỏa thuận Apple chưa được ký** — thỏa thuận thanh toán, biểu mẫu thuế hoặc biểu mẫu ngân hàng đang ở trạng thái chờ xử lý hoặc chưa ký. Khi các thỏa thuận đang chờ xử lý, App Store Connect API sẽ trả về lỗi 403 một cách im lặng trên các endpoint liên quan đến sản phẩm. Không có lỗi rõ ràng nào hiển thị với Adapty; các sản phẩm bị lọc ra một cách âm thầm. Truy cập **App Store Connect → Agreements, Tax, and Banking** và ký tất cả các thỏa thuận đang chờ xử lý. Sau đó đồng bộ lại trong **App settings → iOS SDK** của Adapty. ## Thông báo máy chủ App Store hiển thị "Delayed" \{#app-store-server-notifications-show-delayed\} Trong App Store Connect, trạng thái App Store Server Notifications có thể hiển thị là **Delayed**. Điều này có nghĩa là Apple đang bị trễ trong việc gửi thông báo sự kiện gói đăng ký — các sự kiện gia hạn, hủy và vấn đề thanh toán đang được xếp hàng và đến muộn. Thống kê lượt cài đặt không bị ảnh hưởng. Adapty đếm lượt cài đặt từ lần khởi chạy ứng dụng đầu tiên, không phải từ thông báo phía máy chủ. Nếu dữ liệu gia hạn hoặc hủy bị trễ, trạng thái Delayed là nguyên nhân có khả năng cao nhất. Trạng thái này thường tự động được xóa khi Apple xử lý hết hàng đợi. ## Giá trong Adapty không khớp với App Store \{#prices-in-adapty-dont-match-app-store\} Trường **price** trên trang chỉnh sửa sản phẩm của Adapty hoạt động khác nhau tùy thuộc vào cách sản phẩm được thêm vào. Nếu bạn tạo sản phẩm trong Adapty và đẩy lên cửa hàng từ dashboard, giá này được dùng làm giá cửa hàng ban đầu. Nếu bạn thêm một sản phẩm đã tồn tại trong cửa hàng, giá này chỉ là giá tạm thời. Phân tích, tích hợp và SDK của Adapty đều sử dụng giá thực tế được lấy từ App Store. Những thay đổi về giá trên App Store không đồng bộ lại để cập nhật giá tạm thời này, và hiện tại bạn không thể chỉnh sửa giá tạm thời từ dashboard. ## Xuất giá CSV trống \{#csv-price-export-is-empty\} Nếu file CSV xuất giá của bạn chỉ trả về tiêu đề cột, khóa API App Store Connect của bạn chưa được cấu hình đầy đủ. Xem [Bước 6 — Thêm khóa API App Store Connect](app-store-connection-configuration#step-6-add-app-store-connect-api-key). ## Không thể đẩy sản phẩm mới lên App Store \{#cant-push-new-products-to-app-store\} Adapty có thể đẩy sản phẩm mới lên App Store Connect khi bạn tạo chúng trong dashboard. Tùy chọn đẩy bị chặn nếu tích hợp App Store của bạn chưa được cấu hình đầy đủ. Hai cài đặt sau là bắt buộc: - **Apple app ID**: Cấu hình tại [Bước 1 — Cung cấp Bundle ID và Apple app ID](app-store-connection-configuration#step-1-provide-bundle-id-and-apple-app-id). - **App Store Connect API key**: Cấu hình tại [Bước 6 — Thêm khóa API App Store Connect](app-store-connection-configuration#step-6-add-app-store-connect-api-key). --- # File: enabling-of-devepoler-api --- --- title: "Bật Developer APIs trong Google Play Console" description: "Bật Developer API của Adapty để tự động hóa và tối ưu hóa việc quản lý gói đăng ký trong ứng dụng của bạn." --- <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/7dN50n5bcLc?si=c2znttIb--4VcrRO" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Nếu ứng dụng của bạn có trên Play Store, việc kích hoạt Developer APIs là bước quan trọng để tích hợp với Adapty. Bước này đảm bảo kết nối thông suốt giữa ứng dụng và nền tảng của chúng tôi, hỗ trợ các quy trình tự động và phân tích dữ liệu thời gian thực để tối ưu mô hình gói đăng ký. Các API sau cần được bật: - [Google Play Android Developer API](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com) - [Google Play Developer Reporting API](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com) - [Cloud Pub/Sub API](https://console.cloud.google.com/marketplace/product/google/pubsub.googleapis.com) Nếu ứng dụng của bạn không phân phối qua Play Store, bạn có thể bỏ qua bước này. Tuy nhiên, nếu bạn có bán qua Play Store, bạn có thể tạm hoãn bước này, dù đây là yêu cầu thiết yếu cho chức năng cơ bản của Adapty. Sau khi hoàn tất quá trình onboarding, bạn có thể cấu hình cài đặt cửa hàng trong phần **App settings**. Dưới đây là cách bật Developer APIs trong Google Play Console: 1. Mở [Google Cloud Console](https://console.cloud.google.com/). 2. Ở góc trên bên trái cửa sổ Google Cloud, chọn dự án bạn muốn sử dụng hoặc tạo dự án mới. Đảm bảo bạn dùng cùng một dự án Google Cloud cho đến khi tải tệp khóa service account lên Adapty. <img src="/assets/shared/img/fd66a11-google_cloud_project.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Mở trang [**Google Play Android Developer API**](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com). <img src="/assets/shared/img/f754f72-google_play_api.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấn nút **Enable** và chờ đến khi trạng thái **Enabled** hiện ra. Điều này có nghĩa là Google Android Developer API đã được bật. <img src="/assets/shared/img/d47ed14-google_play_api_create_credentials.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Mở trang [**Google Play Developer Reporting API**](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com). <img src="/assets/shared/img/966cf73-Google_play_developer_reporting_api.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Nhấn nút **Enable** và chờ đến khi trạng thái **Enabled** hiện ra. <img src="/assets/shared/img/e776d77-Google_play_developer_reporting_api_enabled.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Mở trang [**Cloud Pub/Sub API**](https://console.cloud.google.com/marketplace/product/google/pubsub.googleapis.com). <img src="/assets/shared/img/b13f609-enable_Cloud_Pub_Sub_API.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 8. Nhấn nút **Enable** và chờ đến khi trạng thái **Enabled** hiện ra. <img src="/assets/shared/img/3f45602-Cloud_Pub_Sub_API_enabled.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Developer APIs đã được bật. Bạn có thể kiểm tra lại trên trang [**APIs & Services**](https://console.cloud.google.com/apis/dashboard) của Google Cloud Console. Cuộn trang xuống và xác nhận bảng ở cuối trang có đủ 3 API sau: - Google Play Android Developer API - Google Play Developer Reporting API - Cloud Pub/Sub API <img src="/assets/shared/img/b81d174-google_enabled_api.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> **Bước tiếp theo** - [Tạo service account trong Google Cloud Console](create-service-account) --- # File: create-service-account --- --- title: "Tạo tài khoản dịch vụ trong Google Cloud Console" description: "Tìm hiểu cách tạo tài khoản dịch vụ để truy cập API an toàn trong Adapty." --- Để Adapty tự động hóa việc truy cập dữ liệu, cần có một tài khoản dịch vụ trong Google Play Console. 1. Mở mục [**IAM & Admin** -> **Service accounts**](https://console.cloud.google.com/iam-admin/serviceaccounts) trong Google Cloud Console. Hãy đảm bảo bạn đang dùng đúng project. <img src="/assets/shared/img/17bbf45-google_cloud_create_service_account.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong cửa sổ **Service accounts**, nhấn nút **Create service account**. <img src="/assets/shared/img/b93eec1-service_account_details.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong phần con **Service account details** của cửa sổ **Create service account**, nhập **Service Account Name** tùy ý. Chúng tôi khuyên bạn nên đặt tên có chứa "Adapty" để dễ nhận biết mục đích của tài khoản này. **Service account ID** sẽ được tạo tự động. 4. Sao chép địa chỉ email của tài khoản dịch vụ và lưu lại để dùng sau. 5. Nhấn nút **Create and continue**. <img src="/assets/shared/img/e69d713-grant_access_to_project.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Trong danh sách thả xuống **Select a role** của phần con **Grant this service account access to project**, chọn **Pub/Sub -> Pub/Sub Admin**. Vai trò này cần thiết để bật thông báo nhà phát triển theo thời gian thực. <img src="/assets/shared/img/976299c-service_account_role.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Nhấn nút **Add another role**. 8. Trong danh sách thả xuống **Role** mới xuất hiện, chọn **Monitoring -> Monitoring Viewer**. Vai trò này cần thiết để cho phép giám sát hàng đợi thông báo. 9. Nhấn nút **Continue**. <img src="/assets/shared/img/ffe8d82-grant_user_access.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 10. Nhấn nút **Done** mà không thay đổi gì. Cửa sổ **Service accounts** sẽ mở ra. **Tiếp theo** - [Cấp quyền cho tài khoản dịch vụ trong Google Play Console](grant-permissions-to-service-account) --- # File: grant-permissions-to-service-account --- --- title: "Cấp quyền cho service account trong Google Play Console" description: "Cấp quyền cho service account để truy cập API an toàn và hiệu quả." --- Cấp các quyền cần thiết cho service account mà Adapty sẽ sử dụng để quản lý gói đăng ký và xác thực các giao dịch mua. 1. Mở trang [**Users and permissions**](https://play.google.com/console/u/0/developers/8970033217728091060/users-and-permissions) trong Google Play Console và nhấn nút **Invite new users**. <img src="/assets/shared/img/7b0e614-users_and_permissions.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong trang **Invite user**, nhập email của service account bạn đã tạo. <img src="/assets/shared/img/3afd002-invite_user.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Chuyển sang tab **Account permissions**. <img src="/assets/shared/img/4e2717b-account_permissions.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Chọn các quyền sau: - View app information and download bulk reports (read-only) - View financial data, orders, and cancellation survey responses - Manage orders and subscriptions - Manage store presence 5. Nhấn nút **Invite user**. 6. Trong cửa sổ **Send invite?**, nhấn nút **Send invite**. Service account sẽ xuất hiện trong danh sách người dùng. **Bước tiếp theo** - [Tạo file khóa service account trong Google Play Console](create-service-account-key-file) --- # File: create-service-account-key-file --- --- title: "Tạo file khóa tài khoản dịch vụ trong Google Play Console" description: "Tìm hiểu cách tạo file khóa tài khoản dịch vụ để tích hợp liền mạch với Adapty." --- Để liên kết ứng dụng di động của bạn trên Play Store với Adapty, bạn cần tạo các file khóa tài khoản dịch vụ đặc biệt trong Google Play Console và tải chúng lên Adapty. Các file này giúp bảo mật ứng dụng và ngăn chặn truy cập trái phép. :::warning Thông thường, tài khoản dịch vụ mới cần ít nhất 24 giờ để được kích hoạt. Tuy nhiên, có một [mẹo](https://stackoverflow.com/a/60691844) như sau: Sau khi tạo tài khoản dịch vụ trong [Google Play Console](https://play.google.com/apps/publish/), mở bất kỳ ứng dụng nào và điều hướng đến **Monetize** -> **Products** -> **Subscriptions/In-app products**. Chỉnh sửa mô tả của bất kỳ sản phẩm nào rồi lưu lại. Thao tác này sẽ kích hoạt tài khoản dịch vụ ngay lập tức, và bạn có thể hoàn tác các thay đổi sau đó. ::: 1. Mở mục [**Service accounts**](https://console.cloud.google.com/iam-admin/serviceaccounts) trong Google Play Console. Đảm bảo bạn đã chọn đúng dự án. <img src="/assets/shared/img/c3156cb-action_manage_keys.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong cửa sổ hiện ra, nhấp vào **Add key** và chọn **Create new key** từ menu thả xuống. <img src="/assets/shared/img/44b30ee-create_new_key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Create private key for [Your_project_name]**, nhấp vào **Create**. Khóa riêng tư của bạn sẽ được lưu vào máy tính dưới dạng file JSON. Bạn có thể tìm thấy nó bằng tên file được hiển thị trong cửa sổ **Private key saved to your computer**. <img src="/assets/shared/img/e7b8101-cretae_private_key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trong cửa sổ **Create private key for Your_project_name**, nhấp nút **Create**. Thao tác này sẽ lưu khóa riêng tư của bạn vào máy tính dưới dạng file JSON. Bạn có thể dùng tên file được cung cấp trong cửa sổ **Private key saved to your computer** để tìm lại file đó khi cần. <img src="/assets/shared/img/187ddc6-Private_key_saved.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bạn sẽ cần file này khi [cấu hình tích hợp Google Play Store](google-play-store-connection-configuration). :::warning Thông thường, tài khoản dịch vụ mới cần ít nhất 24 giờ để được kích hoạt. Tuy nhiên, có một [mẹo](https://stackoverflow.com/a/60691844) như sau: Sau khi tạo tài khoản dịch vụ trong [Google Play Console](https://play.google.com/apps/publish/), mở bất kỳ ứng dụng nào và điều hướng đến **Monetize** -> **Products** -> **Subscriptions/In-app products**. Chỉnh sửa mô tả của bất kỳ sản phẩm nào rồi lưu lại. Thao tác này sẽ kích hoạt tài khoản dịch vụ ngay lập tức, và bạn có thể hoàn tác các thay đổi sau đó. ::: **Tiếp theo** - [Cấu hình tích hợp Google Play Store](google-play-store-connection-configuration) --- # File: google-play-store-connection-configuration --- --- title: "Cấu hình tích hợp Google Play Store" description: "Cấu hình kết nối Google Play Store trong Adapty để xử lý in-app purchase suôn sẻ." --- Phần này mô tả quy trình tích hợp ứng dụng di động của bạn được bán qua Google Play với Adapty. Bạn cần nhập dữ liệu cấu hình ứng dụng từ Play Store vào Adapty Dashboard. Bước này rất quan trọng để xác thực các giao dịch mua và nhận cập nhật gói đăng ký từ Play Store trong Adapty. Bạn có thể hoàn tất quá trình này trong lần onboarding đầu tiên hoặc thay đổi sau trong **App Settings** của Adapty Dashboard. :::danger Chỉ được phép thay đổi cấu hình trước khi bạn phát hành ứng dụng di động tích hợp Adapty paywall. Việc thay đổi sau khi phát hành sẽ phá vỡ tích hợp và các paywall sẽ ngừng hiển thị trong ứng dụng di động của bạn. ::: ## Bước 1. Cung cấp Package name \{#step-1-provide-package-name\} Package name là định danh duy nhất của ứng dụng trên Google Play Store. Đây là yêu cầu bắt buộc cho các chức năng cơ bản của Adapty, chẳng hạn như xử lý gói đăng ký. 1. Mở [Google Play Developer Console](https://play.google.com/console/u/0/developers). 2. Chọn ứng dụng mà bạn cần lấy ID. Cửa sổ **Dashboard** sẽ mở ra. <img src="/assets/shared/img/7889edb-package_name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Tìm product ID bên dưới tên ứng dụng và sao chép nó. 4. Mở [**App settings**](https://app.adapty.io/settings/android-sdk) từ menu trên cùng của Adapty. <img src="/assets/shared/img/b00066c-package_name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Trong tab **Android SDK** của cửa sổ **App settings**, dán **Package name** vừa sao chép vào. ## Bước 2. Tải lên tệp khóa tài khoản \{#step-2-upload-the-account-key-file\} 1. Tải lên tệp khóa riêng tư của tài khoản dịch vụ ở định dạng JSON mà bạn đã tạo ở bước [Tạo tệp khóa tài khoản dịch vụ](create-service-account) vào khu vực **Service account key file**. <img src="/assets/shared/img/20fdba1-service_key_file.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đừng quên nhấn nút **Save** để xác nhận các thay đổi. **Tiếp theo** - [Bật Real-time developer notifications (RTDN) trong Google Play Console](enable-real-time-developer-notifications-rtdn) --- # File: enable-real-time-developer-notifications-rtdn --- --- title: "Bật thông báo nhà phát triển theo thời gian thực (RTDN) trong Google Play Console" description: "Luôn được thông báo về các sự kiện quan trọng và đảm bảo độ chính xác của dữ liệu bằng cách bật Thông báo nhà phát triển theo thời gian thực (RTDN) trong Google Play Console cho Adapty. Tìm hiểu cách thiết lập RTDN để nhận cập nhật tức thì về hoàn tiền và các sự kiện quan trọng khác từ Play Store" --- Việc thiết lập thông báo nhà phát triển theo thời gian thực (RTDN) rất quan trọng để đảm bảo độ chính xác của dữ liệu, vì nó cho phép bạn nhận cập nhật tức thì từ Play Store, bao gồm thông tin về hoàn tiền và các sự kiện khác. ## Bật thông báo \{#enable-notifications\} 1. Đảm bảo bạn đã bật **Google Cloud Pub/Sub**. Mở [liên kết này](https://console.cloud.google.com/flows/enableapi?apiid=pubsub) và chọn dự án ứng dụng của bạn. Nếu chưa bật **Google Cloud Pub/Sub**, bạn phải thực hiện tại đây. <img src="/assets/shared/img/pubsub.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Vào [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) từ menu trên cùng của Adapty và sao chép nội dung trong trường **Enable Pub/Sub API** bên cạnh tiêu đề **Google Play RTDN topic name**. <img src="/assets/shared/img/a72ff2d-copy_topic.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <p> </p> :::note Nếu nội dung trong trường **Enable Pub/Sub API** có định dạng sai (định dạng đúng bắt đầu bằng `projects/...`), hãy tham khảo phần [Sửa định dạng sai trong trường Enable Pub/Sub API](enable-real-time-developer-notifications-rtdn#fixing-incorrect-format-in-enable-pubsub-api-field) để được hỗ trợ. ::: 3. Mở [Google Play Console](https://play.google.com/console/), chọn ứng dụng của bạn, rồi vào **Monetize with Play** -> **Monetization setup**. Trong phần **Google Play Billing**, chọn hộp kiểm **Enable real-time notifications**. 4. Dán nội dung của trường **Enable Pub/Sub API** mà bạn đã sao chép trong **App Settings** của Adapty vào trường **Topic name**. 5. Nhấp **Save changes** trong Google Play Console. <img src="/assets/shared/img/e55ba0e-paste_topic_name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Kiểm tra thông báo \{#test-notifications\} Để kiểm tra xem bạn đã đăng ký nhận thông báo nhà phát triển theo thời gian thực thành công chưa: 1. Lưu các thay đổi trong cài đặt Google Play Console. 2. Bên dưới **Topic name** trong Google Play Console, nhấp **Send test notification**. <img src="/assets/shared/img/rtdn-test.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Vào [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) trong Adapty. Nếu thông báo kiểm tra đã được gửi, bạn sẽ thấy trạng thái của nó phía trên tên topic. <img src="/assets/shared/img/rtdn-adapty-test.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sửa định dạng sai trong trường Enable Pub/Sub API \{#fixing-incorrect-format-in-enable-pubsub-api-field\} Nếu nội dung trong trường **Enable Pub/Sub API** có định dạng sai (định dạng đúng bắt đầu bằng `projects/...`), hãy làm theo các bước sau để khắc phục sự cố: ### 1. Xác minh việc bật API và phân quyền \{#1-verify-api-enablement-and-permissions\} Hãy đảm bảo cẩn thận rằng tất cả các API cần thiết đã được bật và quyền đã được cấp đúng cho service account. Dù bạn đã hoàn thành các bước này rồi, vẫn nên thực hiện lại để chắc chắn không bỏ sót bước nào. Lặp lại các bước trong các phần sau: 1. [Bật Developer APIs trong Google Play Console](enabling-of-devepoler-api) 2. [Tạo service account trong Google Cloud Console](create-service-account) 3. [Cấp quyền cho service account trong Google Play Console](grant-permissions-to-service-account) 4. [Tạo file khóa service account trong Google Play Console](create-service-account-key-file) 5. [Cấu hình tích hợp Google Play Store](google-play-store-connection-configuration) ### 2. Điều chỉnh chính sách Domain \{#2-adjust-domain-policies\} Thay đổi chính sách **Domain restricted contacts** và **Domain restricted sharing**: 1. Mở [Google Cloud Console](https://console.cloud.google.com/) và chọn dự án mà bạn đã tạo service account để quản lý ứng dụng. 2. Trong phần **Quick Access**, chọn **IAM & Admin**. <img src="/assets/shared/img/google-cloud-IAM-and-Admin.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Ở khung bên trái, chọn **Organization Policies**. 4. Tìm chính sách **Domain restricted contacts**. <img src="/assets/shared/img/google-cloud-policy-action.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp vào nút dấu ba chấm trong cột **Actions** và chọn **Edit policy**. 6. Trong cửa sổ chỉnh sửa chính sách: 1. Dưới **Policy source**, chọn radio button **Override parent's policy**. 2. Dưới **Policy enforcement**, chọn radio button **Replace**. 3. Dưới **Rules**, nhấp nút **ADD A RULE**. <img src="/assets/shared/img/google-cloud-edit-policy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Dưới **New rule** -> **Policy values**, chọn **Allow All**. <img src="/assets/shared/img/google-cloud-allow-all-policy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp **SET POLICY**. 7. Lặp lại các bước 4-6 cho chính sách **Domain restricted sharing**. Cuối cùng, tạo lại nội dung của trường **Enable Pub/Sub API** bên cạnh tiêu đề **Google Play RTDN topic name**. Trường này sẽ có định dạng đúng. Hãy nhớ chuyển **Policy source** trở lại **Inherit parent's policy** cho các chính sách đã cập nhật sau khi bạn đã bật thành công Thông báo nhà phát triển theo thời gian thực (RTDN). ## Chuyển tiếp sự kiện thô \{#raw-events-forwarding\} Đôi khi bạn vẫn muốn nhận các sự kiện S2S thô từ Google. Để tiếp tục nhận chúng khi sử dụng Adapty, chỉ cần thêm endpoint của bạn vào trường **URL for forwarding raw Google events**, và chúng tôi sẽ chuyển tiếp các sự kiện thô nguyên bản từ Google. <img src="/assets/shared/img/e388892-001774-September-22-GhkjOFbT.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- **Tiếp theo** Thiết lập Adapty SDK cho: - [Android](sdk-installation-android) - [React Native](sdk-installation-reactnative) - [Flutter](sdk-installation-flutter) - [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform) - [Unity](sdk-installation-unity) --- # File: stripe --- --- title: "Tích hợp ban đầu với Stripe" description: "Tích hợp Stripe với Adapty để xử lý thanh toán gói đăng ký liền mạch." --- Adapty hỗ trợ luồng gói đăng ký web2app bằng cách theo dõi các khoản thanh toán và gói đăng ký web được thực hiện qua [Stripe](https://stripe.com/). Tích hợp này bao gồm các giao dịch mua được khởi tạo từ web (Stripe Checkout, các trang thanh toán được lưu trữ, hoặc các luồng web tùy chỉnh) và đồng bộ hóa chúng với quyền truy cập ứng dụng di động và phân tích. Tích hợp này hữu ích trong các trường hợp sau: - Tự động cấp quyền truy cập vào các tính năng trả phí cho người dùng đã mua trên web nhưng sau đó cài đặt ứng dụng và đăng nhập vào tài khoản của họ - Có toàn bộ phân tích gói đăng ký trong một Adapty Dashboard duy nhất (bao gồm cohort, dự đoán, và các công cụ phân tích khác của chúng tôi) Mặc dù các giao dịch mua trên web đang ngày càng phổ biến với các ứng dụng, Apple App Store chỉ cho phép hệ thống khác ngoài in-app purchase đối với hàng hóa kỹ thuật số tại Hoa Kỳ. Hãy đảm bảo bạn không quảng bá gói đăng ký web bên trong ứng dụng của mình cho các quốc gia khác. Nếu không, ứng dụng của bạn có thể bị từ chối hoặc bị cấm. Các bước dưới đây mô tả cách cấu hình tích hợp Stripe. :::important Tích hợp này tập trung vào việc theo dõi và đồng bộ hóa các giao dịch mua Stripe trên web. Nếu bạn cần chuyển người dùng từ ứng dụng sang một trang thanh toán web, hãy xem [Web paywalls](web-paywall). ::: ## 1\. Kết nối Stripe với Adapty \{#1-connect-stripe-to-adapty\} Tích hợp này chủ yếu dựa vào việc Adapty lấy dữ liệu gói đăng ký từ Stripe qua webhook. Do đó, bạn cần kết nối tài khoản Adapty của mình với tài khoản Stripe bằng cách cung cấp API Keys và sử dụng URL webhook của Adapty trong Stripe. Để tự động hóa việc cấu hình webhook, hãy cài đặt ứng dụng Adapty trong Stripe: :::note Các bước dưới đây giống nhau cho cả chế độ Production và Test của Stripe, nhưng bạn cần sử dụng các API key khác nhau cho mỗi chế độ. ::: 0. Xác định xem bạn đang kết nối Stripe ở chế độ test hay live. Nếu ban đầu bạn thực hiện ở chế độ test, bạn sẽ cần lặp lại các bước dưới đây cho chế độ live. 1. Truy cập [Stripe App Marketplace](https://marketplace.stripe.com/apps/adapty) và cài đặt ứng dụng Adapty. Lưu ý rằng chế độ sandbox không hỗ trợ cài đặt ứng dụng. Bạn chỉ có thể thực hiện điều này ở chế độ production hoặc test. <img src="/assets/shared/img/stripe1.png"/> 2. Cấp cho ứng dụng các quyền cần thiết. Điều này cho phép Adapty truy cập dữ liệu và lịch sử gói đăng ký. Sau đó, nhấp vào **Continue to app settings** để tiếp tục. Ở cuối pop-up quyền, bạn có thể chọn cài đặt ứng dụng ở chế độ live hay test. <img src="/assets/shared/img/stripe2.png"/> 3. Trong pop-up, tạo một restricted key mới. Bạn sẽ cần xác minh danh tính bằng email, Touch ID, hoặc security key. Sau khi tạo key, bạn sẽ không thể xem lại được nữa, vì vậy hãy lưu trữ an toàn trong trình quản lý mật khẩu hoặc secret store. <img src="/assets/shared/img/stripe4.png"/> 4. Sao chép key đã tạo từ pop-up và truy cập [App Settings → Stripe](https://app.adapty.io/settings/stripe) của Adapty. Dán key vào phần **Stripe App Restricted API Key** tùy theo chế độ của bạn. Lưu ý rằng bạn phải tạo các key khác nhau cho chế độ test và live. <img src="/assets/shared/img/Stripe3.png"/> Xong rồi! Tiếp theo, hãy tạo sản phẩm trên Stripe và thêm chúng vào Adapty. <Details> <summary>Quy trình cài đặt đã lỗi thời</summary> 1. Truy cập [Developers → API Keys](https://dashboard.stripe.com/apikeys) trong Stripe: <img src="/assets/shared/img/6549602-CleanShot_2023-12-06_at_17.29.122x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào nút **Reveal live (test) key** bên cạnh tiêu đề **Secret key**, sao chép nó và truy cập [App Settings → Stripe](https://app.adapty.io/settings/stripe) của Adapty. Dán key vào đây: <img src="/assets/shared/img/2989508-CleanShot_2023-12-07_at_14.59.122x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Tiếp theo, sao chép Webhook URL từ cuối trang tương tự trong Adapty. Truy cập [**Developers** → **Webhooks**](https://dashboard.stripe.com/webhooks) trong Stripe và nhấp vào nút **Add endpoint**: <img src="/assets/shared/img/e7149f5-CleanShot_2023-12-07_at_17.31.392x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Dán webhook URL từ Adapty vào trường **Endpoint URL**. Sau đó chọn **Latest API version** trong trường **Version** của webhook. Tiếp theo chọn các sự kiện sau: - charge.refunded - customer.subscription.created - customer.subscription.deleted - customer.subscription.paused - customer.subscription.resumed - customer.subscription.updated - invoice.created - invoice.updated - payment_intent.succeeded <img src="/assets/shared/img/cbc5404-CleanShot_2023-12-07_at_17.36.232x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấn "Add endpoint" rồi nhấn "Reveal" bên dưới "Signing secret". Đây là key dùng để giải mã dữ liệu webhook phía Adapty, hãy sao chép nó sau khi hiển thị: <img src="/assets/shared/img/0460cbb-CleanShot_2023-12-07_at_17.52.582x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Cuối cùng, dán key này vào App Settings → Stripe của Adapty tại mục "Stripe Webhook Secret": <img src="/assets/shared/img/055db20-CleanShot_2023-12-07_at_14.56.212x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Details> ## 2\. Tạo sản phẩm trên Stripe \{#2-create-products-on-stripe\} :::note Nếu bạn đang thiết lập ở chế độ test, hãy đảm bảo Stripe cũng đang ở chế độ Test trước khi tiếp tục bước này. ::: Truy cập [Product catalog](https://dashboard.stripe.com/products?active=true) của Stripe và tạo các sản phẩm bạn muốn bán cùng với các gói giá của chúng. Lưu ý rằng Stripe cho phép bạn có nhiều gói giá cho mỗi sản phẩm, rất hữu ích để tùy chỉnh ưu đãi mà không cần tạo thêm sản phẩm mới. <img src="/assets/shared/img/b202e2e-CleanShot_2023-12-06_at_15.06.262x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::warning Hiện tại Adapty chỉ hỗ trợ **Flat rate** ($9.99/tháng) hoặc **Package pricing** ($9.99/10 đơn vị), vì chúng hoạt động tương tự như các cửa hàng ứng dụng. Các tùy chọn **Tiered pricing**, **Usage-based fee** và **Customer chooses price** không được hỗ trợ ::: ## 3\. Thêm sản phẩm Stripe vào Adapty \{#3-add-stripe-products-to-adapty\} :::warning Sản phẩm là bắt buộc! Hãy chắc chắn tạo sản phẩm Stripe của bạn trong Adapty Dashboard. Adapty chỉ theo dõi các sự kiện cho các giao dịch được liên kết với những sản phẩm này, vì vậy đừng bỏ qua bước này—nếu không, các sự kiện giao dịch sẽ không được tạo. ::: Chúng tôi xử lý Stripe tương tự như App Store và Google Play: đó chỉ là một cửa hàng khác nơi bạn bán sản phẩm kỹ thuật số. Vì vậy nó được cấu hình tương tự: chỉ cần thêm các sản phẩm Stripe (cụ thể là `product_id` và `price_id` của chúng) vào phần Products của Adapty: <img src="/assets/shared/img/stripe-add-product.webp" style={{ border: 'none', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Product ID trong Stripe trông giống như `prod_...` và price ID trông giống như `price_...`. Chúng khá dễ tìm cho mỗi sản phẩm trong [Product Catalog](https://dashboard.stripe.com/products?active=true) của Stripe, khi bạn mở bất kỳ sản phẩm nào: <img src="/assets/shared/img/14a72d7-CleanShot_2023-12-06_at_17.32.512x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi bạn đã thêm tất cả các sản phẩm cần thiết, bước tiếp theo là cho Stripe biết người dùng nào đang thực hiện giao dịch mua, để Adapty có thể nhận diện được! ## 4\. Bổ sung thông tin người dùng vào giao dịch mua trên web \{#4-enrich-purchases-made-on-the-web-with-your-user-id\} Adapty dựa vào webhooks từ Stripe để cung cấp và cập nhật mức độ truy cập cho người dùng như là nguồn thông tin duy nhất. Nhưng bạn phải cung cấp thông tin bổ sung từ phía mình khi làm việc với Stripe để tích hợp này hoạt động đúng. Để mức độ truy cập nhất quán trên các nền tảng (web hoặc di động), bạn phải đảm bảo có một ID người dùng duy nhất mà Adapty có thể nhận diện từ các webhook. Đây có thể là email, số điện thoại hoặc bất kỳ ID nào khác từ hệ thống xác thực bạn đang sử dụng. Xác định ID bạn muốn dùng để nhận diện người dùng. Sau đó, truy cập phần code khởi tạo thanh toán qua Stripe — và thêm ID người dùng này vào object `metadata` của [Stripe Subscription](https://docs.stripe.com/api/subscriptions/object#subscription_object-metadata) (`sub_...`) hoặc [Checkout Session](https://docs.stripe.com/api/checkout/sessions/create#create_checkout_session-metadata) (`ses_...`) với tên `customer_user_id` như sau: ```json showLineNumbers title="Stripe Metadata contents" {'customer_user_id': "YOUR_USER_ID"} ``` Chỉ cần thêm một dòng đơn giản này là tất cả những gì bạn cần làm trong code. Sau đó, Adapty sẽ phân tích tất cả các webhook nhận được từ Stripe, trích xuất `metadata` này và liên kết chính xác các gói đăng ký với khách hàng của bạn. :::warning User ID là bắt buộc Nếu không có, chúng tôi không có cách nào để khớp người dùng này và cấp cho họ mức độ truy cập trên di động. Nếu bạn không cung cấp `customer_user_id` vào `metadata`, bạn sẽ có tùy chọn để Adapty tìm kiếm `customer_user_id` ở các vị trí khác: `email` từ Customer object của Stripe hoặc `client_reference_id` từ Session của Stripe. Tìm hiểu thêm về cấu hình hành vi tạo hồ sơ người dùng [bên dưới](stripe#profile-creation-behavior) ::: :::note Customer trong Stripe cũng là bắt buộc Nếu bạn đang sử dụng Checkout Sessions, [hãy đảm bảo bạn đang tạo Stripe Customer](https://docs.stripe.com/api/checkout/sessions/create#create_checkout_session-customer_creation) bằng cách đặt `customer_creation` thành `always`. ::: ## 5\. Cấp quyền truy cập cho người dùng trên di động \{#5-provide-access-to-users-on-the-mobile\} Để đảm bảo người dùng di động đến từ web có thể truy cập các tính năng trả phí, chỉ cần gọi `Adapty.activate()` hoặc `Adapty.identify()` với cùng `customer_user_id` bạn đã cung cấp ở bước trước (xem <InlineTooltip tooltip="Xác định người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip> để biết thêm). ## 6\. Kiểm tra tích hợp của bạn \{#6-test-your-integration\} Hãy đảm bảo bạn đã hoàn thành các bước trên cho cả Sandbox và Production. Các giao dịch bạn thực hiện từ chế độ Test của Stripe sẽ được coi là Sandbox trong Adapty. :::info Xong rồi! Người dùng của bạn giờ có thể hoàn tất giao dịch mua trên web và truy cập các tính năng trả phí trong ứng dụng. Và bạn cũng có thể xem toàn bộ phân tích gói đăng ký của mình ở một nơi duy nhất. ::: ## Hành vi tạo hồ sơ người dùng \{#profile-creation-behavior\} Adapty phải liên kết một giao dịch mua với [hồ sơ người dùng](profiles-crm) để nó khả dụng trên di động — vì vậy mặc định nó tạo hồ sơ người dùng khi nhận webhook từ Stripe. Bạn có thể chọn dùng gì làm customer user ID trong Adapty: 1. **Mặc định và khuyến nghị:** `customer_user_id` bạn đã cung cấp trong metadata ở [bước 4 ở trên](stripe#4-enrich-purchases-made-on-the-web-with-your-user-id) 2. `email` trong Customer object của Stripe (xem [tài liệu Stripe](https://docs.stripe.com/api/customers/object#customer_object-email)) 3. `client_reference_id` trong Session object của Stripe (xem [tài liệu Stripe](https://docs.stripe.com/api/checkout/sessions/create#create_checkout_session-client_reference_id)) Bạn có thể cấu hình ID nào bạn muốn sử dụng trong [App Settings → Stripe](https://app.adapty.io/settings/stripe). :::warning **Lưu ý:** nếu một giao dịch cụ thể từ Stripe không chứa ID được chỉ định, chúng tôi sẽ không tạo hồ sơ người dùng. Giao dịch này sẽ vẫn ẩn danh cho đến khi được liên kết với một hồ sơ nào đó (ví dụ: nếu bạn sử dụng [S2S validate](api-adapty/operations/validateStripePurchase) sau đó và thông báo cho chúng tôi về giao dịch này theo cách thủ công). Nó sẽ hiển thị trong Analytics nhưng không trong các phần dựa vào việc đếm hồ sơ người dùng (LTV, Cohorts, Conversions, v.v.) và bạn sẽ không thể xem nó trong Event feed. ::: Bạn cũng có tùy chọn thứ tư là không tạo hồ sơ người dùng nào cả, nhưng điều này không được khuyến nghị do các giới hạn Analytics đã nêu ở trên. ## Giới hạn hiện tại \{#current-limitations\} ### Nâng cấp, hạ cấp và phân bổ phí \{#upgrading-downgrading-and-proration\} Các thay đổi gói đăng ký như nâng cấp hoặc hạ cấp có thể dẫn đến các khoản phí theo tỷ lệ. Adapty sẽ không tính các khoản phí này vào tính toán doanh thu. Tốt nhất là vô hiệu hóa các tùy chọn này theo cách thủ công qua Stripe dashboard. Bạn cũng có thể vô hiệu hóa chúng bằng cách đặt giá trị thuộc tính `proration_behaviour` thành `none` qua Stripe API. ### Hủy gói đăng ký \{#cancellations\} Stripe có hai tùy chọn hủy gói đăng ký: 1. Hủy ngay lập tức: Gói đăng ký hủy ngay lập tức có hoặc không có tùy chọn phân bổ phí 2. Hủy vào cuối kỳ: Gói đăng ký hủy vào cuối kỳ thanh toán hiện tại (tương tự như gói đăng ký trong ứng dụng trên các cửa hàng ứng dụng). Adapty hỗ trợ cả hai tùy chọn, nhưng tính toán doanh thu khi hủy ngay lập tức sẽ bỏ qua tùy chọn phân bổ phí. ### Vấn đề thanh toán và thời gian ân hạn \{#billing-issues-and-grace-period\} Khi khách hàng gặp vấn đề với thanh toán, Adapty sẽ tạo sự kiện billing issue và quyền truy cập sẽ bị thu hồi. Chúng tôi chưa hỗ trợ Grace Period của Stripe — điều này sẽ là một phần của các bản phát hành trong tương lai. ### Hoàn tiền \{#refunds\} Adapty chỉ theo dõi hoàn tiền toàn bộ. Hoàn tiền theo tỷ lệ hoặc một phần hiện không được hỗ trợ. ### Tính duy nhất của Transaction ID \{#transaction-id-uniqueness\} Adapty khớp hồ sơ người dùng và giao dịch bằng `store_transaction_id` và `store_original_transaction_id`. Những giá trị này **phải là duy nhất** trên các môi trường Test và Production. #### Tại sao điều này quan trọng \{#why-this-matters\} Nếu cùng một transaction ID tồn tại trong cả hai môi trường, Adapty xử lý chúng như một giao dịch, gây ra: - Giao dịch mua Production kế thừa mức độ truy cập và product ID từ Test - Product ID và môi trường sai trong phản hồi API - Liên kết hồ sơ người dùng và sự kiện gói đăng ký bị gián đoạn #### Cách đảm bảo tính duy nhất \{#how-to-ensure-uniqueness\} Invoice ID của Stripe có thể trùng lặp giữa môi trường Test và Live. Để ngăn xung đột giữa các môi trường, hãy chọn một trong các cách sau #### Tùy chọn 1: Đánh số theo tài khoản với tiền tố môi trường \{#option-1-account-level-numbering-with-environment-prefixes\} Cấu hình tiền tố riêng cho mỗi môi trường: 1. Trong Stripe Dashboard, chuyển sang chế độ Test. 2. Truy cập [Settings → Billing → Invoices](https://dashboard.stripe.com/settings/account/?support_details=true). 3. Đặt **Invoice numbering** thành **Sequentially across your account**. 4. Đặt **Invoice prefix** thành TEST- (hoặc tiền tố khác dành riêng cho môi trường test). 5. Chuyển sang chế độ Live và lặp lại các bước 2-4, sử dụng LIVE- (hoặc tiền tố khác dành riêng cho môi trường live) làm tiền tố #### Tùy chọn 2: Đánh số theo khách hàng \{#option-2-customer-level-numbering\} Đặt **Invoice numbering** trong [**Stripe settings** -> **Billing** -> **Invoices** tab](https://dashboard.stripe.com/settings/account/?support_details=true) thành **Sequentially for each customer (customer-level)**. Ngay cả với cấu hình trên, nếu bạn xóa một invoice, Stripe có thể tái sử dụng ID đó cho các invoice mới của cùng khách hàng. Tốt nhất là tránh xóa invoice khi có thể. ### Giao dịch mua một lần qua Stripe Checkout hoặc Payment Links \{#one-time-purchases-via-stripe-checkout-or-payment-links\} Adapty theo dõi các giao dịch mua một lần (không phải gói đăng ký) được thực hiện qua Stripe Checkout (`mode=payment`) hoặc Payment Links chỉ khi Stripe tạo invoice cho giao dịch. Theo mặc định, Stripe không tạo invoice cho các giao dịch mua Checkout một lần. Trong trường hợp này, `payment_intent.succeeded` đến mà không có dữ liệu invoice, không đủ để Adapty ghi lại giao dịch. Để theo dõi các giao dịch mua Checkout một lần trong Adapty, [hãy bật tính năng tạo invoice](https://docs.stripe.com/payments/checkout/receipts?payment-ui=stripe-hosted#paid-invoices-hosted) khi bạn tạo session. Stripe sau đó sẽ tạo invoice và phát ra các sự kiện `invoice.created` và `invoice.updated` liên quan, mà Adapty xử lý để ghi lại giao dịch. ## Khai thác tối đa dữ liệu Stripe của bạn \{#get-more-from-your-stripe-data\} Sau khi tích hợp với Stripe, Adapty sẵn sàng cung cấp thông tin chi tiết ngay lập tức. Để tận dụng tối đa dữ liệu Stripe của bạn, bạn có thể thiết lập thêm các tích hợp Adapty để chuyển tiếp các sự kiện Stripe — đưa toàn bộ phân tích gói đăng ký vào một Adapty Dashboard duy nhất. :::tip Để phân tích nâng cao, bạn có thể thêm `variation_id` vào metadata Stripe để gán giao dịch mua cho các phiên bản paywall cụ thể. Điều này đặc biệt hữu ích khi triển khai các web paywall tự xây dựng mà bạn muốn theo dõi paywall nào đã dẫn đến chuyển đổi. Lưu ý rằng `variation_id` chỉ được đọc từ metadata trong các object Stripe Subscription (`sub_...`) và Checkout Session (`ses_...`): ```json showLineNumbers title="Stripe Metadata with variation_id" { 'customer_user_id': "YOUR_USER_ID", 'variation_id': "YOUR_VARIATION_ID" } ``` ::: Các tích hợp bạn có thể sử dụng để chuyển tiếp và phân tích các sự kiện Stripe: - [Amplitude](amplitude/) - [Webhook](webhook) - [Firebase](firebase-and-google-analytics) - [Mixpanel](mixpanel) - [Posthog](posthog) ### Các sự kiện Stripe được hỗ trợ \{#supported-stripe-events\} Adapty hỗ trợ các sự kiện Stripe sau: - charge.refunded - customer.subscription.created - customer.subscription.deleted - customer.subscription.paused - customer.subscription.resumed - customer.subscription.updated - invoice.created - invoice.updated - payment_intent.succeeded --- # File: paddle --- --- title: "Tích hợp ban đầu với Paddle" description: "Tích hợp Paddle với Adapty để xử lý thanh toán gói đăng ký liền mạch." --- Adapty hỗ trợ flow web2app bằng cách theo dõi các giao dịch thanh toán và gói đăng ký được thực hiện qua [Paddle](https://www.paddle.com/). Tích hợp này bao gồm các giao dịch mua bắt đầu từ web và đồng bộ chúng với quyền truy cập ứng dụng di động và analytics, song song với in-app purchase từ các cửa hàng ứng dụng. Tích hợp này hữu ích trong các trường hợp sau: - Thu thập dữ liệu gói đăng ký từ cả in-app purchase lẫn giao dịch mua trên website vào một hệ thống duy nhất - Cấp quyền truy cập các tính năng trả phí trong ứng dụng di động cho người dùng đã mua trên website - Xem analytics và dữ liệu gói đăng ký từ tất cả kênh bán hàng trên một dashboard :::note Apple hiện cho phép các ứng dụng trên US App Store đặt liên kết đến các hệ thống thanh toán bên ngoài, dù các ứng dụng vẫn có thể cần cung cấp in-app purchase song song với các tùy chọn bên ngoài. Hãy kiểm tra các nguyên tắc App Store hiện hành cho khu vực và danh mục ứng dụng của bạn. ::: :::note Tích hợp này tập trung vào việc theo dõi và đồng bộ giao dịch mua Paddle từ web. Nếu bạn cần chuyển hướng người dùng từ ứng dụng sang trang thanh toán web, hãy sử dụng [web paywalls](web-paywall) của Adapty. ::: Để thiết lập tích hợp Paddle, hãy làm theo các bước sau: ## 1\. Kết nối Paddle với Adapty \{#1-connect-paddle-to-adapty\} Tích hợp sử dụng webhook để gửi dữ liệu gói đăng ký từ Paddle sang Adapty. Để kết nối tài khoản Adapty và Paddle, bạn cần: 1. Cung cấp API key của Paddle. 2. Thêm URL webhook của Adapty vào Paddle. :::note Các bước dưới đây áp dụng cho cả môi trường Production lẫn Test. Bạn có thể cấu hình cả hai cùng lúc. Các liên kết được cung cấp dành cho môi trường Production — để lấy liên kết môi trường Test, chỉ cần thêm `sandbox-` vào đầu mỗi URL. Ví dụ: dùng `https://sandbox-vendors.paddle.com/authentication-v2` thay vì `https://vendors.paddle.com/authentication-v2`. ::: ### 1.1. Lấy và thêm API key của Paddle \{#11-get-and-add-paddle-api-keys\} 1. Trong Paddle, vào [Developer Tools → Authentication](https://vendors.paddle.com/authentication-v2) và nhấp **New API key**. <img src="/assets/shared/img/paddle-new-key.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Đặt tên cho key và thiết lập ngày hết hạn. Để API key hoạt động với Adapty, bạn cần cấp quyền **Read** cho tất cả các đối tượng. Nhấp **Save**. <img src="/assets/shared/img/paddle-key.webp" style={{ border: 'none', /* border width and color */ width: '300px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp **Copy key**. <img src="/assets/shared/img/copy-paddle-key.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trong Adapty, vào [App Settings → Paddle](https://app.adapty.io/settings/paddle) và dán key vào phần **Paddle API key**. :::warning Nếu bạn đặt ngày hết hạn cho Paddle API key, bạn phải tự tạo key mới và cập nhật trong Adapty trước khi key hết hạn. Tích hợp sẽ ngừng hoạt động mà không có cảnh báo khi key hết hạn, và người dùng sẽ không thể thực hiện giao dịch mua. ::: <img src="/assets/shared/img/paddle-api-keys-adapty.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### 1.2. Thêm các sự kiện sẽ được gửi đến Adapty \{#12-add-events-that-will-be-sent-to-adapty\} 1. Sao chép **Webhook URL** từ trang **Paddle** tương ứng trong Adapty. 2. Trong Paddle, vào [**Developer Tools → Notifications**](https://vendors.paddle.com/notifications-v2) và nhấp **New destination** để thêm webhook. <img src="/assets/shared/img/paddle-webhook.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhập tên mô tả cho webhook. Chúng tôi khuyến nghị đặt tên có chứa "Adapty" để dễ tìm khi cần. 4. Dán **Webhook URL** từ Adapty vào trường **URL**. Đảm bảo bạn đang dùng webhook đúng với môi trường. 5. Đặt **Notification type** thành **Webhook**. <img src="/assets/shared/img/paddle-create-webhook.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Chọn các sự kiện sau: - `subscription.created` - `subscription.updated` - `transaction.created` - `transaction.updated` - `adjustment.created` - `adjustment.updated` <img src="/assets/shared/img/paddle_events.png" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Nhấp **Save destination** để hoàn tất thiết lập webhook. ### 1.3. Lấy và thêm secret key của webhook \{#13-retrieve-and-add-the-webhook-secret-key\} 1. Trong cửa sổ **Notifications**, nhấp vào ba dấu chấm bên cạnh webhook vừa tạo và chọn **Edit destination**. 2. Một trường mới tên **Secret key** sẽ xuất hiện trong bảng **Edit destination**. Sao chép nó. <img src="/assets/shared/img/paddle-webhook-secret-key-copy.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong Adapty, vào [App Settings → Paddle](https://app.adapty.io/settings/paddle) và dán key vào trường **Notification secret key**. Key này được dùng để xác minh dữ liệu webhook trong Adapty. <img src="/assets/shared/img/paddle-webhook-secret-key.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### 1.4. Liên kết khách hàng Paddle với hồ sơ người dùng Adapty \{#14-match-paddle-customers-with-adapty-profiles\} Adapty cần liên kết mỗi giao dịch mua với một [hồ sơ người dùng](profiles-crm) để có thể sử dụng trong ứng dụng. Theo mặc định, hồ sơ người dùng được tạo tự động khi Adapty nhận webhook từ Paddle. Bạn có thể chọn giá trị nào sẽ được dùng làm `customer_user_id` trong Adapty: 1. **Mặc định và khuyến nghị:** `customer_user_id` bạn truyền vào trường `custom_data` (xem [tài liệu Paddle](https://developer.paddle.com/build/transactions/custom-data)) 2. `email` từ đối tượng Paddle Customer (xem [tài liệu Paddle](https://developer.paddle.com/paddle-js/methods/paddle-checkout-open/#parameters)) 3. Paddle Customer ID theo định dạng `ctm-...` (xem [tài liệu Paddle](https://developer.paddle.com/paddle-js/methods/paddle-checkout-open/#parameters)) 4. Không tạo hồ sơ người dùng. Chọn tùy chọn này nếu bạn muốn kiểm soát hồ sơ người dùng nhiều hơn và tự xử lý. Bạn có thể cấu hình giá trị nào sẽ được sử dụng trong trường **Profile creation behavior** tại [App Settings → Paddle](https://app.adapty.io/settings/paddle). <img src="/assets/shared/img/paddle-users.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## 2. Thêm sản phẩm Paddle vào Adapty \{#2-add-paddle-products-to-adapty\} :::warning Hãy nhớ thêm sản phẩm Paddle của bạn vào Adapty Dashboard hoặc thêm Paddle product ID vào các sản phẩm hiện có. Adapty chỉ theo dõi sự kiện cho các giao dịch gắn với những sản phẩm này. Nếu bỏ qua bước này, sự kiện giao dịch sẽ không được tạo. ::: Paddle hoạt động trong Adapty giống như App Store và Google Play — đây là một nền tảng khác để bạn bán sản phẩm kỹ thuật số. Để cấu hình, hãy thêm các giá trị `product_id` và `price_id` tương ứng từ Paddle trong phần [Products](https://app.adapty.io/products) trong Adapty. <img src="/assets/shared/img/paddle-create-product.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trong Paddle, product ID có dạng `pro_...` và price ID có dạng `pri_...`. Bạn có thể tìm thấy chúng trong [danh mục sản phẩm Paddle](https://vendors.paddle.com/products-v2) khi mở một sản phẩm cụ thể: <img src="/assets/shared/img/paddle-product-price.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi đã thêm sản phẩm, bước tiếp theo là đảm bảo Adapty có thể liên kết giao dịch mua với đúng người dùng. ## 3\. Cấp quyền truy cập cho người dùng trên thiết bị di động \{#3-provide-access-to-users-on-the-mobile\} Để đảm bảo người dùng mua hàng trên web có thể truy cập trên thiết bị di động, hãy gọi `Adapty.activate()` hoặc `Adapty.identify()` với cùng `customer_user_id` mà bạn đã truyền khi thực hiện giao dịch mua. Xem [Identifying users](identifying-users) để biết thêm chi tiết. ## 4\. Kiểm tra tích hợp \{#4-test-your-integration\} Sau khi thiết lập xong, bạn có thể kiểm tra tích hợp. Các giao dịch thực hiện trong môi trường Test của Paddle sẽ xuất hiện dưới dạng **Test** trong Adapty. Các giao dịch từ môi trường Production sẽ xuất hiện dưới dạng **Production**. Tích hợp của bạn đã hoàn tất. Người dùng có thể mua gói đăng ký trên website và tự động truy cập các tính năng cao cấp trong ứng dụng di động, trong khi bạn theo dõi toàn bộ analytics gói đăng ký từ Adapty Dashboard thống nhất. ## Những lưu ý quan trọng \{#important-considerations\} - Trong analytics của Adapty, số tiền giao dịch bao gồm thuế và phí Paddle, khác với dashboard của Paddle nơi số tiền được hiển thị sau khi trừ thuế và phí. Điều này có nghĩa là các con số bạn thấy trong Adapty sẽ cao hơn so với dashboard Paddle của bạn. - Không giống các cửa hàng khác, hoàn tiền trong Paddle chỉ ảnh hưởng đến giao dịch cụ thể được hoàn tiền và không tự động hủy gói đăng ký. Gói đăng ký sẽ tiếp tục hoạt động trừ khi được hủy rõ ràng. - Bạn cũng có thể thêm `variation_id` vào trường `custom_data` để quy kết giao dịch mua cho các phiên bản paywall cụ thể. Adapty sẽ xử lý dữ liệu này từ webhook và đưa vào analytics. ### Dùng thử có tính phí \{#paid-trials\} Khi làm việc với dùng thử có tính phí trong Paddle, bạn cần tạo hai sản phẩm trong Adapty: 1. Tạo một sản phẩm mua một lần và liên kết với price Paddle tính phí cho giai đoạn dùng thử. 2. Sau đó tạo một sản phẩm gói đăng ký (Hàng tháng/Hàng tuần/v.v.) và liên kết với price Paddle có thành phần dùng thử miễn phí. Từ góc độ Paddle, đây là một sản phẩm với hai price trong một giao dịch — một price cho khoản phí dùng thử (ví dụ: $0,99) và một price cho giai đoạn dùng thử miễn phí ($0,00). Từ góc độ Adapty, điều này tạo ra hai sự kiện riêng biệt: một giao dịch mua một lần cho khoản thanh toán dùng thử và một sự kiện bắt đầu dùng thử cho sản phẩm gói đăng ký. Ví dụ: khi người dùng bắt đầu dùng thử có tính phí $0,99 cho gói đăng ký $9,99/tháng, Paddle tạo một giao dịch với cả hai price, trong khi Adapty xử lý đây là một giao dịch mua một lần $0,99 (thanh toán ngay) và một sự kiện bắt đầu dùng thử $0,00 (gói đăng ký trong tương lai $9,99/tháng). :::note Khi người dùng hủy dùng thử có tính phí, bạn nhận được các sự kiện **Trial expired** và **Trial renewal canceled**. ::: ## Khai thác tối đa dữ liệu Paddle \{#get-more-from-your-paddle-data\} :::important Để các sự kiện Paddle hoạt động với các tích hợp, người dùng của bạn phải đăng nhập vào ứng dụng bằng tài khoản App Store/Google Play ít nhất một lần. ::: Sau khi tích hợp với Paddle, Adapty sẵn sàng cung cấp thông tin phân tích ngay lập tức. Để tận dụng tối đa dữ liệu Paddle, bạn có thể thiết lập thêm các tích hợp Adapty để chuyển tiếp sự kiện Paddle — tập hợp toàn bộ analytics gói đăng ký vào một Adapty Dashboard duy nhất. Các tích hợp bạn có thể dùng để chuyển tiếp và phân tích sự kiện Paddle: - [AppsFlyer](appsflyer) - [Webhook](webhook) - [Posthog](posthog) ## Các hạn chế hiện tại \{#current-limitations\} - **Hủy gói đăng ký**: Paddle có hai tùy chọn hủy gói đăng ký: 1. Hủy ngay lập tức: Gói đăng ký bị hủy ngay. 2. Hủy vào cuối kỳ: Gói đăng ký hủy vào cuối chu kỳ thanh toán hiện tại (tương tự gói đăng ký trong ứng dụng trên các cửa hàng ứng dụng). - **Hoàn tiền**: Adapty theo dõi hoàn tiền toàn phần và một phần. - **Thời gian ân hạn**: Theo mặc định, Paddle áp dụng thời gian ân hạn cố định 30 ngày cho các sự cố thanh toán, trong thời gian đó gói đăng ký vẫn còn hoạt động. Bạn có thể [tùy chỉnh thời gian ân hạn và hành động sau khi kết thúc (tạm dừng hoặc hủy gói đăng ký)](https://developer.paddle.com/build/retain/configure-payment-recovery-dunning#prerequisites). **Dùng thử**: Nếu việc thu tiền thất bại sau khi dùng thử kết thúc, trạng thái gói đăng ký sẽ chuyển sang `past_due`. Trong môi trường production, Retain của Paddle áp dụng cửa sổ dunning để thử lại thanh toán trước khi hủy hoặc tạm dừng gói đăng ký. Trong sandbox, Retain không khả dụng, do đó không có lần thử lại thanh toán nào được thực hiện và gói đăng ký sẽ ở trạng thái `past_due` vô thời hạn. --- **Xem thêm:** - [Xác thực giao dịch mua trong Paddle, lấy mức độ truy cập và import lịch sử giao dịch từ Paddle bằng server-side API](api-adapty/operations/validatePaddlePurchase) --- # File: custom-store --- --- title: "Tích hợp ban đầu với các cửa hàng khác" description: "Tích hợp ban đầu của Adapty với App Store: Hướng dẫn nhanh" --- Chào mừng bạn đến với Adapty! Chúng tôi luôn ưu tiên giúp bạn bắt đầu nhanh chóng và đạt được kết quả tốt nhất cho ứng dụng của mình. Việc tích hợp ban đầu chỉ cần thiết với [App Store](initial_ios), [Google Play](initial-android), [Stripe](stripe) và [Paddle](paddle) vì Adapty xác minh ứng dụng, sản phẩm và ưu đãi của bạn với các cửa hàng này. Adapty không xác thực dữ liệu với các cửa hàng ứng dụng khác và không xử lý các giao dịch mua hàng thực hiện qua chúng. Tuy nhiên, bạn vẫn có thể đánh dấu các sản phẩm được bán qua các cửa hàng khác để Adapty cấp quyền truy cập nội dung có phí sau khi mua thành công, phản ánh giao dịch trong analytics và chia sẻ qua các tích hợp. <img src="/assets/shared/img/Adapty-Communication-Scheme.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <p> </p> :::important Hãy đảm bảo backend của bạn xử lý giao dịch mua hàng và gửi transaction đến Adapty thông qua [Adapty server-side API](getting-started-with-server-side-api). Adapty chỉ cấp quyền truy cập, kích hoạt sự kiện giao dịch, gửi đến các tích hợp và phản ánh trong analytics sau khi nhận được transaction. ::: Để đánh dấu một sản phẩm là được bán qua cửa hàng ứng dụng tùy chỉnh, hãy chọn cửa hàng ứng dụng khi tạo sản phẩm. Nếu cửa hàng bạn cần chưa có trong danh sách, đây là cách tạo mới: 1. Trên trang **Products**, mở sản phẩm bạn muốn bán qua cửa hàng ứng dụng tùy chỉnh. 2. Chọn cửa hàng ứng dụng bạn muốn bán qua. Nếu chưa có trong danh sách, nhấp vào nút **Create Custom Store**. <img src="/assets/shared/img/create_custom-appstore.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhập **Title** và **Store ID** của cửa hàng. 4. Nhấp vào nút **Create store**. Nếu backend của bạn được thiết lập đúng, Adapty sẽ nhận các giao dịch sản phẩm từ cửa hàng tùy chỉnh này, phản ánh chúng trong analytics, **Event Feed**, và [các tích hợp](https://app.adapty.io/integrations), đồng thời cấp quyền truy cập tương ứng. ## Khai thác tối đa dữ liệu từ cửa hàng tùy chỉnh \{#get-more-from-your-custom-store-data\} :::important Để các sự kiện từ cửa hàng tùy chỉnh của bạn hoạt động với các tích hợp, người dùng phải đăng nhập vào ứng dụng bằng tài khoản App Store/Google Play của họ ít nhất một lần. ::: Sau khi thiết lập tích hợp cửa hàng tùy chỉnh, Adapty sẵn sàng cung cấp thông tin chi tiết ngay lập tức. Để tận dụng tối đa dữ liệu của bạn, bạn có thể thiết lập thêm các tích hợp Adapty để chuyển tiếp các sự kiện từ cửa hàng tùy chỉnh — tập hợp toàn bộ analytics gói đăng ký vào một Adapty Dashboard duy nhất. Các tích hợp bạn có thể sử dụng để chuyển tiếp và phân tích sự kiện từ cửa hàng tùy chỉnh: - [AppsFlyer](appsflyer) - [Webhook](webhook) - [Posthog](posthog) --- # File: transfer-apps --- --- title: "Chuyển ứng dụng sang tài khoản khác" description: "Thay đổi chủ sở hữu ứng dụng trong Adapty" --- Chuyển ứng dụng sang chủ sở hữu khác khi công ty bạn được mua lại, bạn đang bán ứng dụng, hoặc tái cơ cấu các thực thể kinh doanh. Quá trình chuyển giao liên quan đến việc phối hợp thay đổi trong Adapty, App Store Connect và Google Play Console để đảm bảo dịch vụ không bị gián đoạn. ## Chuyển quyền sở hữu ứng dụng \{#transfer-app-ownership\} Hoàn tất việc chuyển giao trên cửa hàng trước, sau đó mới chuyển ứng dụng trong Adapty. Thứ tự này đảm bảo các giao dịch mua vẫn hoạt động xuyên suốt quá trình chuyển giao. :::note Không xóa hoặc tạo lại sản phẩm trong quá trình chuyển giao. Không thay đổi ID sản phẩm cho đến khi xác nhận việc chuyển giao đã hoàn tất. ::: ### Chuyển giao App Store (iOS) \{#app-store-ios-transfer\} :::important Khóa API App Store Connect (Issuer ID, Key ID, file .p8) được gắn với tài khoản, không phải với ứng dụng. Sau khi chuyển giao, bạn phải tạo khóa API mới từ tài khoản của chủ sở hữu mới và cập nhật chúng trong Adapty. App-specific shared secret vẫn tiếp tục xác thực receipt trong thời gian chuyển giao, nhưng chủ sở hữu mới cũng phải tạo lại và cập nhật nó trong Adapty sau khi hoàn tất chuyển giao. ::: 1. **Chủ sở hữu mới:** Tạo tài khoản Adapty tại [app.adapty.io](https://app.adapty.io) nếu bạn chưa có. 2. **Chủ sở hữu cũ:** Khởi tạo quá trình chuyển giao ứng dụng trong App Store Connect theo [hướng dẫn chuyển giao](https://developer.apple.com/help/app-store-connect/transfer-an-app/overview-of-app-transfer) của Apple. 3. **Chủ sở hữu mới:** Chấp nhận yêu cầu chuyển giao trong App Store Connect. 4. **Chủ sở hữu cũ:** Gửi email đến [support@adapty.io](mailto:support@adapty.io) để chuyển ứng dụng trong Adapty. Cung cấp tên ứng dụng và địa chỉ email của chủ sở hữu mới. 5. **Chủ sở hữu mới:** Sau khi nhận ứng dụng trong Adapty, hoàn tất [hướng dẫn tích hợp App Store](initial_ios) để tạo và cấu hình tất cả thông tin xác thực dưới tài khoản của bạn. ### Chuyển giao Google Play (Android) \{#google-play-android-transfer\} 1. **Chủ sở hữu mới:** Tạo tài khoản Adapty tại [app.adapty.io](https://app.adapty.io) nếu bạn chưa có. 2. **Cả hai chủ sở hữu:** Đảm bảo cả hai tài khoản Google Play Developer đều đã đăng ký đầy đủ. 3. **Chủ sở hữu cũ:** Gửi yêu cầu chuyển giao qua Google Play Console hoặc Google Play Developer Support. Google có thể yêu cầu thêm tài liệu như số DUNS, hợp đồng hoặc bằng chứng bán hàng. 4. **Chủ sở hữu mới:** Xem xét và phê duyệt yêu cầu chuyển giao. 5. **Google:** Nhóm hỗ trợ của Google xử lý việc chuyển giao, thường trong vài ngày làm việc, nhưng có thể lâu hơn tùy thuộc vào xác minh tài khoản, độ phức tạp của gói đăng ký và thiết lập thanh toán. 6. **Chủ sở hữu cũ:** Sau khi Google hoàn tất chuyển giao, gửi email đến [support@adapty.io](mailto:support@adapty.io) để chuyển ứng dụng trong Adapty. Cung cấp tên ứng dụng và địa chỉ email của chủ sở hữu mới. 7. **Chủ sở hữu mới:** Sau khi nhận ứng dụng trong Adapty, hoàn tất [hướng dẫn tích hợp Google Play](initial-android) để tạo và cấu hình tất cả thông tin xác thực dưới tài khoản của bạn. Quá trình chuyển giao bao gồm người dùng, gói đăng ký, thống kê, xếp hạng và thông tin trang cửa hàng. Tính liên tục thanh toán được duy trì cho các thuê bao hiện tại, nhưng các khoản thanh toán sẽ chuyển sang tài khoản merchant của chủ sở hữu mới chỉ sau khi hoàn tất chuyển giao. Báo cáo thanh toán và đơn hàng trước khi chuyển giao vẫn nằm trong tài khoản gốc. Theo dõi [hướng dẫn chuyển giao](https://support.google.com/googleplay/android-developer/answer/6230247) của Google để biết các yêu cầu chi tiết. ## Giảm thiểu rủi ro và thời điểm thực hiện \{#risk-mitigation-and-timing\} **Những gì vẫn hoạt động trong quá trình chuyển giao:** - Giao dịch mua và gia hạn (app-specific shared secret vẫn tiếp tục xác thực receipt trong thời gian chuyển giao) - Quyền truy cập của các thuê bao hiện tại - SDK vẫn hoạt động bình thường **Những gì tạm thời ngừng hoạt động:** - Các lệnh gọi API App Store Connect (cho đến khi cấu hình khóa mới) - Thông báo từ server (cho đến khi cấu hình lại endpoint) - Analytics có thể bị gián đoạn trong quá trình chuyển đổi thông tin xác thực **Thời điểm khuyến nghị:** - Thực hiện chuyển giao trong các khung giờ ít truy cập (3 giờ sáng - 6 giờ sáng theo múi giờ chính của người dùng) - Chủ sở hữu mới sẵn sàng cấu hình thông tin xác thực ngay sau khi chấp nhận chuyển giao trên cửa hàng - Dự trù 15-30 phút giữa thời điểm chấp nhận chuyển giao và hoàn tất tích hợp Adapty **Sau khi hoàn tất chuyển giao:** - Kiểm tra xác thực receipt ngay lập tức - Theo dõi tỷ lệ gia hạn tự động trong 48 giờ - Xác minh thông báo từ server đang đến được hệ thống của bạn - Kiểm tra xem các giao dịch mua mới có đang được theo dõi chính xác không ## Xác minh chuyển giao đã hoàn tất thành công \{#verify-transfer-completed-successfully\} Sau khi hoàn tất cả việc chuyển giao trong Adapty và trên cửa hàng: 1. **Kiểm tra quyền truy cập Dashboard**: Chủ sở hữu mới sẽ thấy ứng dụng trong Adapty Dashboard của họ. 2. **Xác minh kết nối khóa API**: Kiểm tra xem App Store Connect API Key mới hoặc service account Google Play có kết nối thành công trong Adapty không. 3. **Kiểm tra kết nối SDK**: Chạy ứng dụng của bạn và xác minh SDK Adapty khởi tạo mà không có lỗi. --- # File: installation-of-adapty-sdks --- --- title: "Cài đặt Adapty SDK" description: "Cài đặt Adapty SDK cho iOS, Android và các ứng dụng đa nền tảng." --- Bạn có ba cách để bắt đầu tùy theo sở thích: - **Theo dõi hướng dẫn quickstart theo từng nền tảng**: Các hướng dẫn chứa các đoạn code sẵn sàng cho môi trường production, nên việc triển khai không mất nhiều thời gian. - [iOS](ios-sdk-overview) - [Android](android-sdk-overview) - [React Native](react-native-sdk-overview) - [Flutter](flutter-sdk-overview) - [Unity](unity-sdk-overview) - [Kotlin Multiplatform](kmp-sdk-overview) - [Capacitor](capacitor-sdk-overview) - **Dùng LLM**: Tài liệu của chúng tôi thân thiện với LLM. Đọc [hướng dẫn](adapty-cursor) của chúng tôi để tận dụng tối đa LLM với tài liệu Adapty. - **Khám phá các ứng dụng mẫu**: - [iOS (Swift)](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples) - [Android (Kotlin)](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app) - [React Native (Ví dụ cơ bản trên pure RN)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/BasicExample) - [React Native (Ví dụ nâng cao – hữu ích cho việc phát triển, cho phép bạn làm việc với các trường hợp phức tạp hơn)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/AdaptyDevtools) - [React Native (Expo dev build)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo) - [React Native (Expo Go & Web)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/ExpoGoWebMock) - [Flutter (Dart)](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example) - [Unity (C#)](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Assets) - [Kotlin Multiplatform](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example) - [Capacitor](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples) --- # File: sample-apps --- --- title: "Ứng dụng mẫu" description: "" --- Để giúp bạn bắt đầu với Adapty SDK, chúng tôi đã chuẩn bị các ứng dụng mẫu minh họa cách tích hợp và sử dụng các tính năng chính. Những ứng dụng này cung cấp sẵn các triển khai về paywall, mua hàng và theo dõi analytics. <img src="/assets/shared/img/adapty-scheme.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Tại sao nên dùng ứng dụng mẫu? \{#why-use-sample-apps\} - **Tích hợp nhanh:** Xem cách Adapty SDK hoạt động trong một ứng dụng thực tế. - **Thực hành tốt nhất:** Làm theo các mô hình triển khai được khuyến nghị. - **Gỡ lỗi & Kiểm thử:** Dùng ứng dụng mẫu để khắc phục sự cố và thử nghiệm trước khi tích hợp Adapty vào dự án của bạn. ## Các ứng dụng mẫu hiện có \{#available-sample-apps\} - [iOS (Swift)](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples) - [Android (Kotlin)](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app) - [React Native (Ví dụ cơ bản trên RN thuần)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/BasicExample) - [React Native (Ví dụ nâng cao – hữu ích cho việc phát triển, cho phép làm việc với các trường hợp phức tạp hơn)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/AdaptyDevtools) - [React Native (Expo dev build)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo) - [React Native (Expo Go & Web)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/ExpoGoWebMock) - [Flutter (Dart)](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example) - [Unity (C#)](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Assets) - [Kotlin Multiplatform](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example) - [Capacitor (React)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-react-example) - [Capacitor (Vue.js)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-vue-example) - [Capacitor (Angular)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-angular-example) - [Capacitor (Công cụ phát triển nâng cao)](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/adapty-devtools) --- # File: paywall-builder-templates --- --- title: "Tạo một flow" description: "Bắt đầu một flow mới từ template được thiết kế sẵn trong thư viện hoặc từ template tối giản." --- Bạn có thể tạo một flow từ template hoặc từ đầu. :::link Muốn tìm hiểu thêm về cách xây dựng flow? Xem các video hướng dẫn từng bước trong [danh sách phát YouTube](https://www.youtube.com/playlist?list=PLMksWqaZiWtM) của chúng tôi. ::: ## Tạo flow \{#create-flow\} 1. Mở trang **Flows**. 2. Nhấn **Create flow**. 3. Chọn một tùy chọn: - **Browse templates** (mở thư viện mẫu) - **Start from scratch** (tạo flow trống) 4. Đổi tên flow trong trình chỉnh sửa. Nhấn vào tên flow trên thanh tiêu đề và nhập tên mới. :::warning Adapty cho phép đặt tên flow trùng nhau. Hãy đổi tên cho mỗi flow mới, nếu không bạn sẽ tạo ra nhiều flow **Untitled** khó phân biệt. ::: ### Sử dụng template \{#use-a-template\} Template library chứa nhiều template đóng vai trò là điểm khởi đầu cho flow của bạn. Mỗi template là một flow hoàn chỉnh với nhiều màn hình, các phần tử tương tác và điều hướng hoạt động sẵn. Bạn có thể chỉnh sửa bất kỳ phần tử nào để tùy chỉnh theo ý muốn. Để áp dụng một template: 1. Trong Template library, duyệt qua các thẻ template. Mỗi thẻ hiển thị ảnh chụp màn hình xem trước từ một flow. 2. Nhấp **Use as template** trên thẻ bạn muốn chọn. Template sẽ được tải trong builder. Từ đây, bạn có thể thay đổi bất kỳ phần tử, màn hình hoặc thuộc tính nào. ### Bắt đầu từ đầu \{#start-from-scratch\} Bắt đầu từ đầu tạo ra một flow với một màn hình trắng duy nhất. Thiết kế màn hình bằng các thành phần từ [thư viện thành phần](builder-elements). ## Thay đổi template \{#change-the-template\} Bạn có thể đổi template ngay trong builder. Mở bảng Screens và nhấn nút **Templates** Templates để mở lại thư viện Template, sau đó chọn template mới. :::warning Khi áp dụng template mới, bản nháp flow hiện tại của bạn sẽ bị thay thế. Adapty sẽ yêu cầu xác nhận — nhấn **Use template** để tiếp tục, hoặc **Cancel** để giữ lại bản nháp. Sau khi xác nhận, bản nháp cũ không thể khôi phục được. Flow đã publish vẫn hoạt động bình thường và không bị ảnh hưởng. ::: ## Phông chữ tùy chỉnh trong template \{#custom-fonts-in-templates\} :::link Bài viết chính: [Phông chữ tùy chỉnh trong Flow Builder](using-custom-fonts-in-flow-builder) ::: Các template được đánh dấu chip **Custom font** có sử dụng phông chữ tùy chỉnh. Những phông chữ này không được tích hợp sẵn trong SDK. Di chuột vào chip để xem template đó dùng phông chữ nào. Để hiển thị đúng kiểu chữ như thiết kế trên thiết bị, hãy thêm các file phông chữ vào app bundle. Các phiên bản app cũ chưa có phông chữ đó sẽ tự động dùng phông chữ hệ thống thay thế. Để thay đổi font mà không ảnh hưởng đến các phiên bản cũ, hãy nhân đôi flow, thay đổi font trong bản sao, và giới hạn bản sao chỉ dành cho [người dùng trên các phiên bản ứng dụng có chứa font đó](segments). --- # File: builder-ui --- --- title: "Giao diện Flow Builder" description: "Tổng quan về giao diện và không gian làm việc của Flow Builder." --- Giao diện chính của Flow Builder bao gồm tất cả các công cụ cần thiết để thêm các thành phần trực quan, chỉnh sửa thuộc tính của chúng và thay đổi logic của flow người dùng. Bài viết này trình bày từng khu vực của giao diện: chức năng và vị trí của chúng. :::link Muốn tìm hiểu thêm về cách xây dựng flow? Xem các video hướng dẫn từng bước trong [danh sách phát YouTube](https://www.youtube.com/playlist?list=PLMksWqaZiWtM) của chúng tôi. ::: ## Các điều khiển dự án và phím tắt hữu ích (thanh công cụ trên cùng) \{#project-controls-and-useful-shortcuts-top-toolbar\} * **Close** Close: Thoát khỏi trình chỉnh sửa flow và quay lại trang danh sách flows. * **App name** App: Xác định ứng dụng mà flow thuộc về. * **All flows** Flows: Mở danh sách tất cả các flows của ứng dụng này. * **Flow status**: Biểu tượng bên trái tên flow cho biết [trạng thái flow](builder-save-publish#flow-status) hiện tại: - **Draft** Draft - **Publishing** (vòng xoay đang tải) - **Failed** Failed - hoặc **Live** Live. * **Rename the flow**: Nhấp vào tên flow để đổi tên. Nhiều flows có thể có cùng tên — [hãy đặt tên riêng cho mỗi flow mới](paywall-builder-templates#create-flow). * **View mode toggle**: Chuyển đổi giữa chế độ xem thiết kế Cursor và [chế độ xem remote config](customize-flow-with-remote-config)Remote Config. * **Undo/Redo**: Nhấp vào các biểu tượng mũi tên để hoàn tác Undo hoặc làm lại Redo các thay đổi trong flow. Bạn cũng có thể dùng ⌘Z / Ctrl+Z để hoàn tác. * **Save draft / Publish**: Nhấp **Save draft** để lưu tiến trình mà không phát hành (⌘ / Ctrl+S). Mở menu thả xuống Open dropdown để truy cập nút [**Publish**](builder-save-publish). Bạn chỉ có thể thêm flow vào [placement](create-placement) sau khi đã phát hành. ## Khu vực xem trước (trung tâm) \{#preview-area-center\} Khu vực trung tâm của không gian làm việc mô phỏng giao diện flow của bạn trên thiết bị di động. * Để chọn một phần tử và chỉnh sửa thuộc tính của nó, hãy nhấp vào phần tử đó. Để chọn phần tử con bên trong một container, hãy nhấp vào container trước, rồi nhấp vào phần tử con. * Để chỉnh sửa thuộc tính của chính màn hình, hãy nhấp ra ngoài bất kỳ phần tử nào, hoặc chọn màn hình trong bảng Screens and Layers. * Để thay đổi thứ tự của một phần tử, kéo mục đó lên hoặc xuống trong bảng Screens and Layers. :::warning Trình chỉnh sửa flow được thiết kế để tạo layout responsive. Vì vậy, bạn **không thể thay đổi vị trí của các phần tử theo cách thủ công** — bạn chỉ có thể thay đổi thứ tự của chúng. Cài đặt layout của mỗi container sẽ quyết định cách các phần tử bên trong được phân bổ. ::: ### Thanh màn hình đang hoạt động (phía trên khung xem trước thiết bị) \{#active-screen-bar-above-the-device-preview\} - **Screen name** — viên nhãn hiển thị tên màn hình hiện tại. - **Toggle animations** Toggle animations — bật hoặc tắt xem trước animation của các phần tử; animation chạy liên tục cho đến khi được tắt. Chỉ hiển thị khi màn hình đang active có ít nhất một [animation](builder-styling#animation). Không ảnh hưởng đến việc animation có hiển thị trên thiết bị thực hay không. - **Add element** Plus — mở [thư viện phần tử](builder-elements) tại màn hình hiện tại. Tương đương với nút **+** ở đầu bảng Screens and Layers — hữu ích khi bảng đó đang bị thu gọn. ### Điều khiển hiển thị (thanh công cụ phía dưới) \{#view-controls-bottom-toolbar\} Các công cụ trong thanh công cụ phía dưới giúp bạn điều khiển chế độ xem trước. * **Device**: Chọn một trong các mẫu iPhone hoặc Android có sẵn để thay đổi kích thước viewport và viền thiết bị. * **Screen orientation**: Chuyển đổi giữa chế độ dọc Portrait và ngang Landscape để xem trước flow của bạn ở các hướng khác nhau. * **Color scheme**: Chuyển đổi giữa chế độ sáng Light mode và tối Dark mode để xem thiết kế của bạn thích nghi với các theme khác nhau như thế nào. * **Locale**: Chọn một locale để xem trước flow của bạn với nội dung đã được bản địa hóa. * **View options**: Bật hoặc tắt viền thiết bị và hướng dẫn vùng an toàn. ## Thuộc tính màn hình và thành phần (bảng bên phải) \{#screen-and-element-properties-right-panel\} ### Cài đặt màn hình và bố cục \{#screen-settings-and-layout\} :::link Bài viết chính: [Màn hình và Layer](paywall-layout-and-products) ::: Khi không có phần tử nào được chọn, bảng bên phải cho phép bạn điều chỉnh các thuộc tính của [màn hình flow](paywall-layout-and-products) đang hoạt động, bao gồm: * Tương tác với giao diện hệ thống (ví dụ: thanh trạng thái có hiển thị hay không) * Quy tắc bố cục tự động * Nền (màu sắc, hình ảnh hoặc video) * Kích thước padding * Hành vi cuộn dọc Nếu màn hình chứa một số thành phần nhất định, chẳng hạn như [câu hỏi tương tác](onboarding-quizzes), danh sách này sẽ được mở rộng với các thuộc tính liên quan. ### Thuộc tính của phần tử \{#element-properties\} Khi bạn chọn một phần tử, bảng bên phải cho phép bạn thay đổi style và các thuộc tính tương tác của nó. #### Thuộc tính thiết kế \{#design-properties\} :::link Tìm hiểu thêm: [Bố cục và vị trí](manage-paywall-ui-elements), [Style và giao diện](builder-styling) ::: Tab **Design** cho phép bạn cấu hình giao diện trực quan và bố cục của phần tử được chọn: * **Visibility**: Hiển thị hoặc ẩn phần tử. Bật **Conditional** visibility để đặt các quy tắc xác định khi nào phần tử sẽ hiển thị. * **Position**: Chọn giữa Relative, Absolute, hoặc Fixed positioning. * **Content** (chỉ dành cho phần tử văn bản): Chỉnh sửa nội dung văn bản của phần tử, chèn [biến](#variables), và quản lý bản địa hóa. * **Typography** (chỉ dành cho phần tử văn bản): Cấu hình font, độ đậm, kích thước, màu sắc, căn chỉnh, trang trí và cắt bớt văn bản. * **Spacing**: Đặt margin và padding cho phần tử. * **Effects**: Thêm drop shadow, inner shadow, background blur, hoặc layer blur. * **Animation**: Thêm hiệu ứng động (ví dụ: Pulse) và cấu hình thời gian cũng như cường độ của chúng. * **Appearance**: Điều chỉnh độ mờ và góc xoay. * **Layout**: Chọn hướng bố cục (dọc hoặc ngang) và xác định cách phân bố các phần tử con. #### Thuộc tính tương tác \{#interactions-properties\} :::link Tìm hiểu thêm: [Actions](onboarding-actions), [Navigation and Interaction](onboarding-navigation-branching) ::: Tab **Interactions** cho phép bạn định nghĩa điều gì xảy ra khi người dùng tương tác với phần tử đã chọn. Mỗi tương tác bao gồm một **trigger** và một hoặc nhiều **actions**: * **Triggers** (Kích hoạt) định nghĩa *khi nào* điều gì đó xảy ra — ví dụ: **On Tap** (người dùng nhấn vào phần tử). * **Actions** (Hành động) định nghĩa *điều gì* xảy ra — ví dụ: điều hướng đến màn hình khác hoặc thay đổi giá trị của một biến. Thêm nhiều hành động vào một trigger để thực hiện chúng theo thứ tự. Bạn có thể thêm nhiều trigger vào cùng một phần tử để thực thi nhiều hành động liên tiếp nhau. ## Bảng bên trái \{#left-panel\} Bảng bên trái thay đổi chức năng dựa trên nút nào đang được chọn. Bạn có thể chọn giữa: * [Screens and layers](#screens-and-layers) * [Add element](#element-selection) * [Products](#products) * [Styles](#saved-styles) * [Variables](#variables) * [Localization](#localization) ### Screens and Layers \{#screens-and-layers\} :::link Bài viết chính: [Screens and Layers](paywall-layout-and-products) ::: Nút layers Layers mở Screens and Layers (được hiển thị mặc định khi bạn mở flow builder). Nó hiển thị từng màn hình dưới dạng cây các layer. Mỗi thành phần trên một màn hình là một layer, và các container có các thành phần con lồng bên trong. Bạn có thể kéo và thả các layer để sắp xếp lại chúng. ### Chọn thành phần \{#element-selection\} :::link Bài viết chính: [Elements](builder-elements) ::: Nếu bạn nhấn vào nút plus Plus, bảng bên trái hiển thị danh sách các thành phần UI có sẵn và các biến thể của chúng. Nhấn vào một mục để thêm nó vào màn hình hiện tại dưới dạng một layer mới. ### Products \{#products\} :::link Bài viết chính: [Products](paywall-product-block) ::: Nút products Products mở danh sách sản phẩm. Nó hiển thị các sản phẩm nào được gán cho từng màn hình trong flow của bạn. Danh sách này chỉ để xem. Để gán sản phẩm cho một màn hình, hãy thêm thành phần Product và cấu hình nó trong bảng bên phải. Để tạo hoặc chỉnh sửa sản phẩm, hãy sử dụng trang **Products** trên Adapty Dashboard. ### Kiểu đã lưu \{#saved-styles\} :::info Tìm hiểu thêm: - [Kiểu và giao diện](builder-styling) - [Nội dung văn bản](onboarding-text) - [Chế độ tối](paywall-dark-mode) ::: Nút Styles Styles mở Saved Styles. Tại đây, bạn có thể chỉnh sửa và quản lý các kiểu toàn cục. Nếu nhiều phần tử trong flow của bạn dùng chung kiểu chữ hoặc màu sắc, hãy lưu dữ liệu đó thành một kiểu toàn cục. Sau đó bạn có thể tái sử dụng nó chỉ bằng một cú nhấp. Hiện tại, Flow Builder hỗ trợ hai loại global style — Font styles và Color styles. Mỗi Color style có thể tùy chọn đặt giá trị riêng cho chế độ Dark mode. ### Biến \{#variables\} :::link Bài viết chính: [Biến](onboarding-variables) ::: Nút dấu ngoặc Variables mở phần Biến. Tại đây, bạn có thể tạo và quản lý các biến cho flow của mình. Khi chạy, SDK sẽ thay thế các placeholder biến bằng giá trị thực tế — thuộc tính người dùng, giá sản phẩm, chuỗi đã được bản địa hóa, và nhiều hơn nữa. Các biến được nhóm thành hai tab: * **Custom**: Biến do bạn tạo và kiểm soát thông qua các action. * **Elements**: Giá trị được xác định bởi tương tác của người dùng — chẳng hạn như câu trả lời quiz, trạng thái toggle, hay lựa chọn tab. Các biến sản phẩm — giá, tên và các dữ liệu sản phẩm khác — không được liệt kê trong panel này. Hãy tham chiếu trực tiếp đến chúng khi chỉnh sửa một phần tử văn bản. Dùng biến để: * **Liên kết văn bản**: Hiển thị nội dung động thay vì chuỗi tĩnh. * **Kiểm soát hiển thị**: Hiển thị hoặc ẩn các phần tử dựa trên điều kiện (ví dụ: ẩn nút nâng cấp đối với người dùng premium). * **Tương tác với người dùng**: Truy cập dữ liệu từ các trường nhập liệu, chẳng hạn như biểu mẫu hoặc bài kiểm tra. ### Bản địa hóa \{#localization\} :::link Bài viết chính: [Bản địa hóa](add-flow-remote-config-locale) ::: Giao diện Localization cho phép bạn quản lý toàn bộ nội dung có thể dịch trong flow của mình. Nó hiển thị bảng gồm tất cả các chuỗi văn bản và hình ảnh, được sắp xếp theo màn hình, với các cột tương ứng cho từng locale. Từ giao diện này, bạn có thể: * Thêm locale mới và chỉnh sửa các chuỗi đã dịch trực tiếp. * Theo dõi trạng thái dịch — mỗi hàng được đánh dấu là **Done** hoặc **Missing**. * Lọc theo màn hình hoặc chỉ hiển thị những bản dịch còn thiếu. * Dùng **AI Translate** để tự động dịch nội dung, hoặc **Import/Export** bản dịch hàng loạt. --- # File: flow-builder-recipes --- --- title: "Công thức flow phổ biến" description: "Hướng dẫn từng bước để xây dựng các mẫu màn hình phổ biến trong Flow Builder." --- Phần này hướng dẫn cách xây dựng các mẫu màn hình phổ biến nhất trong Flow Builder — từng thành phần một, từ cách bố cục đến cách tương tác. Mỗi hướng dẫn độc lập và sử dụng các thành phần Flow Builder tiêu chuẩn. <CustomDocCardList /> Xem video khởi động nhanh này để tạo một flow cá nhân hóa cơ bản: <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/aa-m459VIuY?si=zN_Co6B6qB88UPZP" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> :::link Muốn tìm hiểu thêm về cách xây dựng flow? Xem các video hướng dẫn từng bước trong [danh sách phát YouTube](https://www.youtube.com/playlist?list=PLMksWqaZiWtM) của chúng tôi. ::: --- # File: basic-paywall-screen --- --- title: "Tạo màn hình paywall cơ bản" description: "Hướng dẫn từng bước để xây dựng màn hình paywall tiêu chuẩn trong Flow Builder." --- Đây là mẫu paywall phổ biến nhất. Bạn có thể dùng nó như một màn hình độc lập, hoặc đặt ở cuối một [flow](adapty-flow-builder) nhiều màn hình. Một màn hình paywall tiêu chuẩn bao gồm tiêu đề, mô tả giá trị, danh sách tính năng, danh sách sản phẩm, nút mua hàng và các liên kết ở phần cuối để khôi phục giao dịch, điều khoản sử dụng và chính sách bảo mật. ## Trước khi bắt đầu \{#before-you-start\} - [Tạo sản phẩm](create-product) trong Adapty Dashboard. - [Kết nối Adapty với App Store và Google Play](integrate-payments). ## 1. Thiết lập styles có thể tái sử dụng \{#set-up-reusable-styles\} Styles có thể tái sử dụng cho phép bạn áp dụng cùng một kiểu chữ và màu sắc trên mọi màn hình chỉ với một cú nhấp. Mỗi flow mới đều có sẵn một bộ text styles mặc định (H1, Body, Button Label, v.v.) — hãy điều chỉnh chúng cho phù hợp với thiết kế của bạn trước khi bắt đầu thêm các phần tử. Thêm color styles cho các màu thương hiệu mà bạn sẽ dùng xuyên suốt màn hình. Để xem hướng dẫn đầy đủ, tham khảo [Styles và giao diện — Styles có thể tái sử dụng](builder-styling#reusable-styles). Để thiết lập styles: 1. Trong bảng bên trái, mở bảng **Styles** Styles. 2. Trong tab **Text**, nhấp vào một style hiện có để chỉnh sửa font, độ đậm, kích thước và màu sắc. Chỉ thêm style mới nếu các style mặc định không đáp ứng được nhu cầu của bạn. 3. Trong tab **Colors**, nhấp vào **Plus Create style** và thêm các màu bạn định tái sử dụng trên màn hình. ## 2. Thiết lập bố cục màn hình \{#2-set-up-the-screen-layout\} Bản thân màn hình đóng vai trò như một container chứa mọi thứ bạn thêm vào. Hãy cấu hình bố cục, nền và padding trước để các phần tử bạn thêm vào sau này được phân bổ đúng cách. Để xem danh sách đầy đủ các thuộc tính màn hình, xem [Screens and layers — Screen settings](paywall-layout-and-products#screen-settings). Để cấu hình màn hình: 1. Nhấp vào vùng trống trên canvas để chọn màn hình. Bảng bên phải sẽ chuyển sang cài đặt màn hình. 2. Trong **System UI**, tắt **Safe area** để nội dung kéo dài đến mép màn hình. 3. Trong **Layout**, đặt hướng thành **Vertical** Vertical và phân phối thành **Space evenly**. 4. Trong mục **Fill**, chọn loại nền — màu đơn, gradient, hoặc hình ảnh. Ví dụ này dùng **Gradient** Gradient với hai điểm màu. ## 3. Thêm nút đóng \{#3-add-the-close-button\} Nút đóng giúp người dùng thoát khỏi paywall. Preset **Close** đã được cấu hình sẵn — không cần thiết lập thêm hành động. 1. Trên canvas, nhấp **+**. 2. Chọn **Buttons** > **Close**. ## 4. Thêm tiêu đề và ghép với nút đóng \{#add-the-title-and-pair-it-with-the-close-button\} H1 nằm cạnh nút đóng ở phía trên màn hình. Để căn chúng theo chiều ngang, hãy bọc cả hai vào một container ngang. Để thêm tiêu đề: 1. Nhấn **+** > **Text** > **H1**. 2. Chọn H1, mở tab **Design** ở bảng bên phải và chỉnh nội dung trong trường **Content**. Để nhóm tiêu đề với nút đóng: 1. Trong bảng **Layers**, nhấp vào menu ba chấm Context menu trên layer nút đóng và chọn **Wrap** > **Wrap in Horizontal Container**. 2. Kéo layer H1 vào container nằm ngang vừa tạo. Để căn chỉnh hai phần tử: 1. Điều chỉnh kích thước nút đóng và cỡ chữ H1 sao cho chúng hiển thị thoải mái trên cùng một dòng. 2. Khi container ngang được chọn, thiết lập căn chỉnh và phân phối trong bảng bên phải để các phần tử hiển thị đúng vị trí. ## 5. Thêm mô tả giá trị \{#5-add-the-value-description\} Một dòng body ngắn bên dưới tiêu đề giải thích những gì người dùng nhận được từ gói đăng ký. 1. Nhấp **+** > **Text** > **Body**. 2. Khi phần tử body được chọn, chỉnh sửa văn bản trong trường **Content** trên tab **Design**. ## 6. Thêm danh sách tính năng \{#add-the-feature-list\} Danh sách tính năng làm nổi bật những gì người dùng nhận được khi mở khóa gói đăng ký. Mỗi hàng gồm một biểu tượng, tiêu đề tính năng và mô tả ngắn. Xem toàn bộ các preset danh sách tại [Elements — List](builder-elements#list). Để thêm danh sách tính năng: 1. Nhấn **+** > **List** và chọn một preset danh sách. Icon List là lựa chọn phổ biến nhất cho paywall. 2. Chọn từng hàng, sau đó chỉnh tiêu đề và mô tả trong trường **Content**. 3. Để thêm hoặc xóa hàng, chọn danh sách và dùng các điều khiển hàng trong bảng **Layers**. ## 7. Thêm danh sách sản phẩm \{#add-the-product-list\} Danh sách sản phẩm hiển thị các tùy chọn gói đăng ký mà người dùng có thể lựa chọn. Phần tử Products hiển thị một thẻ cho mỗi sản phẩm được gán cho màn hình, và một thẻ sẽ tự động được đánh dấu là mặc định. Để biết thêm về cách quản lý sản phẩm, xem [Thiết lập mua hàng](paywall-product-block). Để thêm và cấu hình sản phẩm: 1. Nhấp vào **+** > **Products** và chọn một bố cục preset. Vertical List là phổ biến nhất. 2. Chọn từng thẻ sản phẩm trên canvas và chọn một sản phẩm từ dropdown trong tab **Design**. Dropdown hiển thị mọi sản phẩm đã được cấu hình trong Adapty Dashboard. 3. Để thay đổi lựa chọn mặc định, chọn thẻ bạn muốn và bật **Set as default product** trong tab **Design**. 4. Để tùy chỉnh badge giảm giá, mở rộng thẻ sản phẩm trong bảng **Layers**, chọn layer badge và chỉnh sửa văn bản của nó trong trường **Content**. Ẩn badge trên các thẻ khác bằng cách bật/tắt biểu tượng mắt Show bên cạnh mỗi layer badge. ## 8. Thêm nút mua hàng \{#add-the-purchase-button\} Nút mua hàng sẽ khởi động in-app purchase cho sản phẩm mà người dùng đã chọn. Biến `products.selectedProduct` luôn trỏ đến sản phẩm đang được chọn trên màn hình. Để thêm nút mua hàng: 1. Nhấn **+** > **Buttons** và chọn một kiểu nút. 2. Khi nút được chọn, mở tab **Interactions** ở bảng bên phải. 3. Nhấn **Add trigger** > **On tap**, sau đó nhấn **Add action**. 4. Đặt **Action** thành **Purchase** và **Product** thành `products.selectedProduct`. ## 9. Thêm liên kết footer \{#add-footer-links\} Footer chứa các liên kết đến điều khoản sử dụng và chính sách bảo mật (bắt buộc theo yêu cầu của cửa hàng) và một nút để khôi phục các lần mua trước đó. Để thêm liên kết footer: 1. Nhấn **+** > **Buttons** > **Links**. Thao tác này sẽ thêm một hàng gồm Restore Purchases, Terms of Use và Privacy Policy. 2. Trong bảng **Layers**, chọn nút **Terms of Use**. Mở tab **Interactions** — hành động **Open URL** đã được gắn sẵn. Nhấn vào hành động đó và nhập URL đích. 3. Lặp lại tương tự với nút **Privacy Policy** bằng URL chính sách bảo mật của bạn. 4. Giữ nguyên nút **Restore Purchases**. Hành động của nút này đã được cấu hình sẵn. :::tip Nếu vị trí của bất kỳ phần tử nào có vẻ quá cao hoặc quá thấp, hoặc nếu bạn muốn thêm khoảng cách ở bất kỳ đâu, hãy điều chỉnh margin và padding của phần tử đó. ::: ## Các bước tiếp theo \{#next-steps\} - [Lưu và xuất bản flow của bạn](builder-save-publish). - [Thêm flow vào một placement](create-placement) để bắt đầu hiển thị cho người dùng. --- # File: show-plans-bottom-sheet --- --- title: "Hiển thị tất cả gói trong một bottom sheet" description: "Xây dựng một paywall hero với một CTA duy nhất, một liên kết 'Hiển thị tất cả gói', và một bottom sheet hiển thị toàn bộ danh sách sản phẩm." --- Template này hiển thị một ưu đãi nổi bật duy nhất trước, kèm theo một liên kết nhỏ dẫn đến danh sách gói đầy đủ. Nhấn **Show all plans** sẽ kéo lên một bottom sheet chứa các sản phẩm còn lại, nút mua hàng và các liên kết footer. Dùng template này khi một gói chuyển đổi tốt hơn đáng kể — bottom sheet giữ các lựa chọn khác chỉ một lần nhấn mà không làm màn hình chính bị lộn xộn. ## Trước khi bắt đầu \{#before-you-start\} - [Tạo sản phẩm](create-product) trong Adapty Dashboard. - [Kết nối Adapty với App Store và Google Play](integrate-payments). ## 1. Thiết lập bố cục màn hình \{#1-set-up-the-screen-layout\} Dùng ảnh hero làm nền màn hình và nhóm phần còn lại của nội dung ở phía dưới, để ảnh lấp đầy phần trên của màn hình. Để xem toàn bộ danh sách thuộc tính màn hình, xem [Screens and layers — Screen settings](paywall-layout-and-products#screen-settings). Để cấu hình màn hình: 1. Nhấn vào vùng trống trên canvas để chọn màn hình. 2. Trong **System UI**, tắt **Safe area** để ảnh hero mở rộng đến các cạnh màn hình. 3. Trong **Fill**, chọn **Image** Image và tải ảnh hero lên. 4. Trong **Layout**, cấu hình hướng, khoảng cách và căn chỉnh để neo nội dung vào vị trí bạn muốn. Với template này, hướng **Vertical** Vertical với khoảng cách nhỏ và căn chỉnh **bottom-middle** sẽ nhóm tiêu đề và các nút ở phần dưới màn hình. ## 2. Thêm tiêu đề CTA \{#2-add-the-cta-heading\} Tiêu đề nằm ở phần dưới màn hình, ngay phía trên nút đăng ký. Ảnh hero lấp đầy khu vực phía trên. 1. Nhấn **+** > **Text** > **H1**. 2. Với H1 được chọn, mở tab **Design** và chỉnh sửa văn bản trong trường **Content**. ## 3. Thêm bottom sheet và tiêu đề của nó \{#3-add-the-bottom-sheet-and-its-title\} Bottom sheet là một container bố cục trượt lên từ phía dưới màn hình. Thêm nó ở trạng thái hiển thị trước — bạn sẽ điền nội dung vào các bước tiếp theo và ẩn nó sau khi đã có nội dung đầy đủ. Các phần tử bị ẩn không thể chỉnh sửa, vì vậy sheet phải ở trạng thái hiển thị cho đến khi bạn điền xong. Để biết thêm về bottom sheet và các container bố cục khác, xem [Elements — Layout](builder-elements#layout). Để thêm bottom sheet và tiêu đề của nó: 1. Nhấn **+** > **Layout** > **Bottom Sheet**. 2. Trong bảng **Layers**, mở rộng bottom sheet, chọn layer **Title** và chỉnh sửa trường **Content** trong tab **Design** — ví dụ: `Choose your plan`. ## 4. Thêm danh sách sản phẩm vào bottom sheet \{#4-add-the-product-list-inside-the-bottom-sheet\} Đặt tất cả sản phẩm vào bên trong bottom sheet. Một trong số chúng cũng sẽ điều khiển giá hiển thị trên nút CTA chính. Để biết thêm về quản lý sản phẩm, xem [Set up purchases](paywall-product-block). Để thêm và cấu hình sản phẩm: 1. Nhấn **+** > **Products** và chọn một preset bố cục. Vertical List phù hợp với hầu hết các trường hợp. Phần tử xuất hiện trên màn hình, bên ngoài bottom sheet. 2. Trong bảng **Layers**, kéo layer Products vào container **Content** bên trong bottom sheet. 3. Chọn từng thẻ sản phẩm trên canvas và chọn sản phẩm từ dropdown trong tab **Design**. ## 5. Thêm nút mua hàng vào bottom sheet \{#5-add-the-purchase-button-inside-the-bottom-sheet\} Bottom sheet cần nút mua hàng riêng để mua bất kỳ gói nào người dùng chọn từ danh sách. 1. Nhấn **+** > **Buttons** và chọn một preset nút. 2. Trong bảng **Layers**, kéo nút mới vào container **Content** bên trong bottom sheet. 3. Với nút được chọn, mở tab **Interactions** ở bảng bên phải. 4. Nhấn **Add trigger** > **On tap**, rồi nhấn **Add action**. 5. Đặt **Action** thành **Purchase** và **Product** thành `products.selectedProduct`. ## 6. Thêm các liên kết footer vào bottom sheet \{#6-add-the-footer-links-inside-the-bottom-sheet\} :::important Đừng dùng [inline links](onboarding-text#inline-link) cho văn bản lồng trong nút. Thay vào đó, hãy thiết lập hành động **Open URL** trực tiếp trên nút. ::: Điều khoản sử dụng, chính sách bảo mật và khôi phục mua hàng nằm ở cuối sheet — màn hình chính vẫn gọn gàng. 1. Nhấn **+** > **Buttons** > **Links**. Thao tác này thêm một hàng với Restore Purchases, Terms of Use và Privacy Policy. 2. Trong bảng **Layers**, kéo hàng Links vào container **Content** bên trong bottom sheet. 3. Trong bảng **Layers**, chọn nút **Terms of Use**. Mở tab **Interactions** và dán URL điều khoản của bạn vào trường **Open URL**. 4. Lặp lại cho nút **Privacy Policy** với URL chính sách bảo mật của bạn. 5. Giữ nguyên liên kết **Restore Purchases**. Hành động của nó đã được cấu hình sẵn. ## 7. Ẩn bottom sheet \{#7-hide-the-bottom-sheet\} Sau khi nội dung của sheet đã đầy đủ, hãy ẩn nó để nó không xuất hiện trên màn hình theo mặc định. Người dùng sẽ hiển thị nó bằng cách nhấn **Show all plans** ở bước cuối. Trong bảng **Layers**, chọn bottom sheet và đặt trạng thái của nó thành **Hide** Hide. Sheet vẫn còn trong cây layer nhưng không còn hiển thị trên canvas. ## 8. Thêm nút đăng ký chính \{#8-add-the-main-subscribe-button\} Nút chính trên màn hình đăng ký người dùng vào gói hàng tháng chỉ với một lần nhấn. Nhãn của nó sử dụng biến giá của sản phẩm hàng tháng để nút luôn đồng bộ với sản phẩm. 1. Trong bảng **Layers**, nhấn vào màn hình để các phần tử mới được thêm vào root, không phải bên trong bottom sheet. 2. Nhấn **+** > **Buttons** và chọn một preset nút. 3. Với nút được chọn, mở tab **Design** và đặt con trỏ vào trường **Content**. Nhấn Variable icon và chọn biến giá cho sản phẩm chính. Bao quanh nó bằng phần còn lại của nhãn — ví dụ: `Subscribe for {price}/month`. 4. Chuyển sang tab **Interactions** và nhấn **Add trigger** > **On tap** > **Add action**. 5. Đặt **Action** thành **Purchase** và **Product** thành sản phẩm bạn cần. Không giống nút trong bottom sheet, nút này nhắm đến một sản phẩm cụ thể thay vì `products.selectedProduct`. ## 9. Thêm liên kết 'Show all plans' \{#9-add-the-show-all-plans-link\} Một liên kết văn bản bên dưới nút đăng ký hiển thị bottom sheet khi nhấn. Thêm nó dưới dạng phần tử văn bản với style **Button Label** giữ giao diện tối giản trong khi vẫn cho phép bạn gắn hành động. Để biết thêm về hành động Show/Hide, xem [Actions — Show/hide elements](onboarding-actions#showhide-elements). Để thêm liên kết: 1. Với màn hình được chọn trong bảng **Layers**, nhấn **+** > **Text** > **Button Label**. 2. Với phần tử văn bản được chọn, chỉnh sửa trường **Content** thành `Show all plans`. 3. Mở tab **Interactions** và nhấn **Add trigger** > **On tap** > **Add action**. 4. Đặt **Action** thành **Show** và chọn phần tử bottom sheet từ dropdown. ## Các bước tiếp theo \{#next-steps\} - [Lưu và xuất bản flow của bạn](builder-save-publish). - [Thêm flow vào một placement](create-placement) để bắt đầu hiển thị cho người dùng. --- # File: paywall-with-tabs --- --- title: "Tạo paywall với tab" description: "Xây dựng màn hình paywall với hai tab chuyển đổi giữa các danh sách tính năng, nhóm sản phẩm và hành động mua khác nhau." --- Template này dùng tab để chuyển đổi giữa hai biến thể của cùng một ưu đãi trên một màn hình duy nhất. Mỗi tab chứa danh sách tính năng, danh sách sản phẩm và nút mua riêng. Nhấn vào tab sẽ thay thế nội dung hiển thị mà không cần rời khỏi màn hình — hữu ích khi phân chia các gói theo cấp độ, kỳ thanh toán, hoặc phân khúc đối tượng. ## Trước khi bắt đầu \{#before-you-start\} - [Tạo sản phẩm](create-product) trong Adapty Dashboard. - [Kết nối Adapty với App Store và Google Play](integrate-payments). ## 1. Thiết lập bố cục màn hình \{#1-set-up-the-screen-layout\} Màn hình đóng vai trò là container cho nút đóng, tiêu đề, tab và nội dung tab. Trong ví dụ này, nền là một hình ảnh, nhưng màu nền đơn sắc hoặc gradient cũng hoạt động tương tự. Để xem danh sách đầy đủ các thuộc tính màn hình, hãy xem [Màn hình và layer — Cài đặt màn hình](paywall-layout-and-products#screen-settings). Để cấu hình màn hình: 1. Nhấp vào vùng trống trên canvas để chọn màn hình. 2. Trong **System UI**, tắt **Safe area** để nền kéo dài đến các cạnh màn hình. 3. Trong **Fill**, chọn loại nền và cấu hình nó. Ví dụ này dùng **Image** Image, nhưng màu đơn hoặc gradient cũng hoạt động tương tự. 4. Trong **Layout**, đặt hướng thành **Vertical** Vertical và cấu hình khoảng cách và căn chỉnh để các phần tử xếp chồng từ trên xuống với nội dung tab lấp đầy phần không gian còn lại. ## 2. Thêm nút đóng \{#2-add-the-close-button\} Nút đóng dùng để thoát khỏi paywall. Preset **Close** đã được cấu hình sẵn — không cần thiết lập thêm hành động. 1. Trên canvas, nhấp **+**. 2. Chọn **Buttons** > **Close**. ## 3. Thêm tiêu đề và ghép nó với nút đóng \{#add-the-title-and-pair-it-with-the-close-button\} Tiêu đề nằm cạnh nút đóng ở đầu màn hình. Để căn chúng theo chiều ngang, hãy bọc cả hai trong một container ngang. Để thêm tiêu đề: 1. Nhấn **+** > **Text** > **H1**. 2. Khi đã chọn H1, mở tab **Design** và chỉnh nội dung trong trường **Content**. Để nhóm tiêu đề với nút đóng: 1. Trong bảng **Layers**, nhấp vào menu ba chấm Context menu trên layer nút đóng và chọn **Wrap** > **Wrap in Horizontal Container**. 2. Kéo layer H1 vào horizontal container vừa tạo. Để căn chỉnh hai phần tử: 1. Điều chỉnh kích thước nút đóng và cỡ chữ H1 để chúng nằm thoải mái trên cùng một dòng. 2. Chọn container ngang, sau đó thiết lập căn chỉnh và phân phối trong bảng bên phải để các phần tử hiển thị đúng vị trí. ## 4. Thêm các tab và cấu hình nhãn của chúng \{#4-add-the-tabs-and-configure-their-labels\} Phần tử Tabs chia một phần màn hình thành các bảng nội dung có thể chuyển đổi. Mỗi tab có vùng chứa nội dung riêng, hiển thị khi người dùng chọn tab đó. Để tìm hiểu thêm về phần tử Tabs, xem [Elements — Tabs](builder-elements#tabs). Để tìm hiểu thêm về các nhóm có thể chọn, xem [Selectable elements and groups](flow-selectable-elements). Để thêm các tab: 1. Nhấn **+** > **Tabs** và chọn một kiểu — Segment control, Button Tabs hoặc Underline. 2. Chọn tên của từng tab trên canvas hoặc trong bảng **Layers**, rồi chỉnh trường **Content** ở tab **Design** để thay đổi nhãn — ví dụ: `Premium` và `Pro`. ## 5. Thêm danh sách tính năng vào tab đầu tiên \{#5-add-a-feature-list-to-the-first-tab\} Một danh sách tính năng ngắn gọn, súc tích bên trong tab đầu tiên giúp người dùng biết gói đó bao gồm những gì. Để xem toàn bộ các preset danh sách, xem [Elements — List](builder-elements#list). Để thêm danh sách tính năng: 1. Nhấn **+** > **List** và chọn một preset danh sách. Icon List là loại gọn nhất cho paywall. Phần tử sẽ xuất hiện ở cuối cây layer. 2. Chọn từng hàng, rồi chỉnh sửa tiêu đề trong trường **Content**. 3. Trong bảng **Layers**, kéo danh sách vào container **Content** của tab đầu tiên. ## 6. Thêm danh sách sản phẩm vào tab đầu tiên \{#add-the-product-list-to-the-first-tab\} Danh sách sản phẩm hiển thị các tùy chọn gói đăng ký cho tab đầu tiên. Phần tử Products hiển thị một thẻ cho mỗi sản phẩm được gán cho màn hình và tạo nhóm có thể chọn riêng của nó. Để biết thêm về quản lý sản phẩm, xem [Thiết lập mua hàng](paywall-product-block). Để thêm và cấu hình sản phẩm: 1. Nhấp **+** > **Products** và chọn bố cục mẫu. Vertical List phù hợp cho các gói xếp chồng. Phần tử xuất hiện ở cuối cây lớp. 2. Chọn từng thẻ sản phẩm trên canvas và chọn sản phẩm từ menu thả xuống trong tab **Design**. 3. Trong bảng **Layers**, kéo layer Products vào vùng **Content** của tab đầu tiên. ## 7. Thêm nút mua hàng vào tab đầu tiên \{#add-the-purchase-button-to-the-first-tab\} Nút mua hàng khởi động quá trình in-app purchase cho sản phẩm mà người dùng đã chọn trong tab đầu tiên. Nhãn của nút sử dụng giá của sản phẩm được chọn để luôn đồng bộ với lựa chọn của người dùng. Để tìm hiểu thêm về hành động Purchase, xem [Hành động — Purchase](onboarding-actions#purchase). Để thêm và cấu hình nút mua hàng: 1. Nhấn **+** > **Buttons** và chọn một preset nút. Phần tử sẽ xuất hiện ở cuối cây lớp. 2. Khi nút đang được chọn, mở tab **Design** và đặt con trỏ vào trường **Content**. Nhấn vào biểu tượng biến Variable icon, chọn `products.selectedProduct`, rồi chọn thuộc tính `prod_price` — biến đầy đủ sẽ là `products.selectedProduct.prod_price`. Bao quanh nó bằng phần còn lại của nhãn — ví dụ: `Subscribe for {prod_price}`. 3. Chuyển sang tab **Interactions** và nhấn **Add trigger** > **On tap** > **Add action**. 4. Đặt **Action** thành **Purchase** và **Product** thành `products.selectedProduct`. 5. Trong bảng **Layers**, kéo nút vào container **Content** của tab đầu tiên. ## 8. Sao chép nội dung từ tab đầu tiên sang tab thứ hai \{#copy-the-first-tabs-content-into-the-second-tab\} Thay vì xây dựng lại cấu trúc từ đầu, hãy sao chép danh sách tính năng, danh sách sản phẩm và nút mua hàng từ tab đầu tiên sang tab thứ hai. Sau đó bạn chỉ cần cập nhật lại các giá trị. Để sao chép nội dung: 1. Trong bảng **Layers**, mở rộng container **Content** của tab đầu tiên. 2. Chọn từng phần tử bên trong (danh sách tính năng, sản phẩm, nút mua), sao chép bằng ⌘C / Ctrl+C, rồi dán bằng ⌘V / Ctrl+V. Các bản sao sẽ xuất hiện ở cuối cây layer. 3. Kéo từng phần tử vừa sao chép vào container **Content** của tab thứ hai. ## 9. Cập nhật nội dung tab thứ hai \{#update-the-second-tabs-content\} Tab thứ hai hiện đang giống hệt tab đầu tiên. Hãy cập nhật từng phần tử để phản ánh gói thứ hai. Để cập nhật tab thứ hai: 1. Chỉnh sửa danh sách tính năng trong tab thứ hai để các hàng khớp với các tính năng của gói thứ hai. 2. Chọn từng thẻ sản phẩm trong phần tử Products của tab thứ hai và gán các sản phẩm của gói thứ hai từ menu thả xuống. Phần tử Products này sẽ tự động trở thành một nhóm có thể chọn riêng biệt (`products2`). 3. Chọn nút mua trong tab thứ hai. Trong trường **Content** của tab **Design**, đổi biến giá từ `products.selectedProduct.prod_price` thành `products2.selectedProduct.prod_price`. 4. Chuyển sang tab **Interactions** và cập nhật **Product** trong hành động **Purchase** từ `products.selectedProduct` thành `products2.selectedProduct`. ## 10. Thêm các liên kết footer dùng chung \{#add-the-shared-footer-links\} Các liên kết điều khoản sử dụng, chính sách bảo mật và khôi phục mua hàng luôn hiển thị dù đang ở tab nào. Thêm chúng ở cấp màn hình — bên ngoài cả hai container nội dung tab — để chúng được dùng chung giữa các tab. Để thêm các liên kết footer: 1. Nhấp vào **+** > **Buttons** > **Links**. Thao tác này sẽ thêm một hàng gồm Restore Purchases, Terms of Use và Privacy Policy vào cuối cây layer — đây chính xác là vị trí bạn muốn, ở gốc của màn hình, không lồng bên trong tab nào. 2. Trong bảng **Layers**, chọn nút **Terms of Use**. Mở tab **Interactions** và dán URL điều khoản của bạn vào trường **Open URL**. 3. Lặp lại tương tự cho nút **Privacy Policy** với URL chính sách quyền riêng tư của bạn. 4. Để nguyên liên kết **Restore Purchases**. Hành động của nó đã được cấu hình sẵn. ## Các bước tiếp theo \{#next-steps\} - [Lưu và xuất bản flow của bạn](builder-save-publish). - [Thêm flow vào một placement](create-placement) để bắt đầu hiển thị cho người dùng. --- # File: paywall-features-per-product --- --- title: "Hiển thị tính năng khác nhau theo sản phẩm" description: "Hiển thị danh sách tính năng khác nhau tùy theo sản phẩm người dùng chọn, sử dụng hiển thị có điều kiện." --- Template này sử dụng tính năng hiển thị có điều kiện để làm nổi bật các danh sách tính năng khác nhau cho từng gói. Màn hình hiển thị hai sản phẩm — ví dụ như Pro và Pro+ — và danh sách tính năng sẽ thay đổi tùy theo sản phẩm người dùng đang chọn. Một sản phẩm được đánh dấu là mặc định, vì vậy danh sách tính năng của nó sẽ hiển thị ngay khi màn hình tải lần đầu. ## Trước khi bắt đầu \{#before-you-start\} - [Tạo sản phẩm](create-product) trong Adapty Dashboard. - [Kết nối Adapty với App Store và Google Play](integrate-payments). ## 1. Thiết lập bố cục màn hình \{#1-set-up-the-screen-layout\} Màn hình đóng vai trò là vùng chứa cho tất cả nội dung bạn thêm vào. Trong ví dụ này, nền là một hình ảnh, nhưng màu đơn sắc hoặc gradient cũng hoạt động tương tự. Để xem danh sách đầy đủ các thuộc tính màn hình, hãy xem [Màn hình và các lớp — Cài đặt màn hình](paywall-layout-and-products#screen-settings). Để cấu hình màn hình: 1. Nhấp vào vùng trống của canvas để chọn màn hình. 2. Trong **System UI**, tắt **Safe area** để nền mở rộng đến các cạnh màn hình. 3. Trong **Fill**, chọn loại nền và cấu hình nó. Ví dụ này dùng **Image** Image, nhưng màu nền đơn sắc hoặc gradient cũng hoạt động tương tự. 4. Trong **Layout**, đặt hướng thành **Vertical** Vertical và cấu hình khoảng cách và căn chỉnh để các phần tử xếp chồng từ trên xuống với nội dung lấp đầy phần không gian còn lại. ## 2. Thêm nút đóng \{#2-add-the-close-button\} Nút đóng sẽ đóng paywall. Preset **Close** đã được cấu hình sẵn — không cần thiết lập hành động. 1. Trên canvas, nhấp **+**. 2. Chọn **Buttons** > **Close**. ## 3. Thêm tiêu đề và ghép với nút đóng \{#add-the-title-and-pair-it-with-the-close-button\} Tiêu đề nằm cạnh nút đóng ở đầu màn hình. Để căn chỉnh chúng theo chiều ngang, hãy bọc cả hai trong một container nằm ngang. Để thêm tiêu đề: 1. Nhấn **+** > **Text** > **H1**. 2. Với H1 đang được chọn, mở tab **Design** và chỉnh sửa văn bản trong trường **Content**. Để nhóm tiêu đề với nút đóng: 1. Trong bảng **Layers**, nhấp vào menu ba chấm Context menu trên layer nút đóng và chọn **Wrap** > **Wrap in Horizontal Container**. 2. Kéo layer H1 vào horizontal container vừa tạo. Để căn chỉnh hai phần tử: 1. Điều chỉnh kích thước nút đóng và cỡ chữ H1 sao cho chúng nằm thoải mái trên cùng một dòng. 2. Với container ngang đang được chọn, thiết lập căn chỉnh và phân bổ trong panel bên phải để các phần tử được sắp xếp đúng vị trí. ## 4. Thêm danh sách sản phẩm \{#4-add-the-product-list\} Thêm các sản phẩm để người dùng lựa chọn. Đánh dấu một sản phẩm làm mặc định để màn hình có trạng thái có nghĩa khi tải lần đầu. Để tìm hiểu thêm về quản lý sản phẩm, xem [Thiết lập mua hàng](paywall-product-block). Để thêm và cấu hình sản phẩm: 1. Nhấn **+** > **Products** và chọn kiểu bố cục. Vertical List phù hợp với template này. 2. Chọn từng thẻ sản phẩm trên canvas và chọn sản phẩm từ danh sách thả xuống trong tab **Design**. 3. Chọn thẻ bạn muốn được chọn mặc định — ví dụ, Pro+ — và bật **Set as default product** trong tab **Design**. ## 5. Thêm danh sách tính năng cho sản phẩm đầu tiên \{#add-the-feature-list-for-the-first-product\} Danh sách tính năng đầu tiên mô tả sản phẩm mặc định. Nó chỉ hiển thị khi người dùng chọn sản phẩm đầu tiên. Để tìm hiểu thêm về hiển thị có điều kiện, xem [Hiển thị có điều kiện](onboarding-element-visibility). :::tip Thay vì dùng hai danh sách riêng biệt, bạn có thể thêm một danh sách duy nhất và đặt điều kiện hiển thị cho các phần tử văn bản bên trong, để một danh sách tự thích nghi theo sản phẩm đã chọn. Xem [Thêm văn bản có điều kiện](onboarding-text#add-conditional-text). ::: Để thêm và cấu hình danh sách tính năng: 1. Nhấn **+** > **List** và chọn một preset danh sách dạng compact. Icon List phù hợp tốt với paywall. 2. Với mỗi hàng được chọn, chỉnh sửa tiêu đề trong trường **Content** để mô tả các tính năng của sản phẩm đầu tiên. 3. Khi danh sách vẫn đang được chọn, mở tab **Design**. Trong phần **Visibility**, chọn **Conditional** Conditional. 4. Thiết lập điều kiện sao cho danh sách chỉ hiển thị khi sản phẩm đầu tiên là sản phẩm đang được chọn. Khớp với biến `products.selectedProduct.prod_title`. Ở phần **Value**, nhấn vào biểu tượng biến `{}`, chọn thẻ sản phẩm đầu tiên, rồi chọn thuộc tính `prod_title` của nó — phép so sánh sẽ trả về tiêu đề của sản phẩm đó. ## 6. Thêm danh sách tính năng cho sản phẩm thứ hai \{#6-add-the-feature-list-for-the-second-product\} Lặp lại cách làm tương tự cho sản phẩm thứ hai. Hai danh sách này loại trừ lẫn nhau — chỉ một danh sách được hiển thị tại một thời điểm, tùy thuộc vào sản phẩm nào đang được chọn. Để thêm danh sách tính năng cho sản phẩm thứ hai: 1. Nhấp **+** > **List** và chọn cùng preset compact để giao diện đồng nhất. 2. Chỉnh sửa từng hàng để mô tả các tính năng của sản phẩm thứ hai. 3. Trong mục **Visibility**, chọn **Conditional** Conditional và thiết lập điều kiện tương tự như bước 5, nhưng trỏ bộ chọn biến **Value** vào `prod_title` của thẻ sản phẩm thứ hai. ## 7. Thêm nút mua hàng \{#add-the-purchase-button\} Nút mua hàng sẽ khởi động in-app purchase cho sản phẩm mà người dùng đã chọn. Nhãn của nút hiển thị giá của sản phẩm được chọn, vì vậy nó sẽ cập nhật khi người dùng chuyển đổi giữa các gói. Để biết thêm về hành động Purchase, xem [Actions — Purchase](onboarding-actions#purchase). Để thêm và cấu hình nút mua hàng: 1. Nhấp **+** > **Buttons** và chọn một preset nút. 2. Khi nút được chọn, mở tab **Design** và đặt con trỏ vào trường **Content**. Nhấp vào biểu tượng biến Variable icon, chọn `products.selectedProduct`, rồi chọn thuộc tính `prod_price` — biến đầy đủ sẽ thành `products.selectedProduct.prod_price`. Bao quanh nó bằng phần còn lại của nhãn — ví dụ: `Subscribe for {prod_price}`. 3. Chuyển sang tab **Interactions** và nhấn **Add trigger** > **On tap** > **Add action**. 4. Đặt **Action** thành **Purchase** và **Product** thành `products.selectedProduct`. ## 8. Thêm các liên kết ở chân trang \{#add-the-footer-links\} Điều khoản sử dụng, chính sách bảo mật và khôi phục mua hàng nằm bên dưới nội dung chính. Để thêm các liên kết ở chân trang: 1. Nhấp **+** > **Buttons** > **Links**. Thao tác này thêm một hàng gồm Restore Purchases, Terms of Use và Privacy Policy vào cuối cây layer. 2. Trong bảng **Layers**, chọn nút **Terms of Use**. Mở tab **Interactions** và dán URL điều khoản của bạn vào trường **Open URL**. 3. Lặp lại với nút **Privacy Policy** bằng URL chính sách bảo mật của bạn. 4. Giữ nguyên liên kết **Restore Purchases**. Hành động của nó đã được cấu hình sẵn. ## Các bước tiếp theo \{#next-steps\} - [Lưu và xuất bản flow của bạn](builder-save-publish). - [Thêm flow vào một placement](create-placement) để bắt đầu hiển thị cho người dùng. --- # File: show-offer-on-close --- --- title: "Hiển thị ưu đãi khi người dùng nhấn đóng" description: "Chặn lần nhấn đóng đầu tiên để hiển thị ưu đãi cơ hội cuối cùng trước khi người dùng rời khỏi paywall." --- Khi người dùng nhấn nút đóng, họ sắp rời đi mà không thanh toán. Recipe này chặn lần nhấn đóng đầu tiên: thay vì đóng paywall, nó hiển thị một overlay ưu đãi cơ hội cuối cùng. Khi người dùng tắt ưu đãi đó, nút đóng hoạt động bình thường và đóng flow. Logic này chỉ cần một biến Boolean tùy chỉnh và một hành động có điều kiện: - `close_tapped` bắt đầu là `False`. - Lần đầu tiên nhấn đóng sẽ hiển thị overlay ưu đãi và đặt `close_tapped` thành `True`. - Bất kỳ lần nhấn đóng nào sau đó sẽ đóng flow. ## Trước khi bắt đầu \{#before-you-start\} - Xây dựng màn hình paywall có nút đóng — ví dụ, làm theo hướng dẫn [Tạo màn hình paywall cơ bản](basic-paywall-screen). - [Tạo một sản phẩm](create-product) kèm [ưu đãi](offers) để quảng bá trong overlay. ## 1. Tạo biến \{#create-the-variable\} Để tìm hiểu thêm về biến tùy chỉnh, xem [Biến](onboarding-variables#custom-variables). 1. Trong bảng bên trái, nhấp vào biểu tượng **{ }** để mở **Variables**. 2. Trên tab **Custom**, nhấp **+**. 3. Đặt tên biến là `close_tapped` và đặt **Value Type** thành **Boolean**. Giữ **Initial Value** là **False**. 4. Nhấp **Create variable**. ## 2. Xây dựng lớp phủ ưu đãi \{#2-build-the-offer-overlay\} Lớp phủ là một container ghim vào màn hình thiết bị và nằm trên paywall. Hãy để nó hiển thị trong lúc xây dựng — các phần tử ẩn không thể chỉnh sửa được, vì vậy bạn sẽ ẩn nó ở bước 4, sau khi đã đặt nội dung vào đúng chỗ. 1. Nhấn **+** > **Layout** > **Vertical Container**. 2. Với container đã được chọn, mở tab **Design** và đặt **Position** thành **Fixed**. Đặt căn chỉnh ngang thành **Left & Right** và căn chỉnh dọc thành **Top**. Vì không có tùy chọn căn giữa theo chiều dọc, hãy nhập giá trị offset từ trên xuống (ví dụ: `300`) để đưa overlay về giữa màn hình. 3. Trong mục **Fill**, đặt nền — màu đơn sắc hoặc hình ảnh. Container mặc định trong suốt, vì vậy nếu không có fill, paywall phía dưới overlay vẫn sẽ hiển thị. 4. Thêm nội dung ưu đãi. Với container đã chọn trong panel **Layers**, nhấn **+** > **Text** > **H2**. Để hiển thị giá đã giảm, chèn [biến ưu đãi](onboarding-variables#product-variables) như `offer_price` vào trường **Content**. 5. Nhấn **+** > **Products**, chọn một preset bố cục, rồi kéo vào overlay. Chọn thẻ sản phẩm trên canvas và chọn sản phẩm cùng ưu đãi trong tab **Design**. 6. Nhấn **+** > **Buttons**, chọn một preset nút, rồi kéo vào overlay. Trong tab **Interactions**, nhấn **Add trigger** > **On tap** > **Add action**, sau đó đặt **Action** thành **Purchase** và **Product** thành sản phẩm ưu đãi của bạn. ## 3. Thêm nút đóng vào overlay \{#3-add-the-dismiss-button-to-the-overlay\} Overlay cần có nút đóng riêng. Hành động của nút này phải ẩn overlay — không phải đóng flow. 1. Chọn overlay, nhấn **+** > **Buttons** > **Close flow**. 2. Với nút đã được chọn, mở tab **Interactions**. Preset đi kèm với hành động **Close Flow** được cấu hình sẵn. Nhấn vào hành động đó và đổi kiểu thành **Hide element**. Đặt mục tiêu là container của overlay. :::important Đừng để nguyên hành động **Close Flow** mặc định trên nút đóng của overlay — nó sẽ đóng toàn bộ flow thay vì ẩn overlay. ::: ## 4. Ẩn overlay \{#hide-the-overlay\} Overlay phải luôn ẩn cho đến lần đầu người dùng nhấn nút đóng. Trong bảng **Layers**, chọn container overlay và đặt trạng thái của nó thành **Hide** Hide. Overlay vẫn còn trong cây layer nhưng không còn hiển thị trên canvas nữa. ## 5. Thiết lập nút đóng \{#5-set-up-the-close-button\} Thay thế hành động mặc định của nút đóng bằng một hành động có điều kiện phân nhánh dựa trên `close_tapped`. Để tìm hiểu thêm về hành động có điều kiện, xem [Hành động — Hành động có điều kiện](onboarding-actions#conditional-actions). 1. Chọn nút đóng của paywall — nút trên màn hình, không phải nút đóng của overlay. 2. Mở tab **Interactions**, nhấp vào action **Close Flow** đã được cấu hình sẵn, và đổi loại của nó thành **Conditional Action**. 3. Trong khối **if**, nhấp **Add condition** và đặt: `close_tapped` **Equals** **False**. 4. Trong khối **then**, thêm hai action: - **Set Variable**: Đặt `close_tapped` thành **True**. - **Show element**: Đặt mục tiêu là lớp phủ ưu đãi. 5. Trong khối **else**, thêm action **Close Flow**. Bây giờ lần nhấn đóng đầu tiên sẽ hiển thị ưu đãi. Sau khi người dùng bỏ qua ưu đãi đó, nhấn đóng lần nữa sẽ khớp với nhánh **else** và đóng flow. ## Các bước tiếp theo \{#next-steps\} - [Lưu và xuất bản flow của bạn](builder-save-publish). - [Thêm flow vào một placement](create-placement) để bắt đầu hiển thị cho người dùng. --- # File: strikethrough-price --- --- title: "Hiển thị giá bị gạch ngang kèm huy hiệu giảm giá" description: "Gạch ngang giá của gói tháng bên cạnh giá hiệu quả hàng tháng của gói năm để người dùng nhìn thấy mức giảm giá ngay lập tức." --- Một mức giá bị gạch ngang đặt cạnh giá thực sẽ giúp người dùng nhận ra ngay mức chiết khấu. Recipe này chỉnh sửa thẻ sản phẩm theo năm để hiển thị giá gói tháng — được gạch ngang — bên cạnh giá hiệu quả hàng tháng của gói năm, phía trên có thêm huy hiệu giảm giá. Định dạng gạch ngang áp dụng cho toàn bộ một phần tử văn bản — bạn không thể gạch ngang một biến số bên trong một đoạn văn bản lớn hơn. Đó là lý do giá bị gạch ngang nằm trong phần tử văn bản riêng của nó, đặt cạnh giá đã giảm trong một container nằm ngang. :::tip Nếu chỉ cần hiển thị giá "trước đây" bị gạch ngang cho cùng một sản phẩm — ví dụ: gấp đôi giá hiện tại, có gạch ngang — hãy dùng [phần tử Old Price](onboarding-text#add-an-old-price) có sẵn. Phần hướng dẫn này dành cho trường hợp giá bị gạch ngang là giá thực của một sản phẩm khác. ::: ## Trước khi bắt đầu \{#before-you-start\} - Xây dựng màn hình paywall với các sản phẩm — ví dụ, hãy làm theo [Tạo màn hình paywall cơ bản](basic-paywall-screen). Hướng dẫn này giả định paywall có cung cấp gói đăng ký hàng năm và hàng tháng. ## 1. Xếp chồng các dòng giá \{#1-stack-the-price-rows\} Thẻ hàng năm bắt đầu với một dòng giá duy nhất — biến `prod_price` của sản phẩm hàng năm theo sau là `/year`, hiển thị như `$29.99/year`. Thêm một dòng thứ hai phía trên để hiển thị giá hàng tháng. 1. Trong bảng **Layers**, chọn text giá bên trong thẻ sản phẩm hàng năm. Text này đã nằm trong container dọc của thẻ, vì vậy bản sao sẽ xếp bên dưới nó. 2. Nhấn vào menu ba chấm Context menu trên layer và chọn **Duplicate**. Bản sao xuất hiện bên dưới bản gốc. Ở bước tiếp theo, text phía trên sẽ là dòng giá hàng tháng; text phía dưới giữ nguyên giá hàng năm. ## 2. Tách hàng tháng thành hai mức giá \{#2-split-the-monthly-row-into-two-prices\} Hàng tháng chứa hai phần tử văn bản: giá gói tháng bị gạch ngang và giá mỗi tháng của gói năm. :::important Bộ chọn biến chỉ hiển thị giá của các sản phẩm có mặt trên một màn hình flow. Để tham chiếu đến một sản phẩm không có trong flow, hãy tạo một màn hình trống, thêm phần tử **Products** vào đó và gán sản phẩm cho nó. Đảm bảo không có hành động điều hướng nào dẫn đến màn hình đó. ::: 1. Chọn phần tử văn bản ở trên cùng, nhấp vào menu ba chấm và chọn **Wrap** > **Wrap in Horizontal Container**. 2. Nhấp vào menu ba chấm trên layer văn bản bên trong container mới và chọn **Duplicate**. 3. Chọn phần tử văn bản đầu tiên và xóa trường **Content** của nó. Nhấp vào biểu tượng biến Variable icon, chọn sản phẩm theo tháng, rồi chọn thuộc tính `prod_price_per_month` của nó. Gõ `/month` sau biến. 4. Trong tab **Design**, ở mục **Typography**, đặt **Decoration** thành **Strikethrough** Strikethrough (xem [Kiểu dáng và giao diện — Decoration](builder-styling#decoration)). 5. Chọn phần tử văn bản thứ hai và thay thế nội dung theo cách tương tự, nhưng chọn thuộc tính `prod_price_per_month` của sản phẩm theo năm. Gõ `/month` sau biến. ## 3. Làm nhạt giá theo năm \{#tone-down-the-yearly-price\} Các mức giá theo tháng được nhân đôi đã kế thừa kiểu định dạng giá gốc, nên cả ba mức giá hiện đang có cùng kích thước và độ đậm. Hãy làm cho giá theo năm trông nhẹ hơn để hàng theo tháng nổi bật hơn. 1. Trên canvas, chọn phần tử văn bản ở dưới cùng — giá theo năm. 2. Trong thanh công cụ phía trên phần tử, mở dropdown kiểu chữ và chọn một kiểu nhẹ hơn — ví dụ, **Caption**. Hoặc chọn kiểu khác trong phần **Typography** trên tab **Design**. ## 4. Thêm huy hiệu giảm giá \{#add-the-discount-badge\} Huy hiệu này làm nổi bật mức tiết kiệm bên cạnh giá. 1. Nhấn **+** > **Badge**. 2. Trong bảng **Layers**, kéo huy hiệu vào bên trong sản phẩm theo năm, giữa container dọc chứa các mức giá và nút radio. 3. Chọn layer văn bản của huy hiệu và chỉnh trường **Content** — ví dụ: `Save 75%`. Không có biến nào cho phần trăm giảm giá — bạn cần tự tính từ giá của mình và nhập dưới dạng văn bản tĩnh. Giờ đây thẻ yearly neo giá của mình theo gói monthly: giá monthly bị gạch ngang, giá monthly hiệu quả của gói yearly, và badge giảm giá nằm trên một dòng, với giá yearly đầy đủ bên dưới. ## Các bước tiếp theo \{#next-steps\} - [Lưu và xuất bản flow của bạn](builder-save-publish). - [Thêm flow vào một placement](create-placement) để bắt đầu hiển thị cho người dùng. --- # File: onboarding-flow-tutorial --- --- title: "Xây dựng flow onboarding cá nhân hóa" description: "Hướng dẫn toàn bộ quy trình xây dựng flow onboarding nhiều màn hình — các màn hình, nội dung, điều hướng, và phân nhánh có điều kiện — thông qua một ví dụ thực tế." --- Một flow nhiều màn hình trong Flow Builder là một chuỗi các màn hình được kết nối với nhau thông qua các hành động điều hướng. Flow có thể đi theo đường thẳng, hoặc phân nhánh dựa trên thông tin người dùng nhập ở màn hình trước. Hướng dẫn này sẽ đi qua toàn bộ quá trình — tạo màn hình, xây dựng nội dung, thiết lập điều hướng và thêm phân nhánh có điều kiện — sử dụng một flow onboarding bốn màn hình làm ví dụ thực tế. Ví dụ sử dụng: - Một **ô nhập tên** hiển thị tên người dùng dưới dạng biến để cá nhân hóa nội dung. - Một **câu hỏi trắc nghiệm một lựa chọn** mà câu trả lời sẽ quyết định màn hình tiếp theo người dùng thấy. - **Hai nhánh phân luồng** với nội dung riêng biệt cho từng phân khúc đối tượng. - Một **paywall** là màn hình cuối cùng. Cách làm tương tự áp dụng cho bất kỳ flow nào cần cá nhân hóa nội dung dựa trên đầu vào của người dùng. Thích xem video hơn? Hướng dẫn quickstart này sẽ đưa bạn qua toàn bộ quy trình từ đầu đến cuối: <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/aa-m459VIuY?si=zN_Co6B6qB88UPZP" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ## Trước khi bắt đầu \{#before-you-start\} - [Tạo sản phẩm](create-product) trong Adapty Dashboard. Flow ví dụ sử dụng hai sản phẩm — gói đăng ký Hàng năm và Hàng tháng. - [Kết nối Adapty với App Store và Google Play](integrate-payments). ## 1. Thiết lập các style có thể tái sử dụng \{#set-up-reusable-styles\} Các style có thể tái sử dụng giúp bạn áp dụng typography và màu sắc nhất quán trên mọi màn hình chỉ với một cú nhấp. Style màu sắc có cả biến thể Sáng và Tối, vì vậy flow tự động hỗ trợ cả hai chủ đề. Để xem hướng dẫn đầy đủ, hãy tham khảo [Style và giao diện — Các style có thể tái sử dụng](builder-styling#reusable-styles). Để thiết lập các style: 1. Trong bảng bên trái, mở bảng **Styles** Styles. 2. Trên tab **Colors**, nhấn **Plus Create style** và thêm các màu bạn muốn tái sử dụng. Với mỗi màu, chọn giá trị Light, chuyển sang tab Dark và chọn giá trị Dark. 3. Trên tab **Text**, nhấn vào một style hiện có để chỉnh font, độ đậm và kích thước, hoặc nhấn **Plus Create style** để thêm preset tùy chỉnh. ## 2. Tạo các màn hình \{#2-create-the-screens\} Một flow là một chuỗi các màn hình. Hãy cấu hình màn hình đầu tiên với nền chung — bố cục, nền và safe area — rồi nhân đôi nó cho các màn hình còn lại. Bằng cách này, mọi màn hình đều dùng chung một nền tảng và bạn chỉ cần thiết lập một lần. Để tìm hiểu thêm về quản lý màn hình, xem [Màn hình và Layers — Quản lý màn hình](paywall-layout-and-products#manage-screens). Để thiết lập các màn hình: 1. Nhấn vào vùng trống trên canvas của màn hình đầu tiên để mở cài đặt màn hình. 2. Trong **System UI**, tắt **Safe area** để nền và các thành phần căn sát cạnh có thể kéo dài đến tận rìa màn hình. 3. Trong **Fill**, chọn loại nền và cấu hình nó — ví dụ, một **Image** Image hiển thị phía sau mọi màn hình của flow. 4. Trong **Layout**, đặt hướng thành **Vertical** Vertical và chọn kiểu phân bổ phù hợp với thiết kế của bạn. 5. Trong phần **Screens** của bảng bên trái, nhấp vào menu ba chấm Context menu trên màn hình đầu tiên và chọn **Duplicate**. Lặp lại cho đến khi bạn có bốn màn hình — nhánh phân nhánh thứ hai sẽ được thêm sau bằng cách nhân đôi màn hình đầu tiên. 6. Đổi tên từng màn hình theo chức năng của nó — trong ví dụ của chúng ta: `Welcome`, `Quiz`, `Rock path`, và `Paywall`. ## 3. Xây dựng màn hình giới thiệu \{#build-the-introduction-screen\} Màn hình đầu tiên thường tạo ấn tượng ban đầu — một tiêu đề, danh sách tính năng và lời kêu gọi hành động để mở phần còn lại của flow. Trong ví dụ của chúng ta, đây là màn hình Welcome. Nhấp vào màn hình **Welcome** trong bảng **Screens**, sau đó thêm các phần tử: 1. Thêm hình ảnh chính. Nhấp **+** > **Media** > **Image**, tải lên hình ảnh của bạn và điều chỉnh lề nếu cần. 2. Thêm tiêu đề: nhấp **+** > **Text**, chọn kiểu tiêu đề từ các text style đã lưu, rồi chỉnh nội dung trong trường **Content**. 3. Thêm danh sách tính năng. Nhấp **+** > **List** > **Icon Cards**, sau đó chỉnh icon và nhãn trong từng card. 4. Thêm nút điều hướng chính ở phía dưới. Nút này sẽ được gắn action ở bước cấu hình điều hướng. ## 4. Xây dựng màn hình nhập liệu và câu hỏi \{#build-the-input-and-quiz-screen\} Màn hình thứ hai thu thập thông tin từ người dùng. Trong ví dụ này, màn hình sẽ hỏi tên và một câu hỏi lựa chọn đơn để xác định hành trình tiếp theo của người dùng. Để tìm hiểu thêm về nhập liệu và câu hỏi, xem [Nhập liệu và biểu mẫu](builder-inputs-and-forms) và [Khảo sát và câu hỏi](onboarding-quizzes). Nhấp vào màn hình **Quiz** trong bảng **Screens**, sau đó thêm các phần tử. Mỗi nhóm trên màn hình — phần giới thiệu, câu hỏi + nhập liệu, câu hỏi + quiz — nằm trong một Vertical Container riêng để các phần tử liên quan được nhóm lại với nhau về mặt trực quan. 1. Thêm tiêu đề và nội dung giới thiệu. Nhấn **+** > **Text** > **H1** cho tiêu đề và **+** > **Text** > **Body** cho đoạn văn hỗ trợ. 2. Nhóm phần giới thiệu lại. Nhấn **+** > **Layout** > **Vertical Container**, kéo container mới lên đầu cây layer, rồi kéo H1 và body vào bên trong. 3. Thêm câu hỏi đầu tiên và ô nhập liệu. Nhấn **+** > **Text** cho tiêu đề câu hỏi, rồi nhấn **+** > **Inputs** > **Text** cho ô nhập. 4. Đặt **Element ID** cho ô nhập trong tab **Design** — trong ví dụ này là `name`. Điều này giúp hiển thị giá trị dưới dạng biến mà các màn hình khác có thể tham chiếu. 5. Nhóm tiêu đề và ô nhập liệu vào một Vertical Container theo cách tương tự như phần giới thiệu. 6. Thêm câu hỏi thứ hai và quiz. Nhấn **+** > **Text** để tạo tiêu đề, sau đó nhấn **+** > **Quiz** và chọn một layout preset như Icon Options. Cấu hình các tùy chọn — trong ví dụ của chúng tôi là `Rock` và `Hip hop`. 7. Nhóm tiêu đề và quiz vào một Vertical Container theo cách tương tự. 8. Đặt ID cho các tùy chọn. Chọn từng tùy chọn quiz, mở tab **Interactions** và đặt **Element ID** cho nó. Các ID này sẽ được tham chiếu trong phần điều hướng có điều kiện ở bước sau. 9. Chuyển quiz sang chế độ chọn một đáp án: nhấp vào vùng trống trên canvas để mở **Screen settings**, cuộn xuống **Selectable Groups**, nhấp vào tên nhóm quiz, rồi đặt kiểu thành **Single choice**. 10. Thêm một nút chính ở phía dưới — đây là nút Next dùng để kích hoạt phân nhánh. ## 5. Xây dựng nhánh đầu tiên \{#build-the-first-branching-path\} Mỗi màn hình trong một nhánh sẽ hiển thị nội dung phù hợp với một phân khúc đối tượng cụ thể. Trong ví dụ của chúng ta, nhánh Rock hiển thị nội dung liên quan đến nhạc rock — danh sách phát, nghệ sĩ và các gợi ý. Để tìm hiểu thêm về biến, xem [Biến](onboarding-variables). Để xây dựng màn hình: 1. Trong bảng **Screens**, nhấp vào màn hình **Rock path**. 2. Thêm tiêu đề. Đặt con trỏ vào trường **Content** tại vị trí muốn hiển thị nội dung được cá nhân hóa, nhấp vào biểu tượng biến Variable icon, và mở tab **Elements**. Chọn màn hình chứa ô nhập liệu — trong ví dụ của chúng ta là **Quiz** — rồi chọn biến giá trị của ô nhập liệu đó. Bộ chọn sẽ hiển thị nó dưới dạng `<elementId>.value` — trong ví dụ của chúng ta là `name.value`. Khi chạy ứng dụng, tiêu đề sẽ cập nhật theo nội dung người dùng đã nhập. 3. Thêm nội dung chính dưới dạng các phần tử văn bản bổ sung, được điều chỉnh phù hợp với phân khúc đối tượng cho path này. 4. Thêm một nút chính ở phía dưới. ## 6. Xây dựng nhánh đường dẫn thứ hai \{#build-the-second-branching-path\} Các màn hình đường dẫn thường có cùng bố cục — chỉ khác nội dung. Hãy nhân đôi màn hình đường dẫn đầu tiên và cập nhật nội dung. Để nhân đôi và cập nhật: 1. Trong bảng **Screens**, chọn màn hình đường dẫn đầu tiên và nhấn ⌘D / Ctrl+D để nhân đôi. Bản sao sẽ xuất hiện ở cuối danh sách màn hình. 2. Đổi tên bản sao — trong ví dụ của chúng ta là `Hip hop path` — rồi kéo nó về đúng vị trí trong danh sách màn hình, sao cho nó nằm cạnh màn hình được nhân đôi từ đó. 3. Cập nhật nội dung chính cho phân khúc đối tượng còn lại. Tiêu đề cá nhân hóa vẫn hoạt động bình thường — biến số được giữ nguyên. ## 7. Xây dựng paywall \{#build-the-paywall\} Màn hình cuối cùng là paywall — nơi người dùng có thể đăng ký. Để xem hướng dẫn chi tiết hơn về cơ chế paywall, xem [Tạo màn hình paywall cơ bản](basic-paywall-screen). Phiên bản dưới đây tóm tắt lại hướng dẫn đó. Nhấp vào màn hình **Paywall** trong panel **Screens**, sau đó thêm các thành phần: 1. Thêm một **Horizontal Container** ở đầu trang và thêm nút **Close** bên trong. Preset Close đã được cấu hình sẵn. 2. Thêm hình ảnh chính, tiêu đề (với biến cá nhân hóa giống như trên các màn hình path), và tiêu đề phụ làm nội dung hỗ trợ. 3. Thêm sản phẩm: nhấn **+** > **Products** và chọn **Vertical List**. Gán sản phẩm cho từng thẻ từ menu thả xuống trong tab **Design**. 4. Nhấn vào thẻ sản phẩm mặc định và bật **Set as default product** để sản phẩm đó được chọn sẵn khi màn hình tải lên. 5. Thêm nút mua hàng. Nhấn **+** > **Buttons** và chọn một preset. Trong tab **Interactions**, nhấn **Add trigger** > **On tap** > **Add action** và đặt **Action** thành **Purchase** với **Product** là `products.selectedProduct`. 6. Thêm template **Button** > **Links** vào màn hình. Template này bao gồm ba liên kết ở chân trang: Restore Purchases, Terms of Use, và Privacy Policy. Liên kết Restore đã được cấu hình sẵn. Để cấu hình các liên kết khác, chọn phần tử button, mở tab **Interactions**, và đặt mục tiêu cho action **Open URL**. ## 8. Kết nối điều hướng giữa các màn hình \{#wire-navigation-between-the-screens\} Các màn hình không tự động kết nối với nhau. Dùng trigger **On tap** và action **Navigate to** để kết nối nút chính của từng màn hình với màn hình tiếp theo. Màn hình phân nhánh dựa trên dữ liệu người dùng nhập thì dùng **Conditional action** thay thế. Để tìm hiểu thêm về điều hướng và conditional action, xem [Navigation and interaction](onboarding-navigation-branching) và [Actions — Conditional actions](onboarding-actions#conditional-actions). Để kết nối điều hướng cho flow ví dụ: 1. **Điều hướng tĩnh từ màn hình giới thiệu.** Mở màn hình Welcome, chọn nút chính, rồi chuyển sang tab **Interactions**. Nhấp **Add trigger** > **On tap** > **Add action**, đặt **Action** thành **Navigate to**, và chọn màn hình tiếp theo — trong ví dụ này là màn hình Quiz. 2. **Điều hướng có điều kiện từ quiz.** Mở màn hình Quiz, chọn nút Next, và thêm trigger **On tap** với **Conditional action**. Thiết lập quy tắc IF/ELSE: - Trong bộ chọn biến, mở tab **Elements**, chọn màn hình **Quiz**, và chọn `quiz.selectedOptionId`. - Dùng toán tử **Equals** và so sánh với ID của một trong các tùy chọn — trong ví dụ này là tùy chọn Rock. - **IF** so sánh khớp, kích hoạt **Navigate to** và chọn màn hình path đầu tiên. - **ELSE**, kích hoạt **Navigate to** và chọn màn hình path thứ hai. 3. **Điều hướng tĩnh từ mỗi nhánh đến paywall.** Lặp lại quy trình từ bước 1 trên mỗi màn hình nhánh, với paywall là đích đến. ## Các bước tiếp theo \{#next-steps\} - [Lưu và xuất bản flow của bạn](builder-save-publish). - [Thêm flow vào một placement](create-placement) để bắt đầu hiển thị cho người dùng. - Nếu muốn hiển thị flow riêng cho từng đối tượng (thay vì phân nhánh trong flow), hãy tạo các phân khúc đối tượng và gán các flow khác nhau trên trang Placement. :::link Muốn tìm hiểu thêm về cách xây dựng flow? Xem các video hướng dẫn từng bước trong [danh sách phát YouTube](https://www.youtube.com/playlist?list=PLMksWqaZiWtM) của chúng tôi. ::: --- # File: migrate-to-flows --- --- title: "Migrate sang flows" description: "Chuyển onboarding và paywall riêng lẻ của bạn thành một Adapty flow — những gì thay đổi và cách triển khai mà không làm gián đoạn người dùng trên các phiên bản ứng dụng cũ hơn." --- Trong Adapty, một *flow* kết hợp onboarding và paywall thành một thực thể duy nhất đằng sau một placement. Một flow thay thế cho onboarding và paywall riêng lẻ mà bạn xây dựng và phục vụ độc lập hiện nay. Hướng dẫn này giải thích những gì thay đổi khi bạn chuyển sang flow và cách triển khai thay đổi mà không làm gián đoạn người dùng trên các phiên bản app cũ hơn. :::important Flow hiện được hỗ trợ trên iOS, Android, React Native, Flutter và Capacitor SDK v4 trở lên. Hỗ trợ cho các nền tảng và framework khác sẽ sớm ra mắt. ::: <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/8Cby6lVGI0o?si=rYA1HtdayyF1ffWd" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ## Flow so với onboarding và paywall \{#flows-vs-onboardings-and-paywalls\} Với onboarding và paywall riêng biệt, bạn phải quản lý hai builder và hai placement. Bạn cũng phải tự viết code để chuyển người dùng từ onboarding sang paywall. Một flow thay thế cả hai bằng một trải nghiệm liền mạch — màn hình giới thiệu, quiz, và màn hình mua hàng — được xây dựng trong một editor và phục vụ từ một placement duy nhất. Bảng dưới đây so sánh những gì mỗi lựa chọn mang lại cho bạn: | | Flow | Paywall xây dựng bằng Paywall Builder | Onboarding | |---|---|---|---| | Nhiều màn hình | Có | Không — chỉ một màn hình | Có | | Hiển thị | Native | Native | WebView | | Sản phẩm và placement | Một placement; bạn thêm sản phẩm trực tiếp vào flow | Một placement; bạn thêm sản phẩm trực tiếp vào paywall | Một placement, nhưng không có sản phẩm riêng — để bán hàng, bạn tạo một paywall riêng và phục vụ nó từ placement của chính nó | ## Bạn có nên migrate không? \{#should-you-migrate\} Các onboardings và paywalls hiện tại của bạn vẫn hoạt động bình thường, và Adapty tiếp tục hỗ trợ chúng. Tuy nhiên, các tính năng mới sẽ được phát triển cho flows thay vì các builder onboarding và paywall độc lập. **Nếu bạn đang xây dựng cho lâu dài, flows là nền tảng tốt hơn** — hãy migrate sang chúng khi phù hợp với lịch phát hành của bạn. ## Cách thực hiện migration \{#how-to-migrate\} Migration gồm bốn bước. Phần lớn công sức là nâng cấp SDK một lần duy nhất — việc xây dựng và xem trước flow không cần code. 1. **[Xây dựng flow của bạn](#build-your-flow)**: Tạo flow trong editor không cần code; không cần lập trình viên. 2. **[Xem trước trên thiết bị](#preview-on-device)**: Kiểm tra flow trên thiết bị thực thông qua ứng dụng di động Adapty; không cần build app. 3. **[Tạo placement mới cho flow của bạn](#create-a-new-placement-for-your-flow)**: Tạo placement dành riêng cho flow với ID duy nhất, và quyết định cách nó cùng tồn tại với các placement hiện có. 4. **[Cập nhật SDK](#update-the-sdk)**: Nâng cấp lên iOS, Android, React Native hoặc Capacitor SDK v4, lấy flow từ placement tương ứng, và xác minh giao dịch mua trong sandbox. Đây là nhiệm vụ chính dành cho lập trình viên. ### Xây dựng flow của bạn \{#build-your-flow\} Trên trang **Flows**, nhấn **Create flow** để bắt đầu xây dựng, kết hợp onboarding và paywall của bạn thành một trải nghiệm liền mạch. Để tìm hiểu thêm về builder: - **[Tài liệu Flows](adapty-flow-builder)**: Hướng dẫn bạn qua builder và những gì bạn có thể tạo ra. - **[Các flow recipe phổ biến](flow-builder-recipes)**: Hướng dẫn từng bước cho các màn hình thông dụng nhất. - **Hỏi AI**: Dùng chat trên bất kỳ trang tài liệu nào khi bạn gặp khó khăn. :::note Tính năng tạo flow từ template có sẵn hoặc tạo tự động bằng AI hiện chưa khả dụng — cả hai sẽ sớm ra mắt. Hiện tại, mỗi flow mới sẽ bắt đầu với một số màn hình thông dụng mà bạn có thể chỉnh sửa và tùy chỉnh theo nhu cầu. ::: ### Xem trước trên thiết bị \{#preview-on-device\} Bạn có thể xem trước flow trên thiết bị thật mà không cần chỉnh sửa ứng dụng. Tải [ứng dụng Adapty](https://apps.apple.com/us/app/adapty/id6739359219) từ App Store. Sau đó, trong flow builder, nhấn **Test on device**, chọn ngôn ngữ và quét mã QR bằng thiết bị của bạn. Thao tác này sẽ hiển thị các màn hình thực tế, luồng phân nhánh, nội dung văn bản và giao diện thiết kế. :::note Trong chế độ xem trước, Adapty không thể kết nối với sản phẩm của bạn trên các cửa hàng, nên giá hiển thị trong bản xem trước không phải giá thực. Giao dịch mua thực sẽ được xác minh sau, trong bản build v4 với tài khoản sandbox — xem [Cập nhật SDK](#update-the-sdk). ::: ### Tạo một placement mới cho flow của bạn \{#create-a-new-placement-for-your-flow\} Một placement chỉ phục vụ một loại nội dung duy nhất — một flow, một paywall, hoặc một onboarding. Bạn không thể chuyển đổi placement onboarding hay paywall hiện có thành placement flow (xem [loại placement](create-placement)). Một flow cần có placement mới riêng của nó. **Đặt cho placement flow mới một placement ID hoàn toàn mới và duy nhất.** ID này không được trùng hoặc tái sử dụng ID của bất kỳ placement paywall hay onboarding nào đang tồn tại. :::warning Giữ các placement cũ của bạn hoạt động trong suốt quá trình chuyển đổi Người dùng trên các phiên bản ứng dụng cũ đã có sẵn ID placement của onboarding và paywall được biên dịch trong ứng dụng. Họ vẫn tiếp tục gọi các phương thức onboarding và paywall và thấy onboarding cũng như paywall hiện tại của bạn cho đến khi cập nhật. Chỉ ngừng sử dụng các placement cũ khi tỷ lệ áp dụng SDK v4 đã đủ cao. ::: Bạn không cần phải chuyển tất cả các vị trí sang flow cùng một lúc. Trong SDK v4, phương thức `getFlow` lấy dữ liệu từ cả flow placement lẫn paywall placement, vì vậy ứng dụng của bạn gọi cùng một phương thức ở mọi nơi. Giữ nguyên các paywall Paywall Builder ở những placement bạn muốn, và dùng flow cho phần còn lại. Trong quá trình chuyển đổi, mỗi loại placement theo dõi chỉ số riêng của nó. Khi cả phiên bản app cũ và mới đều đang hoạt động, dữ liệu của bạn sẽ được chia thành hai tập placement. Các placement onboarding và paywall cũ phục vụ các phiên bản app cũ hơn; placement flow mới phục vụ SDK v4+. Hãy so sánh chúng như các cohort riêng biệt, và lưu ý rằng tỷ lệ của placement flow sẽ tăng dần khi người dùng cập nhật app. Bạn vẫn có thể chạy A/B test với flow: chạy [A/B test thông thường](ab-tests) trên các biến thể flow ở một placement flow. A/B test đa placement hiện chỉ hỗ trợ cho paywall, nên bạn chưa thể chạy loại này trên các placement flow. Việc so sánh một flow mới với paywall cũ là so sánh cohort, không phải một bài test đơn lẻ — vì chúng thuộc hai loại placement khác nhau. ### Cập nhật SDK \{#update-the-sdk\} Sau khi placement flow đã sẵn sàng, hãy trỏ ứng dụng vào đó. Flow chỉ hoạt động trên Adapty SDK v4 trở lên. Hãy nâng cấp SDK và lấy flow từ placement mới bằng `getFlow`. Xem hướng dẫn migration lên v4 theo từng nền tảng — [iOS](migration-to-ios-sdk-v4), [Android](migration-to-android-sdk-v4), [React Native](migration-to-react-native-sdk-v4), hoặc [Capacitor](migration-to-capacitor-sdk-v4) — để biết các bước nâng cấp cụ thể. Sau khi đã kết nối flow, hãy kiểm tra nó như bất kỳ luồng mua hàng nào khác: chạy trên thiết bị thực hoặc máy giả lập và thực hiện một giao dịch mua sandbox ([iOS](ios-test) / [Android](testing-on-android)) để xác nhận rằng sản phẩm, giao dịch mua và mức độ truy cập đều hoạt động đúng. :::note Người dùng chỉ thấy flow sau khi họ cài đặt bản app được build với SDK v4+. Những ai đang dùng phiên bản app cũ hơn vẫn tiếp tục thấy onboarding và paywall hiện tại của bạn — đó là lý do các placement cũ vẫn được giữ hoạt động trong quá trình chuyển đổi. Điều tương tự cũng áp dụng cho các nền tảng chưa hỗ trợ flow. ::: --- # File: paywall-layout-and-products --- --- title: Màn hình và lớp description: "Quản lý màn hình và cấu trúc phân cấp các phần tử trong mỗi màn hình trong Flow Builder." --- Một flow bao gồm một hoặc nhiều màn hình. Mỗi màn hình đại diện cho một bước trong hành trình của người dùng — ví dụ: một paywall, một quiz, hoặc một slide thông tin sản phẩm. Các phần tử trên mỗi màn hình được tổ chức theo cấu trúc phân cấp lớp. Để quản lý màn hình, layer và các thành phần, hãy mở giao diện mặc định **Screens and Layers**. Tại đây bạn sẽ thấy trình tự màn hình và cấu trúc layer của từng màn hình. ## Quản lý màn hình \{#manage-screens\} Phần trên cùng của bảng điều khiển bên trái liệt kê tất cả các màn hình trong flow. Mỗi mục hiển thị nhãn đánh số và ảnh xem trước thu nhỏ. * **Chọn màn hình**: Nhấp vào một mục màn hình để kích hoạt nó. Trình chỉnh sửa trực quan sẽ hiển thị màn hình được chọn, và phần Layers bên dưới sẽ cập nhật để hiển thị cấu trúc phân lớp của nó. * **Thêm màn hình**: Nhấp vào nút Plus ở đầu phần Screens để thêm một màn hình trống mới vào flow. * **Mở thư viện Template**: Nhấp vào nút Templates ở đầu phần Screens để duyệt và áp dụng [các template flow](paywall-builder-templates). * **Sắp xếp lại màn hình**: Kéo và thả các mục màn hình để thay đổi thứ tự của chúng trong flow. :::important Nếu flow của bạn có các màn hình trống chưa được sử dụng, bạn sẽ không thể xuất bản. Hãy xóa các màn hình nháp trước khi xuất bản. ::: ### Hành động với màn hình \{#screen-actions\} Nhấp vào biểu tượng ba chấm Context trên một mục màn hình để mở menu ngữ cảnh. | Hành động | Phím tắt | Mô tả | |--------|----------|-------------| | **Play Animation** | | Xem trước các animation được cấu hình trên màn hình này | | **Copy** | ⌘C / Ctrl+C | Sao chép màn hình vào clipboard | | **Paste here** | ⌘V / Ctrl+V | Dán màn hình đã sao chép trước đó | | **Duplicate** | ⌘D / Ctrl+D | Tạo bản sao của màn hình và thêm vào flow | | **Rename** | | Đổi tên hiển thị của màn hình | | **Delete** | ⌘⌫ / Ctrl+Del | Xóa màn hình khỏi flow | :::tip Clipboard được giữ nguyên khi chuyển qua lại giữa các flow. Sao chép một màn hình hoặc phần tử từ flow này, mở flow khác và dán vào. ::: :::warning Khi bạn xóa một màn hình, bất kỳ hành động [Navigate to Screen](onboarding-navigation-branching) nào trỏ đến màn hình đó sẽ **mất đích đến**, nhưng bản thân hành động đó **không bị xóa**. Hãy gán đích đến mới hoặc xóa hành động đó — nếu không, bạn sẽ không thể [xem trước hoặc xuất bản flow](builder-save-publish#publish-a-flow). ::: ## Điều hướng giữa các màn hình \{#navigate-between-screens\} :::link Bài viết chính: [Điều hướng và tương tác](onboarding-navigation-branching) ::: Thứ tự màn hình trong danh sách không tự quyết định điều hướng. Để kết nối các màn hình, hãy sử dụng tương tác phần tử: cấu hình một nút để điều hướng người dùng đến màn hình khác. ## Cài đặt màn hình \{#screen-settings\} Để xem thuộc tính và cài đặt của màn hình đang hoạt động, nhấp vào vùng trống trên màn hình xem trước. Bảng bên phải sẽ chuyển sang giao diện cài đặt màn hình. ### Giao diện hệ thống \{#system-ui\} Kiểm soát cách màn hình tương tác với phần cứng thiết bị. * **Safe area** thêm padding để giữ nội dung không bị che bởi notch và thanh hệ thống. * **Status bar** hiển thị hoặc ẩn thanh trạng thái hệ thống (giờ, pin, tín hiệu). ### Đưa màn hình vào chỉ báo tiến trình \{#include-screen-in-progress-indicator\} Nếu bạn thêm phần tử [Progress Indicator](builder-loaders-and-progress-bars#progress-indicators) vào flow, Adapty sẽ hiển thị nó trên mọi màn hình. Bỏ chọn **Include screen in progress indicator** để ẩn chỉ báo tiến trình khỏi một màn hình cụ thể. Dùng tính năng này để làm gọn các màn hình chào mừng, paywall cuối cùng, hoặc bất kỳ bước nào bạn không muốn tính vào tiến trình. ### Bố cục màn hình \{#screen-layout\} :::link Bài viết đầy đủ: [Bố cục và định vị](manage-paywall-ui-elements) ::: Phần **Layout** xác định cách màn hình sắp xếp các phần tử con. Các thuộc tính này có thể dùng trên bất kỳ phần tử container nào. * **Free**: Các phần tử con được định vị độc lập với nhau. * **Vertical**: Các phần tử được sắp xếp từ trên xuống dưới, giống như flexbox column. * **Horizontal**: Các phần tử được sắp xếp từ trái sang phải, giống như flexbox row. Đối với bố cục dọc và ngang, bạn cũng có thể cấu hình khoảng cách và căn chỉnh. * **Alignment**: Vị trí của phần tử theo trục phụ. * **Gap**: Khoảng cách giữa các phần tử liền kề. * **Distribution**: Phân bổ khoảng cách giữa và xung quanh các phần tử con. #### Bố cục RTL \{#rtl-layout\} Bật checkbox **Mirror for RTL** để phản chiếu bố cục cho các hệ thống chữ viết đọc từ phải sang trái. Thứ tự các phần tử trong container ngang sẽ bị đảo ngược. ### Nền màn hình \{#screen-background\} :::link Bài viết chính: [Nền](paywall-head-picture) ::: **Fill** đặt [nền màn hình](paywall-head-picture) thành màu đơn sắc, gradient, hình ảnh hoặc video. Nền sẽ phủ toàn bộ vùng hiển thị của thiết bị, bao gồm cả khu vực phía sau notch và thanh hệ thống — ngay cả khi **Safe area** được bật. #### Phát lặp video nền \{#loop-background-video\} Bật nút gạt **Loop** để phát video nền liên tục. #### Gán ID media tùy chỉnh \{#assign-a-custom-media-id\} Cũng giống như [bất kỳ hình ảnh hoặc video nào](custom-media), bạn có thể gán cho nền màn hình một custom media ID để tham chiếu trong SDK của mình. ### Khoảng cách màn hình \{#screen-spacing\} Điều chỉnh padding của màn hình cho từng cạnh (trên, phải, dưới, trái). ### Cuộn \{#scroll\} Kiểm soát hành vi tràn nội dung. Bật **Vertical scroll** để cho phép cuộn nội dung màn hình khi vượt quá chiều cao viewport. ### Nhóm có thể chọn \{#selectable-groups\} :::link Bài viết chính: [Các phần tử và nhóm có thể chọn](flow-selectable-elements) ::: Phần **Selectable groups** liệt kê tất cả các nhóm có thể chọn trên màn hình hiện tại — từ [quiz](onboarding-quizzes), [sản phẩm](paywall-product-block), [tab](builder-tabs), [nút bật/tắt dùng thử](builder-toggles), hoặc bất kỳ [phần tử có thể chọn tùy chỉnh](flow-selectable-elements#make-an-element-selectable) nào. Nhấn vào một mục nhóm để đổi tên, thay đổi loại, xem các biến mà nhóm đó cung cấp, hoặc xóa nhóm đó. ## Quản lý layer \{#manage-layers\} Mỗi phần tử trên màn hình được biểu diễn dưới dạng một layer. Phần Layers hiển thị thứ tự các phần tử trên màn hình đang hoạt động. :::important Các layer trong Flow không chồng lên nhau như các layer trong phần mềm thiết kế đồ họa. Thay vào đó, chúng đại diện cho các thành phần riêng lẻ của màn hình. Các phần tử chỉ chồng lên nhau *nếu* chúng sử dụng [định vị tuyệt đối hoặc cố định](manage-paywall-ui-elements). Thứ tự xếp chồng được xác định bởi thuộc tính `z-index`, không phải vị trí của chúng trong cây layer. ::: Cấu trúc cây phản ánh mối quan hệ cha-con. Nhấn vào mũi tên trên bất kỳ layer cha nào để mở rộng hoặc thu gọn các layer con. Bạn không thể tạo layer trực tiếp. Mọi thành phần được thêm qua giao diện [Thêm thành phần](builder-elements) sẽ tự động xuất hiện dưới dạng một layer mới trong cây. * **Chọn một layer**: Nhấp vào một layer để chọn nó. Trình soạn thảo trực quan sẽ làm nổi bật phần tử tương ứng trên canvas, và bảng bên phải hiển thị các thuộc tính [thiết kế](builder-styling) và [tương tác](onboarding-navigation-branching) của nó. * **Sắp xếp lại các layer**: Kéo và thả các layer trong cây để thay đổi thứ tự của chúng bên trong container cha. Thứ tự trong cây khớp với thứ tự hiển thị trên màn hình. * **Hiện hoặc ẩn một layer**: Di chuột qua một layer để hiện biểu tượng mắt Eye ở phía bên phải. Nhấp vào đó để bật/tắt khả năng hiển thị của layer. Các layer bị ẩn vẫn nằm trong cây nhưng không xuất hiện trong trình soạn thảo trực quan hoặc trên thiết bị. Để kiểm soát khả năng hiển thị bằng logic khi chạy, hãy dùng [điều kiện hiển thị](onboarding-element-visibility). * **Thu gọn tất cả các layer**: Nhấp vào nút thu gọn Collapse ở góc trên bên phải của phần Layers để gấp toàn bộ cây lại. ### Hành động trên layer \{#layer-actions\} Nhấp vào biểu tượng ba chấm Context để mở menu ngữ cảnh. | Action | Shortcut | Description | |--------|----------|-------------| | **Copy** | ⌘C / Ctrl+C | Sao chép layer vào clipboard | | **Paste here** | ⌘V / Ctrl+V | Dán layer đã sao chép vào đây dưới dạng phần tử con | | **Duplicate** | ⌘D / Ctrl+D | Tạo bản sao của layer trong cùng container | | **Rename** | | Đổi tên hiển thị của layer. Mặc định, layer sẽ lấy nội dung hoặc loại component làm tên | | **Delete** | ⌘⌫ / Ctrl+Del | Xóa layer và tất cả các phần tử con của nó | | **Wrap** | | Bọc layer trong một container mới: **Wrap in Horizontal Container** hoặc **Wrap in Vertical Container** | | **Unwrap / Ungroup** | | Gỡ bỏ container bọc ngoài và chuyển các phần tử con lên một cấp | | **Move up** | ↑ | Di chuyển layer lên một vị trí trong container cha | | **Move down** | ↓ | Di chuyển layer xuống một vị trí trong container cha | --- # File: manage-paywall-ui-elements --- --- title: "Bố cục và vị trí" description: "Sắp xếp các phần tử trên màn hình với bố cục, chế độ vị trí, kích thước và khoảng cách." --- Flow Builder tạo ra các layout responsive. Bạn không kéo thả các phần tử vào tọa độ cụ thể — thay vào đó, bạn lồng chúng vào bên trong các **container** để tự động sắp xếp các phần tử con. Container quyết định hướng sắp xếp (dọc hoặc ngang), căn chỉnh và khoảng cách của các phần tử. Từng phần tử có thể tự tinh chỉnh kích thước và margin, hoặc — khi cần — thoát khỏi flow với định vị tuyệt đối (absolute) hoặc cố định (fixed). :::link Để biết các thuộc tính trực quan như fill, border và hiệu ứng, xem [Styles and appearance](builder-styling). ::: <Tabs groupId="video"> <TabItem value="align" label="Căn chỉnh & định vị"> <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/aRS4Bzb6W4I?si=qH7B6t3kMab70gBi" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ``` </TabItem> <TabItem value="layout" label="Layout, kích thước & khoảng cách"> ``` <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/WQ9fpxrndok?si=ROMdIPvJ32tSwUX6" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> </TabItem> </Tabs> ## Bố cục \{#layout\} Bố cục là công cụ chính để sắp xếp các phần tử trên màn hình. Mỗi container tự động phân bổ các phần tử con theo một tập hợp quy tắc — hướng, căn chỉnh và khoảng cách. Các phần tử bố cục có trong builder bao gồm: * **[Vertical Container](builder-containers#containers)**: Sắp xếp các phần tử con từ trên xuống dưới * **[Horizontal Container](builder-containers#containers)**: Sắp xếp các phần tử con từ trái sang phải * **[Divider](builder-containers#dividers)**: Đường phân cách trực quan giữa các phần tử * **[Carousel](builder-containers#carousel)**: Tập hợp các slide có thể cuộn ngang * **[Bottom Sheet](builder-containers#bottom-sheet)**: Bảng trượt lên từ phía dưới, hiển thị thêm nội dung khi người dùng nhấn vào một nút Containers là các thành phần xây dựng chính của màn hình. Bạn có thể lồng chúng vào nhau để tạo bố cục phức tạp. Mỗi container có mục **Layout** trong bảng bên phải để kiểm soát cách phân bố các phần tử con. Để nhóm các phần tử vào một container mới, dùng [hành động layer](paywall-layout-and-products#layer-actions) **Wrap**. Để xóa container và đưa các phần tử con lên cấp trên, dùng **Unwrap**. :::link Để biết thêm chi tiết về màn hình và hệ thống phân cấp layer, xem [Màn hình và Layer](paywall-layout-and-products). ::: ### Hướng \{#direction\} * Free **Free**: Không có bố cục tự động. Các phần tử con được đặt vị trí độc lập (hữu ích khi các phần tử con sử dụng định vị tuyệt đối) * Vertical **Vertical**: Các phần tử con xếp chồng từ trên xuống dưới, như các hàng trong một cột * Horizontal **Horizontal**: Các phần tử con sắp xếp từ trái sang phải, như các mục trong một hàng ### Thứ tự phần tử \{#element-order\} Các phần tử con được hiển thị theo thứ tự xuất hiện trong bảng **Layers**. Trong container dọc, phần tử ở trên cùng trong danh sách sẽ hiển thị ở đầu màn hình. Trong container ngang, phần tử ở trên cùng sẽ hiển thị ở bên trái. Kéo các phần tử trong bảng Layers để sắp xếp lại thứ tự, hoặc sử dụng **Move Up** và **Move Down** trong [layer actions](paywall-layout-and-products#layer-actions). ### Căn chỉnh \{#alignment\} Lưới căn chỉnh xác định vị trí của các phần tử con theo trục ngang của container. Trong container dọc, căn chỉnh kiểm soát vị trí ngang của các phần tử con (trái, giữa, hoặc phải). Trong container ngang, nó kiểm soát vị trí dọc của chúng (trên, giữa, hoặc dưới). ### Phân bổ \{#distribution\} Phân bổ xác định cách chia không gian giữa các phần tử con dọc theo trục chính: * **Gap** Gap (mặc định): Khoảng cách cố định tính bằng pixel giữa các phần tử con liền kề * **Space Between**: Các phần tử con trải đều đến hai cạnh; khoảng cách bằng nhau xuất hiện giữa chúng * **Space Around**: Không gian đều nhau bao quanh mỗi phần tử con, với khoảng cách nửa kích thước ở hai cạnh * **Space Evenly**: Không gian bằng nhau trước, giữa và sau tất cả các phần tử con ### Cắt nội dung \{#clip-content\} Cắt bỏ phần nội dung vượt ra ngoài ranh giới của container. Tắt tùy chọn này nếu bạn muốn cho phép tràn (ví dụ: một badge cố tình nhô ra ngoài mép card). ## Vị trí \{#position\} Theo mặc định, vị trí của mỗi phần tử được xác định tự động bởi bố cục của container chứa nó. Toggle **Position** cho phép bạn đưa phần tử ra khỏi luồng bình thường và định vị nó theo cách thủ công. ### Tương đối (mặc định) \{#relative-default\} Phần tử vẫn nằm trong luồng bố cục thông thường. Vị trí của nó được xác định tự động theo quy tắc bố cục của container cha — bạn không thể kéo thả tự do. Dùng **Margin** để điều chỉnh khoảng cách xung quanh phần tử tương đối. Dùng định vị tương đối cho phần lớn nội dung: khối văn bản, hình ảnh, thẻ, nút và các mục danh sách. ### Tuyệt đối \{#absolute\} Phần tử thoát khỏi luồng bố cục thông thường và hiển thị đè lên nội dung khác. Nó không còn ảnh hưởng đến bố cục của các phần tử lân cận. Khi bạn chọn **Absolute**, các điều khiển bổ sung sẽ xuất hiện: * **Các trường Offset** (T, L, R, B): Đặt khoảng cách tính bằng pixel từ phần tử đến từng cạnh của container cha * **Lưới Anchor**: Nhấp vào một điểm trên lưới 3×3 để chọn góc, cạnh hoặc tâm của container cha mà phần tử neo vào * **Anchor ngang** Horizontal positioning (Left / Center / Right) và **Anchor dọc** Vertical positioning (Top / Center / Bottom): Các dropdown điều khiển cùng điểm neo như lưới * **Z-index**: Trường số điều khiển [thứ tự xếp chồng](#stacking-order) của phần tử so với các phần tử cùng cấp. Phần tử có giá trị cao hơn sẽ hiển thị phía trên Sử dụng định vị tuyệt đối (absolute positioning) cho các lớp phủ trang trí, badge, nút đóng và icon đặt chồng lên hình ảnh. :::tip Để kéo giãn một phần tử có định vị tuyệt đối theo toàn bộ chiều rộng của phần tử cha, hãy đặt anchor ngang về **Left**, sau đó thêm offset **Right** bằng 0. Phần tử sẽ được ghim vào cả hai cạnh. ::: ### Cố định \{#fixed\} Phần tử bỏ qua hoàn toàn container cha và ghim vào màn hình. Nó vẫn hiển thị trong khi người dùng cuộn — nội dung trang di chuyển bên dưới nó. Định vị cố định (Fixed) sử dụng các điều khiển giống như Absolute (offset, lưới neo, Z-index). Tất cả các offset đều tính tương đối so với vùng an toàn (safe area) của màn hình thay vì so với phần tử cha. Ví dụ: offset bằng 0 từ phía dưới sẽ giữ phần tử nằm phía trên thanh home indicator. Để đo offset từ các cạnh vật lý của màn hình, hãy bật tùy chọn [Bỏ qua vùng an toàn](#ignore-safe-area). Sử dụng định vị cố định cho các phần tử cần nổi trên nội dung có thể cuộn thay vì chiếm không gian — ví dụ như nút đóng hoặc khôi phục dạng nổi, banner cố định ở trên cùng, nút cuộn lên đầu trang, và thanh điều hướng. Đối với khu vực hành động ở cuối trang, hãy dùng [Footer](builder-containers#footer) thay thế. ### Bỏ qua vùng an toàn \{#ignore-safe-area\} Vùng an toàn là phần màn hình tránh khỏi notch, thanh trạng thái và thanh điều hướng phía dưới. Theo mặc định, các phần tử được định vị sẽ nằm trong vùng này. Chọn hộp kiểm **Ignore safe area** bên dưới bộ chọn loại vị trí để đo offset của phần tử từ các cạnh vật lý của màn hình. Khi đó, phần tử có thể mở rộng ra phía sau notch và thanh điều hướng phía dưới. Sử dụng cách này để hiển thị media toàn màn hình: đặt vị trí của hình ảnh hoặc video thành **Fixed**, đặt cả bốn offset về 0 và chọn **Ignore safe area**. Media sẽ phủ kín toàn bộ màn hình, từ cạnh này đến cạnh kia. Tùy chọn này chỉ hoạt động với kiểu định vị absolute và fixed. Với các phần tử relative, nó bị vô hiệu hóa, và khi chuyển phần tử về **Relative** thì cài đặt này sẽ bị xóa. ## Kích thước \{#sizing\} Mỗi phần tử đều có điều khiển **Width** và **Height**. Nhấn vào dropdown để chọn chế độ kích thước: * **Fill**: Phần tử giãn ra để chiếm toàn bộ không gian còn lại trong phần tử cha. Giá trị pixel hiển thị là kết quả đã được tính toán. * **Hug**: Phần tử thu nhỏ vừa khít với nội dung bên trong. Giá trị pixel hiển thị là kết quả đã được tính toán. * **Fixed**: Phần tử sử dụng đúng giá trị pixel bạn chỉ định, bất kể kích thước phần tử cha hay nội dung. Đây là chế độ duy nhất dành cho các phần tử có vị trí tuyệt đối hoặc cố định. ## Khoảng cách \{#spacing\} Đặt giá trị khoảng cách riêng cho từng cạnh của phần tử. * **Margin**: Khoảng cách giữa phần tử và các phần tử lân cận. Không vượt ra ngoài ranh giới của container cha, bất kể giá trị là bao nhiêu. * **Padding**: Khoảng cách giữa ranh giới của phần tử và nội dung bên trong nó. Các phần tử văn bản chỉ có margin. Màn hình chỉ có padding. Cả hai đều khả dụng cho các container và các phần tử khác có nội dung con. ## Thứ tự xếp chồng \{#stacking-order\} Các phần tử Relative không bao giờ chồng lên nhau — mỗi container sắp xếp các phần tử con theo thứ tự tuần tự. Việc chồng chéo chỉ xảy ra khi một phần tử thoát khỏi luồng thông thường với vị trí **Absolute** hoặc **Fixed**. Khi các phần tử chồng lên nhau, các phần tử con xuất hiện sau trong bảng **Layers** sẽ được hiển thị phía trên các phần tử xuất hiện trước — kể cả khi phần tử sau là relative còn phần tử trước là absolute. Các phần tử **Absolute** và **Fixed** có trường **Z-index** để kiểm soát chi tiết hơn: giá trị cao hơn sẽ hiển thị trên cùng. Phần tử Relative không có Z-index — thứ tự layer là yếu tố duy nhất quyết định thứ tự chồng lên nhau. Dùng các [thao tác layer](paywall-layout-and-products#layer-actions) **Move up** và **Move down** để thay đổi thứ tự phần tử. --- # File: builder-styling --- --- title: "Kiểu dáng và giao diện" description: "Cấu hình giao diện trực quan của các phần tử — màu nền, viền, hiệu ứng, kiểu chữ, trạng thái và kiểu dáng toàn dự án." --- Tab **Design** trong bảng bên phải kiểm soát giao diện trực quan của từng phần tử. Các thuộc tính có sẵn tùy thuộc vào loại phần tử, nhưng hầu hết các phần tử đều có chung các tùy chọn tạo kiểu. :::link Về kích thước, khoảng cách và vị trí, xem [Bố cục và định vị](manage-paywall-ui-elements). ::: ## Hiển thị \{#visibility\} Nút bật tắt **Visibility** xác định xem phần tử đó có hiển thị trên màn hình hay không. * Show **Show** (mặc định): Phần tử luôn hiển thị. * Conditional **Conditional**: Phần tử chỉ hiển thị khi đáp ứng các điều kiện cụ thể. Xem [Hiển thị có điều kiện](onboarding-element-visibility) để biết thêm thông tin. * Hide **Hide**: Phần tử luôn ẩn. Dùng tùy chọn này để tạm thời xóa một phần tử khỏi flow mà không cần xóa hẳn. <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/3w3YSOmI3tQ?si=vPhoQGt44SI285Ru" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ## Tô màu nền \{#fill\} Phần **Fill** điều chỉnh nền của phần tử. Có bốn kiểu tô màu nền: màu đặc, gradient, hình ảnh và video. Dùng thuộc tính này để đặt ảnh hero / video cho toàn bộ màn hình. * **Solid color** Solid color. Dùng bộ chọn màu, nhập giá trị hex, hoặc chỉ định [màu dùng chung cho toàn dự án](#color-styles). Điều chỉnh **opacity** để làm nền trong suốt một phần. * **Gradient** Gradient. Thêm gradient với hai điểm màu trở lên. Kéo các điểm màu để thay đổi hiệu ứng chuyển tiếp, và điều chỉnh góc gradient để kiểm soát hướng. * **Image** Image hoặc **Video** Video. Đặt [hình ảnh / video](custom-media) làm nền cho phần tử. ## Đường viền \{#border\} Đường viền mặc định bị tắt. Nhấn Plus bên cạnh **Border** trong bảng bên phải để thêm đường viền. Để xóa đường viền, nhấn Close bên cạnh tiêu đề **Border**. Khi đã có đường viền, bạn có thể cấu hình: * **Color**: Dùng bộ chọn màu, nhập giá trị hex, hoặc gán [màu toàn dự án](#color-styles). Điều chỉnh **opacity** để làm đường viền bán trong suốt. * **Width**: Độ dày của đường viền tính bằng pixel. ## Góc bo tròn \{#corners\} Mục **Corners** kiểm soát bán kính viền (góc bo tròn). * **Radius slider**: Đặt cùng một bán kính cho cả bốn góc * **Per-corner toggle** Per Corner: Bật để đặt bán kính khác nhau cho từng góc riêng lẻ ## Hiệu ứng \{#effects\} Nhấn nút dấu cộng Plus bên cạnh **Effects** để thêm một hoặc nhiều hiệu ứng hình ảnh: * **Drop shadow**: Bóng đổ phía sau phần tử * **Inner shadow**: Bóng đổ bên trong ranh giới phần tử * **Background blur**: Làm mờ nền phía sau * **Layer blur**: Làm mờ phần tử và các phần tử con của nó Bạn có thể kết hợp nhiều hiệu ứng trên cùng một phần tử. Nhấn vào biểu tượng hiển thị Show để tạm thời tắt một hiệu ứng. ## Hiệu ứng động \{#animation\} Nhấn nút Plus bên cạnh **Animation** để thêm hiệu ứng động. Hiện tại, **Pulse** là hiệu ứng duy nhất có sẵn — phần tử sẽ phóng to thu nhỏ nhịp nhàng để thu hút sự chú ý. Cấu hình hiệu ứng Pulse với các thông số sau: | Tham số | Mô tả | |-----------|-------------| | Scale amount (%) | Mức độ phóng to của phần tử so với kích thước gốc | | Duration (ms) | Độ dài của một chu kỳ animation | | Delay between loops (ms) | Khoảng dừng giữa các lần lặp | | Shadow color | Màu của hiệu ứng bóng đổ nhấp nháy | | Shadow size (px) | Kích thước của bóng đổ nhấp nháy | ### Xem trước hoạt ảnh \{#preview-the-animation\} Theo mặc định, builder hiển thị màn hình tĩnh — các hoạt ảnh đứng yên cho đến khi bạn bật chúng lên. Có hai cách: - Nhấp vào nút **Toggle animations** Toggle animations phía trên phần xem trước thiết bị. Nút này bật/tắt hoạt ảnh của màn hình — sau khi bật, hoạt ảnh chạy liên tục cho đến khi bạn nhấp lại. Nút chỉ xuất hiện khi màn hình đang chọn có ít nhất một hoạt ảnh. - Mở [menu ngữ cảnh](paywall-layout-and-products#screen-actions) của màn hình (biểu tượng ba chấm bên cạnh layer màn hình) và chọn **Play Animation**. ## Giao diện \{#appearance\} * **Opacity**: Từ 0% (trong suốt hoàn toàn) đến 100% (không trong suốt) * **Rotation**: Nhập giá trị tính bằng độ để xoay phần tử ## Thuộc tính kiểu chữ (phần tử văn bản) \{#typography-properties-text-elements\} Các phần tử văn bản hiển thị mục **Typography** với các điều khiển sau: ### Phông chữ \{#font\} :::link Xem thêm: [Phông chữ tùy chỉnh](using-custom-fonts-in-flow-builder) ::: Nhấn vào dropdown phông chữ Font select để mở bộ chọn phông chữ. Bộ chọn có hai tab: * **Styles**: Liệt kê các [kiểu chữ](#text-styles) đã lưu trong dự án của bạn. Chọn một kiểu để áp dụng toàn bộ cài đặt typography của nó cùng một lúc. * **Fonts**: Liệt kê tất cả các họ phông chữ có sẵn. Tìm kiếm hoặc cuộn để tìm phông chữ bạn cần. Phông chữ tích hợp sẵn có thể **hiển thị khác nhau trên các thiết bị** — để hiển thị nhất quán, hãy tải lên [phông chữ tùy chỉnh](using-custom-fonts-in-flow-builder). ### Kích thước và độ đậm \{#size-and-weight\} :::warning Đối với [font tùy chỉnh](using-custom-fonts-in-flow-builder), các điều khiển **Weight**, **Bold** và **Italic** chỉ ảnh hưởng đến bản xem trước tích hợp trong trình chỉnh sửa. Để hiển thị các độ đậm và kiểu chữ khác nhau, hãy tải lên từng biến thể của font dưới dạng một file riêng. ::: * **Weight**: Chọn độ đậm của font từ menu thả xuống * **Size**: Chọn kích thước từ menu thả xuống hoặc nhập giá trị tùy chỉnh ### Màu sắc \{#color\} Nhấp vào ô màu để mở bộ chọn màu. Nhập giá trị hex, dùng bảng màu, hoặc chọn một trong các [style có thể tái sử dụng](#reusable-styles). Điều chỉnh thanh trượt opacity để làm văn bản trong suốt một phần. ### Căn chỉnh \{#alignment\} Hai nhóm điều khiển căn chỉnh: * **Horizontal**: Trái Align left, Giữa Align center, hoặc Phải Align right * **Vertical**: Trên Align top, Giữa Align middle, hoặc Dưới Align bottom ### Trang trí \{#decoration\} * **None** None: Không có trang trí (mặc định) * **Underline** Underline: Thêm gạch chân vào văn bản * **Strikethrough** Strikethrough: Thêm đường gạch ngang xuyên văn bản ### Cắt bớt văn bản \{#truncation\} Bật tính năng cắt bớt để ngắt văn bản vượt quá cài đặt **Max Lines**. Tính năng này hữu ích khi hỗ trợ nhiều ngôn ngữ: nếu chuỗi đã dịch dài hơn bản gốc, việc cắt bớt sẽ ngăn nó làm vỡ bố cục. :::note Khi bạn chọn một phần tử văn bản, một **thanh công cụ inline** cũng xuất hiện phía trên nó trên canvas. Thanh công cụ này cung cấp quyền truy cập nhanh vào font, độ đậm, cỡ chữ và căn chỉnh mà không cần cuộn qua bảng bên phải. ::: ## Cài đặt theo trạng thái (phần tử tương tác) \{#state-specific-settings-interactive-elements\} Các phần tử tương tác hỗ trợ nhiều trạng thái trực quan. Khi bạn chọn một phần tử như vậy, mục **States** sẽ xuất hiện trong bảng bên phải. Chuyển đổi giữa các trạng thái để cấu hình các thuộc tính trực quan khác nhau cho từng trạng thái. Mỗi trạng thái có thể ghi đè bất kỳ thuộc tính trực quan nào — fill, viền, màu chữ, opacity và nhiều hơn nữa. ### Trạng thái có thể chọn \{#selectable-states\} :::link Bài viết chính: [Phần tử có thể chọn](flow-selectable-elements) ::: Các phần tử thuộc nhóm có thể chọn (tùy chọn quiz, sản phẩm, tab, nút bật/tắt dùng thử) mặc định có hai trạng thái: * **Default**: Giao diện bình thường của phần tử * **Selected**: Giao diện khi người dùng đã chọn tùy chọn này. Ghi đè các thuộc tính như màu nền, màu viền và màu chữ để làm nổi bật lựa chọn đang hoạt động Để tạo kiểu cho một phần tử có thể chọn khi nó không tương tác được, hãy thêm trạng thái thứ ba theo cách thủ công. Mở **States settings** Settings và thêm **Disabled state**. Trạng thái **Disabled** được điều khiển bởi điều kiện. Chọn nó và nhấp vào **Set conditions** set conditions để xác định khi nào phần tử trở nên bị vô hiệu hóa trong lúc chạy, ví dụ như khi một trường bắt buộc bị trống. ### Trạng thái input \{#input-states\} Các trường nhập liệu cung cấp thêm các trạng thái: * **Default**: Giao diện bình thường, chưa được focus * **Active**: Trường đang được focus và sẵn sàng nhận dữ liệu nhập * **Invalid**: Giá trị đã nhập không qua xác thực * **Disabled**: Trường không tương tác ### Các phần tử có trạng thái khác \{#other-state-bearing-elements\} Một số phần tử hiển thị định dạng theo trạng thái ngoài mẫu **Default / Selected / Disabled** tiêu chuẩn: - **[Bước của progress indicator](builder-loaders-and-progress-bars#step-states)** — ba trạng thái mỗi bước: **Completed**, **Current** và **Upcoming**. - **[Dấu chấm carousel](builder-containers#dots)** — hai biến thể màu: **Color** cho các dấu chấm không hoạt động và **Active Color** cho dấu chấm của slide hiện tại. ## Style có thể tái sử dụng \{#reusable-styles\} Bảng **Styles** Styles trong thanh bên trái cho phép bạn định nghĩa các style có thể tái sử dụng áp dụng trong toàn bộ flow. Có hai loại style: text style và color style. Bạn cần dùng color style để bật tính năng hỗ trợ dark mode. ### Kiểu văn bản \{#text-styles\} :::link Bài viết chính: [Nội dung văn bản](onboarding-text) ::: Kiểu văn bản lưu trữ toàn bộ cài đặt typography — bao gồm họ font, độ đậm, kích thước, chiều cao dòng, căn chỉnh và trang trí. Mỗi template flow đều có sẵn các preset mặc định, và bạn có thể tạo thêm kiểu tùy chỉnh. Để tạo một kiểu văn bản: 1. Mở bảng **Styles** Styles và chọn tab **Text**. 2. Nhấp vào **Plus Create style**. 3. Nhập tên và cấu hình các thiết lập typography. 4. Nhấp vào **Create**. Để áp dụng text style, chọn một phần tử văn bản và chọn style từ dropdown font trong mục **Typography**. ### Màu sắc theo kiểu dáng \{#color-styles\} Màu sắc theo kiểu dáng là các màu được đặt tên mà bạn có thể tham chiếu xuyên suốt flow của mình. Mỗi kiểu màu có một tên (như "Primary text" hoặc "Brand"), giá trị hex và số lần sử dụng cho biết có bao nhiêu phần tử đang tham chiếu đến nó. Để tạo một kiểu màu: 1. Mở bảng **Styles** Styles và chọn tab **Colors**. 2. Nhấn **Plus Create style**. 3. Nhập tên và chọn màu. Khi bạn cập nhật một kiểu màu, mọi phần tử tham chiếu đến nó sẽ tự động được cập nhật. ### Chế độ tối \{#dark-mode\} :::link Bài viết chính: [Chế độ tối](paywall-dark-mode) ::: Nếu cần, bạn có thể thêm hai biến thể cho mỗi kiểu màu — một cho chế độ sáng Light mode và một cho chế độ tối Dark mode. SDK sẽ tự động áp dụng biến thể phù hợp dựa trên chế độ màu hiện tại của thiết bị. Để xem trước chế độ tối trong builder, hãy dùng **nút chuyển đổi giao diện** Dark mode trong [thanh công cụ phía dưới](builder-ui#view-controls-bottom-toolbar). --- # File: paywall-product-block --- --- title: "Thiết lập mua hàng" description: "Gán sản phẩm cho màn hình, thêm các phần tử sản phẩm, và kết nối nút mua hàng trong Flow Builder." --- Để thiết lập mua hàng trên một màn hình, hãy thêm nút mua hàng và cấu hình hành động **Purchase** cho nút đó. Hành động này có thể nhắm đến một sản phẩm cụ thể hoặc sản phẩm mà người dùng chọn từ phần tử Products trên màn hình. <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/LLIZCd94PlE?si=t_8BitA1FBpbd8ue" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ## Thêm sản phẩm \{#add-products\} Phần tử sản phẩm là một thẻ hiển thị trực quan trên canvas. Để thêm phần tử sản phẩm: 1. Trên canvas, nhấn **+** ở màn hình mong muốn. 2. Chọn **Products**. 3. Chọn kiểu bố cục: danh sách dọc, danh sách ngang, băng chuyền tính năng, thẻ tính năng, danh sách banner hoặc bottom sheet. 4. Chọn từng thẻ sản phẩm và gán sản phẩm tương ứng qua dropdown trong bảng **Design**. :::important Một phần tử sản phẩm chưa được gắn sản phẩm sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). Hãy gán sản phẩm hoặc xóa phần tử đó đi. ::: Để hiển thị giá neo bị gạch ngang trên card, hãy thêm [phần tử Old Price](onboarding-text#add-an-old-price) vào bên trong card đó. :::note Bạn cũng có thể gắn trực tiếp hành động **Purchase** vào tương tác **On tap** của thẻ sản phẩm. Khi nhấn vào thẻ, hành động mua sẽ được kích hoạt ngay mà không cần nút mua riêng. ::: :::important Nếu bạn xóa một nhóm sản phẩm và thay thế bằng nhóm mới, hãy kiểm tra lại tất cả các action và biến để đảm bảo chúng trỏ đúng vào nhóm mới. Các tham chiếu vẫn còn trỏ vào nhóm đã xóa sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). ::: ## Thêm nút mua hàng \{#add-a-purchase-button\} Nút mua hàng sẽ kích hoạt hành động **Purchase** khi người dùng nhấn vào. Để thêm nút mua hàng: 1. Trên canvas, nhấn **+** vào màn hình. 2. Chọn **Button** và chọn một preset button. 3. Với button đã được chọn, mở tab **Interactions** ở bảng bên phải. 4. Nhấn **Add trigger** > **On tap**, sau đó nhấn **Add action**. 5. Đặt **Action** thành **Purchase**, rồi đặt **Product** thành một trong các tùy chọn sau: - `products.selectedProduct`: Mua sản phẩm mà người dùng đã chọn từ một phần tử Products trên màn hình. - Một sản phẩm cụ thể: Luôn mua sản phẩm đó, bất kể lựa chọn nào trên màn hình. ### Hiển thị giá trên nút \{#show-the-price-on-the-button\} Để chèn giá của sản phẩm đã chọn vào nhãn nút, hãy dùng biến: 1. Chọn nút, sau đó mở tab **Design** ở bảng bên phải. 2. Trong trường **Content**, đặt con trỏ vào vị trí muốn hiển thị giá. 3. Nhấp vào biểu tượng biến, chọn `products.selectedProduct`, rồi chọn thuộc tính `prod_price`. Biến đầy đủ sẽ là `products.selectedProduct.prod_price`. 4. Thêm văn bản tĩnh xung quanh biến — ví dụ: `Subscribe for {prod_price}`. Nhãn sẽ cập nhật theo khi người dùng chọn các sản phẩm khác nhau. ## Khôi phục giao dịch mua \{#restore-purchases\} Để người dùng có thể khôi phục các giao dịch mua trước đó, hãy thêm nút hoặc liên kết khôi phục vào màn hình. Để thêm thành phần khôi phục giao dịch mua: 1. Trên canvas, nhấp vào **+** trên màn hình. 2. Chọn **Button**, sau đó chọn **Links** cho liên kết văn bản hoặc bất kỳ loại nút nào khác cho nút có kiểu dáng. 3. Với phần tử đã chọn, mở tab **Interactions** ở bảng bên phải và nhấn **Add trigger**. 4. Chọn **On tap** và nhấn **Add action**. 5. Trong dropdown **Action**, chọn **Restore purchases**. ## Hiển thị các phần tử bổ sung dựa trên sản phẩm được chọn \{#display-additional-elements-based-on-the-selected-product\} Nếu một màn hình có các sản phẩm, bạn có thể hiển thị hoặc ẩn các phần tử khác tùy thuộc vào sản phẩm mà người dùng chọn. Để thiết lập hiển thị có điều kiện: 1. Trong phần tử **Products**, chọn một thẻ sản phẩm. 2. Mở tab **Interactions** ở bảng bên phải và nhấp vào **Add trigger**. 3. Chọn **On tap** và nhấp **Add action**. 4. Từ menu thả xuống **Action**, chọn **Show** hoặc **Hide**. 5. Chọn phần tử cần hiển thị hoặc ẩn khi sản phẩm đó được chọn. ## Xem sản phẩm trong flow \{#review-products-in-flow\} Bảng **Products** ở thanh bên trái hiển thị các sản phẩm hiện có được ánh xạ đến từng màn hình trong flow. Mỗi màn hình có hai phần: - **Default** — một sản phẩm, được chọn sẵn khi màn hình tải. - **Other** — các sản phẩm bổ sung có sẵn trên cùng màn hình đó. --- # File: flow-selectable-elements --- --- title: "Các phần tử có thể chọn và nhóm" description: "Làm cho các phần tử có thể chọn, tổ chức chúng thành nhóm, và sử dụng trạng thái của chúng trong các điều kiện trên toàn flow." --- Các phần tử có thể chọn là những phần tử trong flow mà người dùng có thể nhấn để chọn hoặc bỏ chọn. Trạng thái của chúng có thể điều khiển điều hướng, khả năng hiển thị và các logic khác trong toàn bộ flow. Dưới đây là những gì bạn có thể làm: - [Sử dụng các phần tử có thể chọn mặc định](#default-selectable-elements) — tùy chọn quiz, sản phẩm, tab và toggle dùng thử đã có thể chọn ngay - [Làm cho bất kỳ phần tử nào có thể chọn được](#make-an-element-selectable) — biến bất kỳ phần tử nào thành phần tử có thể chọn và gán vào một nhóm - [Tạo và quản lý nhóm](#create-a-group) — tổ chức các phần tử có thể chọn thành nhóm chọn một, chọn nhiều hoặc toggle - [Sử dụng trạng thái đã chọn trong điều kiện](#use-selectable-state-in-conditions) — tham chiếu giá trị nhóm trong các điều kiện trên bất kỳ màn hình nào trong flow ## Các phần tử có thể chọn theo mặc định \{#default-selectable-elements\} Một số loại phần tử có thể chọn theo mặc định — chúng đã thuộc các nhóm được tạo tự động và không cần thiết lập thêm: - **Tùy chọn quiz**: Mỗi câu trả lời quiz là một phần tử có thể chọn trong nhóm quiz. Xem [Quizzes](onboarding-quizzes). - **Sản phẩm**: Thẻ sản phẩm trong một nhóm sản phẩm. Xem [Product block](paywall-product-block). - **Tabs**: Các mục tab trong một nhóm tab. Xem [Tabs](builder-tabs). - **Trial toggles**: Một container thuộc một nhóm và có trạng thái được chọn. Xem [Toggles](builder-toggles). ## Làm cho phần tử có thể chọn \{#make-an-element-selectable\} <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/btpZPOm9VRY?si=1P959iwNfIJ1ZP7N" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Trong một số trường hợp, bạn có thể muốn làm cho các phần tử khác trở nên có thể chọn được. Ví dụ, bạn có thể thêm checkbox **Don't ask me again** hoạt động như một phần tử bên trong một nhóm quiz. Để làm cho một phần tử có thể chọn được: 1. Chọn phần tử trên màn hình hoặc trong bảng **Layers**. 2. Ở bên phải, chuyển sang bảng **Interactions**. 3. Chọn **Turn into selectable element**. 4. Trong dropdown **Group**, chọn một nhóm hiện có hoặc [tạo nhóm mới](#create-a-group). 5. Đặt **Element ID** — mã định danh duy nhất cho phần tử này trong nhóm. 6. Nếu bạn muốn phần tử này được chọn theo mặc định, hãy tích vào ô **Set as default in group**. ## Tạo nhóm \{#create-a-group\} Nhóm giúp tổ chức các phần tử có thể chọn trên một màn hình và xác định cách chọn hoạt động — người dùng có thể chọn một tùy chọn, nhiều tùy chọn, hoặc bật/tắt. Để tạo nhóm: 1. Chọn một phần tử và [làm cho nó có thể chọn được](#make-an-element-selectable). 2. Trong dropdown **Group**, chọn **Create group**. 3. Nhập **Group name**. 4. Chọn [loại nhóm](#group-types). Nhóm hiện đã có sẵn trong dropdown **Group** cho các phần tử có thể chọn khác trên cùng màn hình. ## Loại nhóm \{#group-types\} :::important Hầu hết [các preset quiz](onboarding-quizzes) mặc định là **multi-choice**. Thay đổi [loại nhóm](#manage-groups) để chỉ cho phép một câu trả lời. ::: - **Single choice**: Mỗi lúc chỉ có thể chọn một phần tử trong nhóm. Khi chọn phần tử mới, phần tử trước đó sẽ bị bỏ chọn. - **Multi-choice**: Có thể chọn nhiều phần tử cùng một lúc. - **Toggle**: Mỗi phần tử chuyển đổi giữa trạng thái được chọn và không được chọn sau mỗi lần nhấn, độc lập với các phần tử khác. ## Quản lý nhóm \{#manage-groups\} Để xem và chỉnh sửa các nhóm, mở bảng **Screen settings** và tìm phần **Selectable groups**. Phần này liệt kê tất cả các nhóm trên màn hình hiện tại. Nhấp vào ID nhóm để: - Thay đổi ID nhóm - Thay đổi [loại nhóm](#group-types) - Xem cách các phần tử trong nhóm được tham chiếu trong điều kiện ## Sử dụng trạng thái đã chọn trong điều kiện \{#use-selectable-state-in-conditions\} Bạn có thể tham chiếu trạng thái đã chọn của một nhóm trong các điều kiện trên bất kỳ màn hình nào trong flow — không chỉ màn hình nơi nhóm được định nghĩa. Ví dụ: `IF quiz.photo is selected, THEN navigate to the Photo screen`. :::important Tất cả các phần tử trong một nhóm phải nằm trên cùng một màn hình. Bạn không thể thêm các phần tử từ các màn hình khác nhau vào một nhóm. Tuy nhiên, bạn có thể tham chiếu giá trị của nhóm trong các điều kiện trên bất kỳ màn hình nào trong flow. ::: Sử dụng trạng thái đã chọn với: - **[Hành động có điều kiện](onboarding-actions#conditional-actions)**: Điều hướng người dùng đến các màn hình khác nhau hoặc kích hoạt các hành động khác nhau dựa trên các phần tử được chọn. - **[Điều hướng động](onboarding-navigation-branching)**: Phân nhánh flow dựa trên câu trả lời quiz, trạng thái toggle, hoặc các lựa chọn khác. - **[Hiển thị có điều kiện](onboarding-element-visibility)**: Hiển thị hoặc ẩn các phần tử dựa trên những gì người dùng đã chọn ở các màn hình trước. --- # File: builder-element-states --- --- title: "Trạng thái phần tử" description: "Tùy chỉnh giao diện phần tử theo từng trạng thái và dùng điều kiện để vô hiệu hóa phần tử khi chạy ứng dụng." --- Các phần tử flow tương tác thay đổi giao diện dựa trên hành động của người dùng: một tùy chọn quiz được nhấn sẽ chuyển sang trạng thái **Selected** (Đã chọn), một ô nhập liệu được focus sẽ chuyển sang trạng thái **Active** (Đang hoạt động). Một số trạng thái được điều khiển theo điều kiện — ví dụ, bạn có thể **disable** (vô hiệu hóa) một nút. Hãy tùy chỉnh style riêng cho từng trạng thái để cung cấp phản hồi trực quan cho người dùng mà không cần viết code ứng dụng. <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/gdsNfHpKAqQ?si=VY5mqZgH1j0RB6fE" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ## Các trạng thái có sẵn theo loại phần tử \{#available-states-by-element-kind\} | Loại phần tử | Trạng thái tích hợp sẵn | Trạng thái có thể thêm | |---|---|---| | [Phần tử có thể chọn](#selectable-element-states) | **Default**, **Selected** | **Disabled** | | [Ô nhập liệu](#input-states) | **Default**, **Active**, **Invalid** | **Disabled** | | [Bất kỳ phần tử nào có tương tác chạm](#condition-driven-disabled-state) — nút, hình ảnh, icon, container, v.v. | **Default** | **Disabled** | | [Các bước chỉ báo tiến trình](#step-states-for-progress-indicators) | **Completed**, **Current**, **Upcoming** | — | Các trạng thái **Addable** không hiển thị mặc định — mở **States settings** Settings để thêm chúng. Chúng được [**điều khiển bởi điều kiện**](#condition-driven-disabled-state): bạn tự định nghĩa khi nào chúng được kích hoạt. ## Cách tạo kiểu cho một trạng thái \{#how-to-style-a-state\} 1. Chọn một phần tử. Phần **States** trong panel bên phải liệt kê các trạng thái mà phần tử đó hỗ trợ. 2. Trong phần **States**, kích hoạt trạng thái mục tiêu. Thêm [trạng thái Disabled theo điều kiện](#condition-driven-disabled-state) nếu cần. 3. Thay đổi bất kỳ thuộc tính nào — fill, border, typography, v.v. Thay đổi đó chỉ áp dụng cho trạng thái đó. Nested elements trở thành stateful cùng với parent. Mọi thay đổi trên child đều được áp dụng trong phạm vi trạng thái active của parent. 4. Builder áp dụng style phù hợp tại runtime. ## Trạng thái của phần tử có thể chọn \{#selectable-element-states\} Các phần tử có thể chọn — tùy chọn quiz, sản phẩm, tab, nút bật/tắt dùng thử, và bất kỳ [phần tử có thể chọn tùy chỉnh](flow-selectable-elements#make-an-element-selectable) nào — đều có sẵn hai trạng thái: - **Default**: Giao diện mặc định của phần tử. - **Selected**: Áp dụng khi người dùng nhấn vào phần tử. Builder sẽ quay lại Default khi người dùng bỏ chọn phần tử đó. Trong nhóm chọn một, khi chọn một phần tử thì các phần tử còn lại sẽ bị bỏ chọn. Nhóm chọn nhiều cho phép chọn đồng thời nhiều phần tử. Toggle hoạt động độc lập — chọn một toggle không ảnh hưởng đến các toggle khác cùng cấp. Xem [các loại nhóm](flow-selectable-elements#group-types). :::tip Cần áp dụng cùng một style cho trạng thái của nhiều phần tử (ví dụ: các lựa chọn quiz)? Hãy style một phần tử trước, rồi nhân bản nó. Style theo trạng thái không được kế thừa giữa các phần tử cùng cấp — nhân bản là cách xử lý hiện tại. ::: ## Trạng thái của input \{#input-states\} - **Default**: Giao diện mặc định của input. - **Active**: Áp dụng khi input đang được focus. - **Invalid**: Áp dụng khi nội dung của input không vượt qua validation. Ví dụ: khi trường email không có `@`. Xem [Validation input](builder-inputs-and-forms#input-validation). - **Disabled**: Input không thể tương tác. Thêm trạng thái này thủ công; xem [Trạng thái Disabled theo điều kiện](#condition-driven-disabled-state). Tùy chỉnh từng trạng thái theo cách tương tự như element selectable: kích hoạt trạng thái cần chỉnh, rồi thay đổi thuộc tính. ## Trạng thái Disabled theo điều kiện \{#condition-driven-disabled-state\} Trạng thái Disabled ngăn người dùng tương tác với một phần tử. Khác với các trạng thái Default, Selected, Active hay Invalid, trạng thái Disabled không tự kích hoạt — nó cần một điều kiện do người dùng định nghĩa. Disabled có thể áp dụng trên: - **Inputs**: Bất kỳ [trường nhập liệu](builder-inputs-and-forms) nào — văn bản, email, mật khẩu, số, điện thoại, ngày và/hoặc giờ. - **Các phần tử có thể chọn**: Tùy chọn quiz, sản phẩm, tab, toggle dùng thử, và bất kỳ [phần tử có thể chọn tùy chỉnh](flow-selectable-elements#make-an-element-selectable) nào. - **Bất kỳ phần tử nào có tương tác nhấn**: Ví dụ: nút, hình ảnh hoặc biểu tượng kích hoạt hành động điều hướng. ### Thêm trạng thái Disabled \{#add-the-disabled-state\} Để thêm và cấu hình trạng thái Disabled: 1. Chọn phần tử mục tiêu. 2. Trong phần **States**, nhấp vào **Settings** Settings. 3. Chọn **Add Disabled state**. Trạng thái Disabled sẽ xuất hiện trong phần **States**. 4. Bên cạnh trạng thái Disabled mới, nhấp vào **Edit conditional state** Edit conditional state. 5. Thêm một điều kiện. Nếu bạn muốn vô hiệu hóa nút **submit** khi dữ liệu nhập chưa vượt qua xác thực, hãy so sánh biến `isValid` của input với `false`. 6. Tùy chỉnh giao diện cho trạng thái Disabled để thể hiện rõ sự hạn chế (ví dụ: giảm độ mờ đục). { } SDK đánh giá điều kiện tại thời điểm chạy và tự động áp dụng trạng thái Disabled khi phù hợp — không cần thay đổi code trong ứng dụng. ## Trạng thái bước của progress indicator \{#step-states-for-progress-indicators\} :::link Bài viết chính: [Progress indicator](builder-loaders-and-progress-bars#step-states) ::: Progress indicator cho người dùng biết họ đang ở đâu trong flow onboarding. Mỗi bước có ba trạng thái: - **Completed**: Các bước người dùng đã hoàn thành. - **Current**: Bước hiện tại của người dùng. - **Upcoming**: Các bước người dùng chưa đến. --- # File: builder-containers --- --- title: "Phần tử bố cục: container, carousel, bottom sheet" description: "Nhóm các phần tử thành container, carousel và bottom sheet trong Flow Builder." --- Các phần tử bố cục dùng để nhóm các phần tử khác lại và kiểm soát cách chúng được hiển thị trên màn hình. :Flow Builder có năm loại phần tử bố cục: - **Containers**: sắp xếp các phần tử con theo một trục — dọc hoặc ngang - **Carousel**: vùng chứa có thể vuốt, hiển thị từng slide một - **Bottom Sheet**: panel trượt lên từ phía dưới màn hình và hiển thị phủ lên nội dung bên dưới - **Footer**: panel ghim ở cuối màn hình, nằm ngoài vùng cuộn - **Dividers**: các đường kẻ mỏng phân tách các hàng hoặc cột :::link **Tabs** cũng thuộc nhóm này, nhưng được trình bày trong một bài viết riêng. Xem [Tabs](builder-tabs) để biết thêm chi tiết. ::: ## Container \{#containers\} :::link Bài viết chính: [Định vị phần tử](manage-paywall-ui-elements) ::: Container nhóm các phần tử theo chiều dọc hoặc chiều ngang. **Vertical Container** sắp xếp các phần tử thành hàng; **Horizontal Container** sắp xếp chúng thành cột. :::tip Lồng các container vào nhau để tạo bố cục phức tạp hơn. ::: ### Thay đổi hướng container \{#change-container-direction\} Hướng của container không cố định. Bạn có thể chuyển đổi giữa **Vertical**, **Horizontal** và **Free** trong phần **Layout** của panel bên phải bất kỳ lúc nào — không cần xóa và tạo lại container. Cấu hình khoảng cách, căn chỉnh và phân bố trong cùng phần **Layout**. Các phần tử con được hiển thị theo thứ tự xuất hiện trong panel **Layers** — kéo để sắp xếp lại. ### Wrap và Unwrap \{#wrap-and-unwrap\} Để chuyển một phần tử hiện có thành container, chọn nó và dùng [thao tác layer](paywall-layout-and-products#layer-actions) **Wrap**. Kéo thêm các phần tử vào container mới từ panel **Layers**. Để xóa stack và đưa các phần tử con lên một cấp, dùng **Unwrap**. ## Carousel \{#carousel\} **Carousel** là một vùng chứa có thể vuốt, hiển thị một slide tại một thời điểm. Người dùng vuốt ngang để chuyển sang slide tiếp theo, hoặc carousel tự động chuyển slide theo hẹn giờ. Carousel chứa một tập hợp các layer **Slide**. Khi slide đang active, các phần tử trên layer đó sẽ hiển thị trên màn hình. Không giống Tabs, slide đang hiển thị trong carousel không được hiển thị dưới dạng [nhóm có thể chọn](flow-selectable-elements) — các slide không thể được tham chiếu trong điều kiện hoặc văn bản động. Hãy dùng Carousel cho mục đích xoay vòng hình ảnh, không phải để phân nhánh theo lựa chọn của người dùng. ### Thay đổi slide đang hoạt động \{#change-active-slide\} Khi bạn chọn carousel, builder hiển thị thanh điều khiển pop-up với dropdown **Slide** và nút **+ Add Slide**. - Nhấp **+ Add Slide** để thêm một slide trống mới. - Dùng dropdown **Slide** để chuyển slide nào đang hoạt động trên canvas — hoặc nhấp vào layer Slide tương ứng trong panel **Layers**. Để sắp xếp lại các slide, kéo chúng trong Carousel ở panel Layers. {/* TODO: on-device GIF */} ### Thuộc tính #### Tự động cuộn \{#auto-scroll\} Tự động cuộn sẽ chuyển slide liên tục — người dùng không cần vuốt để xem hết nội dung. Hai tham số thời gian điều khiển hành vi này: - **Delay** — thời gian mỗi slide hiển thị (ms). - **Duration** — thời gian chuyển tiếp giữa các slide (ms). #### Kích thước carousel \{#carousel-sizing\} Các điều khiển chuyên dụng xác định kích thước của carousel và khoảng cách giữa các slide liền kề. Đặt **Height** thành **Fixed** để bố cục không bị dịch chuyển khi người dùng vuốt giữa các slide có độ dài nội dung khác nhau. #### Kích thước slide \{#slide-sizing\} **Width** và **Height** cho từng slide. Mặc định là Fill để mỗi slide theo kích thước của carousel. Đặt chiều rộng cố định để tạo hiệu ứng peek, nơi các slide lân cận hiển thị một phần. #### Dots \{#dots\} Chỉ báo trang ở cuối carousel. Nó cho người dùng biết có bao nhiêu slide và slide nào đang active. Tắt toggle **Show dots** để ẩn chỉ báo slide. Khi dots hiển thị, các thuộc tính sau điều chỉnh giao diện của chúng: - **Color** — màu nền của dấu chấm không hoạt động. - **Active Color** — màu nền của dấu chấm tương ứng với slide đang hiển thị. - **Size** — đường kính của mỗi dấu chấm, tính bằng pixel. - **Gap** — khoảng cách giữa các dấu chấm liền kề. - **Padding** — khoảng cách giữa hàng dấu chấm và nội dung carousel phía trên. ## Bottom Sheet \{#bottom-sheet\} :::link Hướng dẫn: [Hiển thị tất cả các gói trong bottom sheet](show-plans-bottom-sheet) ::: **Bottom Sheet** là một panel bố cục trượt lên từ phía dưới màn hình, hiển thị phía trên nội dung bên dưới. Sheet luôn làm mờ mọi thứ phía sau nó; không thể tắt hiệu ứng mờ này. Kích hoạt nó khi nhấn — ví dụ, đằng sau liên kết **Show all plans** — thay vì khi tải màn hình. ### Cấu trúc \{#structure\} Một Bottom Sheet đi kèm với hai layer cấp cao nhất: - **Heading** — một container ở đầu sheet, được điền sẵn với layer văn bản **Title** và **Close button** Close. Chỉnh sửa hoặc xóa chúng tùy ý. - **Content** — container chính. Thêm sản phẩm, nút, liên kết hoặc bất kỳ phần tử nào khác vào đây. {/* TODO: on-device GIF */} ### Hiển thị ban đầu \{#initial-visibility\} Mặc định, bottom sheet xuất hiện ngay khi màn hình được hiển thị. Để mở nó theo yêu cầu thay vào đó: 1. **Hoàn thiện nội dung của sheet trước** — các layer ẩn không thể chỉnh sửa, vì vậy sheet phải giữ hiển thị cho đến khi bạn điền xong nội dung. 2. Trong panel **Layers**, chọn bottom sheet. 3. Đặt **Visibility** thành **Hide** Hide. Sheet vẫn còn trong cây layer nhưng ngừng hiển thị trên màn hình. ### Kích hoạt bottom sheet \{#triggering-the-bottom-sheet\} Để mở một bottom sheet đang ẩn, gắn hành động **Show** vào một phần tử khác: 1. Chọn phần tử kích hoạt (ví dụ: nút hoặc liên kết văn bản). 2. Mở tab **Interactions** trong panel bên phải. 3. Nhấp **Add trigger** > **On tap**, rồi **Add action**. 4. Đặt **Action** thành **Show** và chọn bottom sheet từ dropdown. ## Footer \{#footer\} **Footer** là một container cố định chiếm phần dưới cùng của màn hình. Nó có thể có chiều cao tùy ý và chứa bất cứ thứ gì, từ một nút đơn lẻ đến nhiều hàng văn bản. Dùng footer cho những nội dung cần giữ nguyên vị trí trong khi phần còn lại của màn hình cuộn — nút CTA, văn bản pháp lý, liên kết. Không giống các phần tử thông thường, footer kéo dài vào vùng an toàn phía dưới của thiết bị: nền của nó trải dài đến tận mép màn hình. Chỉ được phép có một footer trên mỗi màn hình. Bạn không thể sao chép footer hiện có hoặc thêm một footer mới. ### Footer hay một phần tử cố định thông thường \{#footer-vs-a-regular-fixed-element\} Cả hai đều hiển thị trên màn hình trong khi nội dung cuộn. Hãy chọn tùy chọn phù hợp với nhu cầu của bạn: - **Dùng Footer** cho thanh điều hướng chính ở cuối màn hình (nút CTA, văn bản pháp lý, liên kết). Footer tự giữ không gian riêng, đảm bảo nội dung luôn cuộn rõ ràng phía trên nó và không bị ẩn sau, đồng thời tự động phủ vùng an toàn ở đáy màn hình. - **Dùng [phần tử cố định](manage-paywall-ui-elements)** cho những thứ cần nổi trên nội dung đang cuộn thay vì giữ không gian riêng, hoặc ghim vào một cạnh khác ngoài cạnh đáy — ví dụ: nút đóng/khôi phục nổi, banner cố định ở đầu, hoặc nút cuộn lên đầu trang. Bạn tự quản lý khoảng cách vùng an toàn cho chúng. ## Divider \{#dividers\} **Horizontal Divider** và **Vertical Divider** là các đường kẻ mỏng phân tách nội dung. Dùng Horizontal Divider để chia hàng, và Vertical Divider để chia cột bên trong container ngang. Điều chỉnh độ dày, màu sắc và độ dài từ panel bên phải. --- # File: using-custom-fonts-in-flow-builder --- --- title: "Font tùy chỉnh trong Flow Builder" description: "Tải lên và sử dụng font tùy chỉnh trong Flow Builder." --- Khi tạo flow, bạn có thể muốn dùng font tùy chỉnh để phù hợp với phần còn lại của ứng dụng. Dưới đây là cách thêm font tùy chỉnh và sử dụng chúng trong flow của bạn. :::tip [Cấu hình font](onboarding-text) trong panel **Styles** trước khi bắt đầu thiết kế flow. Như vậy, mọi thay đổi bạn thực hiện sẽ được áp dụng toàn cục. ::: ## Font tích hợp sẵn \{#built-in-fonts\} Khi bạn tạo một flow trong Builder, Adapty sử dụng font hệ thống theo mặc định. Thông thường là SF Pro trên iOS và Roboto trên Android, mặc dù có thể khác nhau tùy theo thiết bị. Bạn cũng có thể chọn từ các font phổ biến như Arial, Times New Roman, Courier New, Georgia và Helvetica. Mỗi font trong số này đi kèm với một vài tùy chọn kiểu chữ. Các font chữ này không được cung cấp kèm theo Adapty SDK và chỉ được sử dụng cho mục đích xem trước. Chúng tôi không thể đảm bảo chúng sẽ hoạt động hoàn hảo trên mọi thiết bị. Tuy nhiên, qua quá trình kiểm thử, các font này thường được hầu hết thiết bị nhận dạng mà không cần bạn thực hiện thêm bất kỳ thao tác nào. Bạn cũng có thể [xem danh sách các font có sẵn theo mặc định trên iOS](https://developer.apple.com/fonts/system-fonts/). ## Thêm font chữ tùy chỉnh \{#add-a-custom-font\} :::warning File bạn tải lên **chỉ dùng để xem trước trong editor** — Adapty không đẩy file đó xuống thiết bị của người dùng. Để hiển thị font trên thiết bị, hãy [thêm file vào bundle của ứng dụng](#add-the-font-files-to-your-apps-bundle). Nếu không có file đó, SDK sẽ tự động dùng SF Pro (iOS) hoặc Roboto (Android) khi chạy thực tế. ::: Nếu bạn muốn dùng font khác ngoài font mặc định của hệ thống, bạn có thể thêm font tùy chỉnh. Để thêm font tùy chỉnh: 0. Nếu font là variable font, hãy tách nó thành các file đơn style với tên riêng biệt. Các điều chỉnh về weight, bold và italic không áp dụng cho font tùy chỉnh. Adapty chỉ đăng ký một style cho mỗi file font tùy chỉnh. Để [định dạng văn bản](onboarding-text), hãy chuyển sang biến thể font phù hợp. 1. Chọn **Upload new font** trong bất kỳ dropdown font nào. 2. Trong cửa sổ **Add custom font**, điền vào các trường sau: :::warning Tên **Font name in Builder**, **iOS font name**, và **Android font name** phải là duy nhất trong tất cả các file font tùy chỉnh trong ứng dụng. ::: - **Font name in Builder**: Nhập tên hiển thị cho font. Tên này sẽ xuất hiện trong các dropdown chọn font trong Builder. - **iOS font name**: Nhập tên PostScript của font. Bạn có thể tìm thấy nó trong Font Book → PostScript name, hoặc thông qua [`UIFont` API](https://developer.apple.com/documentation/uikit/uifont). - **Android font name**: Nhập tên file từ `res/font/`. Chỉ dùng chữ thường, số và dấu gạch dưới. - **Font file**: Kéo và thả file font vào hoặc nhấn **Select files**. Định dạng hỗ trợ: `.ttf`, `.otf`, `.woff`, `.woff2`. 3. Nhấn **Save font**. Bằng cách tải tệp phông chữ lên Adapty, bạn xác nhận rằng bạn có quyền sử dụng nó trong ứng dụng của mình. ### Xóa font chữ tùy chỉnh \{#delete-a-custom-font\} Xóa font chữ tùy chỉnh khỏi dashboard sẽ tự động thay thế mọi tham chiếu đến font đó bằng font hệ thống trong tất cả các flow đang ở trạng thái nháp lẫn đã xuất bản. Không có cảnh báo nào được hiển thị và bạn không thể hoàn tác thao tác này. Trước khi xóa, hãy xác nhận rằng không có flow nào đang hoạt động đang dùng font đó. ## Phông chữ tùy chỉnh trong các mẫu flow \{#custom-fonts-in-flow-templates\} [Thư viện mẫu flow](paywall-builder-templates) bao gồm các mẫu có phông chữ tùy chỉnh. Di chuột qua chip **Custom font** trên thẻ mẫu để xem mẫu đó sử dụng những phông chữ nào. Adapty không đính kèm sẵn các phông chữ này. Bạn cần tự tìm nguồn phông chữ, khớp với tên phông được hiển thị trong chip. Một số phông chữ có thể yêu cầu giấy phép thương mại. Sau khi có file phông chữ, hãy làm theo các bước đóng gói bên dưới. ## Thêm file font vào bundle của ứng dụng \{#add-the-font-files-to-your-apps-bundle\} Nếu bạn đã sử dụng font tùy chỉnh ở nơi khác trong ứng dụng, bạn chỉ cần thêm font cho paywall theo cách tương tự. Nếu chưa, hãy đảm bảo đưa file font vào project và bundle của ứng dụng. Xem hướng dẫn bên dưới: - Trên iOS: [Trong tài liệu chính thức của Apple](https://developer.apple.com/documentation/uikit/adding-a-custom-font-to-your-app) - Trên Android: [Trong tài liệu chính thức của Android](https://developer.android.com/develop/ui/views/text-and-emoji/fonts-in-xml) --- # File: paywall-head-picture --- --- title: "Backgrounds" description: "Điền nền màu đơn sắc, gradient, ảnh hoặc video cho màn hình trong Flow Builder." --- Đặt nền cho bất kỳ màn hình nào trong bảng **Fill** ở phần [**Screen settings**](paywall-layout-and-products#screen-settings). Chọn từ bốn loại nền — màu đơn sắc, gradient, ảnh hoặc video. ## Ảnh \{#image\} Tải lên file `.JPG`, `.PNG`, `.GIF` hoặc `.WEBP` tối đa 20 MB. Ảnh sẽ tự động co giãn để bao phủ toàn bộ nền. :::note Ảnh nền và video nền lấp đầy toàn bộ viewport, bao gồm cả vùng phía sau notch và thanh hệ thống — ngay cả khi **Safe area** được bật. Hãy giữ nội dung quan trọng tránh xa các cạnh để không bị cắt mất. ::: Để thay ảnh nền lúc runtime từ code ứng dụng, hãy bật [custom media ID](custom-media#custom-media-id). ## Video \{#video\} Tải lên file `.MP4` hoặc `.WEBM` tối đa 50 MB. Bản xem trước hiển thị một khung tĩnh — nhưng video sẽ phát trên thiết bị lúc runtime. Bật **Loop** để phát lại video liên tục. Để thay video nền lúc runtime, hãy bật [custom media ID](custom-media#custom-media-id). ## Màu đơn sắc \{#solid-color\} Nhập giá trị hex và đặt độ trong suốt từ 0 đến 100%. Chọn [color style](builder-styling) đã lưu từ bảng màu để áp dụng màu thương hiệu — nền sẽ tự động theo chủ đề sáng và tối của bạn. ## Gradient \{#gradient\} Tạo gradient tuyến tính nhiều điểm dừng: - **Direction** — xoay gradient từ 0 đến 360°. - **Stops** — kéo dọc theo thanh để thay đổi vị trí. Nhấp vào một điểm dừng để chỉnh hex và độ trong suốt. --- # File: custom-media --- --- title: "Hình ảnh, video và biểu tượng" description: "Thêm các phần tử hình ảnh, video và biểu tượng vào màn hình trong Flow Builder, và thay thế media lúc chạy bằng ID media tùy chỉnh." --- Flow Builder bao gồm ba loại phần tử media thuộc danh mục **Media**: Image, Video và Icon. :::tip Để hình ảnh hoặc video bao phủ toàn bộ màn hình — kể cả các vùng phía sau notch và thanh điều hướng — hãy đặt vị trí cố định với tất cả các offset bằng 0 và chọn **Ignore safe area**. Xem [Bố cục và vị trí](manage-paywall-ui-elements#ignore-safe-area). ::: ## Hình ảnh \{#image\} Tải lên tệp `.JPG`, `.PNG` hoặc `.GIF` tối đa 20 MB. - **Aspect** — kiểm soát cách hình ảnh vừa với container của nó: - **Fit** — thu phóng hình ảnh để vừa khít bên trong container mà không cắt xén. - **Fill** — kéo giãn hình ảnh để lấp đầy container. - **Cover** — thu phóng hình ảnh để che phủ container, cắt xén nếu cần. Mặc định. - **Use custom media ID** — xem [Custom media ID](#custom-media-id) bên dưới. ## Video \{#video\} Tải lên tệp `.MP4` hoặc `.WEBM` tối đa 50 MB và không dài hơn 30 giây. Độ phân giải video tối thiểu là 640x640 pixel. - **Aspect** — Fit, Fill hoặc Cover. Mặc định là Fill. - **Loop** — phát lại video liên tục. Bật theo mặc định. - **Use custom media ID** — xem [Custom media ID](#custom-media-id) bên dưới. Video không phát trong bản xem trước trên editor — canvas chỉ hiển thị một khung hình tĩnh. Trên thiết bị khi chạy thực tế, video phát ở chế độ tắt tiếng theo mặc định. Khi bật Loop, video lặp lại vô hạn. ### Kích hoạt hành động khi video kết thúc \{#trigger-an-action-when-the-video-ends\} :::link Bài viết chính: [Hành động](onboarding-actions) ::: Phần tử Video hỗ trợ trigger **On playback finished** kích hoạt khi video phát đến cuối. Thiết lập nó trong bảng **Interactions** để điều hướng đến màn hình khác, hiển thị CTA, hoặc thực hiện bất kỳ hành động nào khác. ## Biểu tượng \{#icon\} Chọn từ thư viện [Tabler Icons](https://tabler.io/icons) được tích hợp sẵn, với hàng nghìn biểu tượng theo hai phong cách hiển thị: - **Stroke** — chỉ viền ngoài. - **Filled** — tô đặc. Tìm kiếm trong bộ chọn bằng từ khóa để tìm biểu tượng. Đặt màu cho biểu tượng trong bộ chọn **Color** — chọn [kiểu màu](builder-styling) đã lưu hoặc đặt màu tùy chỉnh. ## ID phương tiện tùy chỉnh \{#custom-media-id\} :::important Bạn cũng có thể đặt ID phương tiện tùy chỉnh cho [nền](paywall-head-picture) hình ảnh và video. ::: Gắn thẻ một phần tử hình ảnh hoặc video với ID phương tiện tùy chỉnh để thay thế nó lúc chạy từ code ứng dụng của bạn. Dùng tính năng này cho [hình ảnh được cá nhân hóa](get-pb-paywalls#customize-assets) — ví dụ: hiển thị avatar mà người dùng đã chọn. Phương tiện bạn tải lên trong Flow Builder sẽ đóng vai trò dự phòng. Nếu code của bạn không cung cấp phương tiện cho ID đó lúc chạy, thì nội dung dự phòng sẽ được hiển thị thay thế. Để bật ID phương tiện tùy chỉnh cho phần tử Hình ảnh hoặc Video: 1. Chọn hộp kiểm **Use custom media ID** bên dưới khu vực tải lên. 2. Nhập media ID. 3. Tải lên ảnh hoặc video dự phòng. Trong code ứng dụng của bạn, lấy media theo ID của nó — xem [Tùy chỉnh assets](get-pb-paywalls#customize-assets) để biết SDK API. --- # File: paywall-buttons --- --- title: "Nút bấm trong Flow builder" description: "Thêm và cấu hình các nút hành động trong Flow Builder." --- :::info Phần này mô tả Flow Builder mới, hoạt động với Adapty SDK phiên bản 4.0 trở lên. ::: Các nút là những phần tử tương tác trong Flow Builder, phản hồi khi người dùng nhấn. Dùng chúng cho: - CTA mua hàng — kết nối với sản phẩm và tự động xử lý giao dịch - Điều hướng — di chuyển người dùng giữa các màn hình (Tiếp theo, Quay lại, Đóng, Bỏ qua) - Liên kết tiện ích — Khôi phục mua hàng, Điều khoản dịch vụ và Chính sách bảo mật :::tip Đặt các CTA mua hàng, liên kết khôi phục và liên kết pháp lý vào trong [Footer](builder-containers#footer) — phần này được ghim cố định ở cuối màn hình và che phủ các phần tử cuộn phía dưới. ::: ## Thêm nút \{#add-buttons\} Để thêm bất kỳ nút nào: 1. Nhấn **+** và chọn **Button**. 2. Chọn loại nút. <img src="/assets/shared/img/button-type.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Các nút mua hàng, liên kết và nút đóng đều được cấu hình sẵn các hành động. Đối với liên kết, [hãy cấu hình URL để điều hướng người dùng đến đích](#links). Đối với các loại nút khác, hãy vào bảng **Interactions**. Tại đó, trong mục **Button triggers**, hãy thiết lập các [hành động](onboarding-actions) mà nút cần thực hiện. <img src="/assets/shared/img/button-action.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cấu hình [thiết kế nút](builder-styling) trong bảng **Design**. ## Các loại nút \{#button-types\} ### Nút mua hàng \{#purchase-buttons\} :::link Để nút mua hàng hoạt động, hãy gắn sản phẩm vào màn hình và thêm phần tử **Products**. Xem [hướng dẫn](paywall-product-block). ::: Nút mua hàng sẽ khởi động in-app purchase cho sản phẩm mà người dùng đã chọn trên màn hình. SDK xử lý giao dịch tự động, nên bạn không cần xử lý việc mua hàng trong code ứng dụng. Để thêm nút mua hàng: 1. Nhấp vào **+** và chọn **Button**, sau đó chọn một preset nút. 2. Khi nút đã được chọn, mở tab **Interactions** ở bảng bên phải. 3. Nhấp vào **Add trigger** > **On tap**, sau đó nhấp vào **Add action**. 4. Đặt **Action** thành **Purchase** và **Product** thành `products.selectedProduct`. Biến `products.selectedProduct` luôn được phân giải thành sản phẩm đang được chọn trên màn hình. :::tip Bạn có thể thu hút sự chú ý hơn vào các nút mua hàng bằng cách thêm animation cho chúng. Paywall Builder hiện hỗ trợ kiểu animation **Pulse**. Cấu hình kiểu animation trong bảng **Design**. ::: <img src="/assets/shared/img/purchase-button.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Liên kết \{#links\} :::important Các nút **Terms of Use** và **Privacy Policy** có sẵn hành động **Open URL**. Hãy đặt URL đích ở đó. Các Open URL trống và [liên kết nội tuyến](onboarding-text#inline-link) sẽ chặn việc xem trước và xuất bản. ::: Để tuân thủ một số yêu cầu của cửa hàng, bạn có thể thêm liên kết đến: - Điều khoản dịch vụ - Chính sách quyền riêng tư - Khôi phục giao dịch mua Để thêm liên kết: 1. Nhấn **+** và chọn **Button > Links**. Thao tác này sẽ thêm một hàng các nút inline với các hành động được xác định sẵn: khôi phục giao dịch mua hoặc mở một URL. Nếu bạn không cần tất cả các nút được thêm vào, hãy xóa những nút không cần thiết trong bảng layers. <img src="/assets/shared/img/add-links.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Bây giờ, hãy thiết lập các hành động cho nút: - Nút **Restore purchases** đã xử lý việc khôi phục giao dịch mua rồi. - Đối với mỗi liên kết còn lại: 1. Nhấp vào nút để chọn nó và chuyển sang tab **Interactions** ở bên phải. 2. Dán URL vào trường đó. 3. Theo mặc định, URL sẽ mở trong trình duyệt trong ứng dụng để mang lại trải nghiệm liền mạch cho người dùng. Nếu bạn muốn điều hướng người dùng đến trình duyệt bên ngoài, hãy chọn hộp kiểm **Open in external browser**. <img src="/assets/shared/img/pb-links.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Đóng flow \{#close-flow\} Nút **Close** sẽ đóng flow tự động. Để thêm nút đóng, nhấn **+** và chọn **Button > Close flow**. <img src="/assets/shared/img/close-flow.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::tip Dùng vị trí **Absolute** để đặt nút đóng ở góc màn hình. ::: Bạn cũng có thể cấu hình bất kỳ nút nào khác để đóng flow bằng cách sử dụng [actions](onboarding-actions). ### Nút tùy chỉnh \{#custom-buttons\} Bất kỳ nút nào bạn thêm vào đều có thể được cấu hình để thực hiện các hành động khi nhấn: - Điều hướng đến màn hình tiếp theo - Hiển thị cảnh báo - Đặt [biến](onboarding-variables) - [Hiển thị hoặc ẩn các phần tử trên màn hình](onboarding-element-visibility) - Mở URL - Khôi phục giao dịch - Thực hiện hành động có điều kiện --- # File: builder-tabs --- --- title: "Tabs" description: "Thêm điều hướng tab để chuyển đổi các bảng nội dung trong một flow." --- **Tabs** chia một phần màn hình thành các bảng nội dung có thể chuyển đổi — người dùng nhấn vào tiêu đề tab và bảng bên dưới cập nhật tương ứng. {/* TODO: on-device GIF */} ## Thêm, xóa và chọn tab \{#add-remove-and-select-tabs\} Mỗi tab gồm hai phần - **Tab header** — nhãn có thể nhấn (Tab 1, Tab 2, v.v.). - **Tab content** — một container cho mỗi tab. Bất cứ thứ gì bạn đặt trong container nội dung sẽ hiển thị khi tab đó được chọn. Nhấn **Add tab** để thêm tab mới. Mỗi tab mới sẽ có một container nội dung tương ứng. Để đặt một tab cụ thể là active khi màn hình xuất hiện lần đầu, bật **Selected by default**. ## Tùy chỉnh giao diện tab \{#style-the-tabs\} ### Templates \{#templates\} Flow Builder cung cấp ba template tab có sẵn: - **Segment control** — bộ chuyển đổi hình viên thuốc với góc bo tròn quanh tab đang được chọn. - **Button Tabs** — các tab được tạo kiểu như nút bấm riêng biệt. - **Underline** — nhãn văn bản có gạch chân đánh dấu tab đang được chọn. ### Trạng thái tab \{#tab-states\} Mỗi tab riêng lẻ có bộ chuyển đổi trạng thái (**Default / Selected**) để tùy chỉnh trạng thái đang active và không active riêng biệt — kiểu chữ, màu sắc, nền và viền theo từng trạng thái. ## Selectable group \{#selectable-group\} Tabs là một **selectable group chọn một** — đúng một tab được active tại một thời điểm. Quản lý nhóm từ bảng **Screen settings**, trong mục [Selectable groups](paywall-layout-and-products#selectable-groups). Nhóm cung cấp hai biến: - `tabs.selectedOptionId` — ID của tab đang được chọn. Dùng trong các điều kiện. - `tabs.selectedOptionTitle` — nhãn của tab đang được chọn. Dùng trong văn bản động. Thay `tabs` bằng **Group ID** tùy chỉnh của bạn nếu bạn đã đổi tên nhóm. Xem [Selectable elements and groups](flow-selectable-elements) để có cái nhìn toàn diện hơn. --- # File: builder-toggles --- --- title: "Toggles" description: "Thêm công tắc toggle vào luồng thanh toán của bạn." --- :::warning Apple có thể từ chối ứng dụng sử dụng toggle dùng thử được chọn sẵn. Một toggle được đặt mặc định ở trạng thái "bật" có thể bị gắn cờ là dark pattern thao túng người dùng theo App Store Review Guidelines — điều này ngụ ý người dùng đồng ý dùng thử miễn phí mà không cần lựa chọn rõ ràng. Để tránh bị từ chối, hãy đặt toggle ở trạng thái **tắt** theo mặc định và để người dùng tự chọn tham gia dùng thử. ::: Toggle dùng thử là một công tắc nhị phân cho phép người dùng lựa chọn giữa sản phẩm tiêu chuẩn và sản phẩm dùng thử trên một paywall. Khi người dùng thay đổi trạng thái của nó, toggle có thể kích hoạt một hành động — chẳng hạn như hoán đổi nhóm sản phẩm, cập nhật biến, hoặc hiển thị/ẩn các phần tử — ngay lập tức. Để thêm toggle dùng thử, nhấn **+** trên màn hình đích và chọn **Trial toggle**. Mỗi toggle dùng thử là một phần tử có thể chọn thuộc kiểu **Toggle**. Mỗi phần tử có thể chọn đều được gán một biến để phản ánh trạng thái của nó — ví dụ: một toggle có tên `trial` sẽ có biến `trial.is_selected` với giá trị `True` hoặc `False`. Để các phần tử khác phụ thuộc vào trạng thái của toggle, hãy thiết lập [hành động](onboarding-actions) có điều kiện hoặc [hiển thị có điều kiện](onboarding-element-visibility) dựa trên biến này. --- # File: builder-reviews-and-testimonials --- --- title: "Đánh giá và nhận xét" description: "Thêm đánh giá, xếp hạng và bằng chứng xã hội vào paywall." --- Danh mục phần tử **User Engagement** cung cấp bốn template để hiển thị đánh giá, xếp hạng và bằng chứng xã hội trên paywall. Mỗi template là một bố cục có thể chỉnh sửa hoàn toàn — thay thế văn bản mẫu và áp dụng [màu sắc](builder-styling) cùng [kiểu chữ](onboarding-text) của bạn để phù hợp với phần còn lại của flow. ## Review \{#review\} Một thẻ gồm xếp hạng, trích dẫn và tên tác giả. Dùng để nổi bật một câu trích dẫn đáng nhớ từ người dùng. ## Rating \{#rating\} Số lượt đánh giá và hàng sao, ví dụ "17000+ rating". Dùng để nhấn mạnh số lượng đánh giá. ## App Rating \{#app-rating\} Điểm nổi bật kèm số lượt đánh giá, ví dụ "4.9 / Based on 1000+ reviews". Dùng để làm nổi bật điểm tổng thể cao. ## Social Proof \{#social-proof\} Nhóm avatar kèm số lượng thành viên, ví dụ "Join 50,000+ users". Dùng để nhấn mạnh quy mô cộng đồng. --- # File: flow-timer --- --- title: "Đồng hồ đếm ngược" description: "Thêm đồng hồ đếm ngược vào paywall." --- **Countdown timer** đếm ngược từ một khoảng thời gian cố định về không — khi về đến không, màn hình sẽ dừng lại. ## Mẫu \{#templates\} Danh mục cung cấp bốn kiểu hiển thị: - **Blocks** — Ngày, giờ, phút và giây trong các ô riêng biệt có nhãn. - **Inline Units** — Văn bản một dòng kèm đơn vị theo sau. - **Inline** — Chỉ hiển thị số. - **Badge** — Hiển thị số dạng viên thuốc. ## Cài đặt \{#settings\} ### Đặt thời lượng \{#set-the-duration\} Trong phần **Countdown** ở bảng bên phải, nhập thời lượng bắt đầu theo ngày, giờ, phút và giây. ### Cấu hình hành vi \{#configure-the-behavior\} Dropdown **Behavior** kiểm soát thời điểm bắt đầu đếm ngược: - **Every appear** — Khởi động lại mỗi khi người dùng mở màn hình. Mặc định. - **First appear** — Bắt đầu khi người dùng xem màn hình lần đầu trong phiên ứng dụng hiện tại. Tiếp tục đếm nếu họ quay lại trong cùng phiên; đặt lại khi khởi động lại ứng dụng. - **First appear (persisted)** — Bắt đầu khi người dùng mở màn hình lần đầu và tiếp tục đếm qua các lần khởi động ứng dụng. ### Kích hoạt hành động khi hết giờ \{#trigger-an-action-when-the-timer-ends\} :::link Bài viết chính: [Hành động](onboarding-actions) ::: Thêm trigger **On timer end** để thực hiện một hành động khi đồng hồ đếm ngược về không — ví dụ: chuyển sang màn hình khác hoặc ẩn huy hiệu giảm giá. --- # File: onboarding-quizzes --- --- title: "Câu hỏi trắc nghiệm trong flow" description: "Thêm câu hỏi trắc nghiệm tương tác vào flow Adapty của bạn để thu thập sở thích người dùng và tạo flow được cá nhân hóa — không cần viết code." --- Dùng câu hỏi trắc nghiệm để hiển thị cho người dùng các lựa chọn được định sẵn. Khác với ô nhập liệu, câu hỏi trắc nghiệm không có trường nhập tay — người dùng chọn từ các tùy chọn bạn đã thiết lập. Dùng chúng để thu thập sở thích, phân khúc người dùng, hoặc phân nhánh flow dựa trên câu trả lời của họ. ### Thêm câu hỏi trắc nghiệm \{#add-a-quiz\} 1. Nhấp **+** ở góc trên bên trái. 2. Chọn **Quiz**. 3. Chọn loại câu hỏi trắc nghiệm: - **Icon/image/emoji options:** Danh sách dọc các tùy chọn có thể chọn, mỗi tùy chọn có icon, hình ảnh hoặc emoji kèm nhãn văn bản. - **Icon/image/emoji grid:** Lưới các tùy chọn có thể chọn, mỗi tùy chọn có icon, hình ảnh hoặc emoji. - **Rating:** Thang điểm để người dùng đánh giá — theo số hoặc theo sao. ### Thiết lập điều hướng có điều kiện \{#set-up-conditional-navigation\} Để định tuyến người dùng theo hướng khác nhau dựa trên lựa chọn của họ, hãy đặt hành động có điều kiện trên **nút điều hướng**, không phải trên tùy chọn câu hỏi: 1. Chọn nút điều hướng. 2. Trong bảng **Interactions**, thêm trigger **On Tap** với hành động **Conditional**. 3. Trong hộp thoại **Edit Action**, xây dựng hàng **if**: - Ở bên trái, nhấp `{}` và chọn **Elements → Screen → `<quizElementId>.selectedOptionId`** để tham chiếu lựa chọn của người dùng. - Giữ toán tử là `=`. - Ở bên phải, nhập elementId cần khớp — ví dụ: `rock`. 4. Trong **then**, đặt hành động thành **Navigate to** và chọn màn hình đích. 5. Trong **else**, đặt đích **Navigate to** dự phòng, hoặc nhấp **+ Add else/if** để thêm điều kiện cho các tùy chọn khác. :::link Xem các hướng dẫn liên quan để hiểu cách sử dụng câu trả lời từ câu hỏi trắc nghiệm: - [Điều hướng có điều kiện](onboarding-navigation-branching) - [Biến](onboarding-variables) - [Hành động](onboarding-actions) ::: ### Thay đổi loại câu hỏi trắc nghiệm \{#change-quiz-type\} Mặc định, câu hỏi trắc nghiệm là **multi choice** — người dùng có thể chọn nhiều tùy chọn cùng lúc. Chuyển sang **single choice** nếu bạn muốn người dùng chỉ chọn một tùy chọn. 1. Chọn màn hình chứa câu hỏi trắc nghiệm. 2. Trong **Screen settings**, cuộn đến **Selectable groups** và nhấp vào câu hỏi trắc nghiệm của bạn. 3. Trong hộp thoại **Edit group**, mở **Group type** và chọn: - **Single choice** — chỉ có thể chọn một tùy chọn tại một thời điểm. - **Multi choice** — người dùng có thể chọn nhiều tùy chọn. 4. Nhấp **Save**. --- # File: builder-inputs-and-forms --- --- title: "Inputs và forms trong Flow Builder" description: "Thêm các phần tử form tương tác như trường văn bản và checkbox." --- Dùng inputs để thu thập dữ liệu người dùng nhập vào — chẳng hạn như tên, địa chỉ email, hoặc ngày sinh. Lưu câu trả lời và tham chiếu chúng ở các nơi khác trong flow, ví dụ như gọi tên người dùng ở màn hình sau. ## Thêm input \{#add-an-input\} 1. Nhấn **+** ở góc trên bên trái. 2. Chọn **Input**. 3. Chọn loại input: - **Text:** Nhập văn bản ngắn bất kỳ. - **Email:** Địa chỉ email, có thể bật kiểm tra định dạng. - **Password:** Nhập văn bản bảo mật, với các yêu cầu có thể cấu hình. - **Number:** Giá trị số, với định dạng có thể cấu hình. - **Phone number:** Số điện thoại. - **Date:** Mở bộ chọn ngày. - **Time:** Mở bộ chọn giờ. - **Date and time:** Mở bộ chọn kết hợp. ## Cấu hình input \{#configure-an-input\} :::link Để biết thêm chi tiết về cài đặt hiển thị — bố cục, kiểu dáng và khả năng hiển thị — xem [Kiểu dáng và giao diện](builder-styling). ::: Với tất cả các loại input, bạn có thể cấu hình các mục sau trong tab **Design**: - **Type:** Thay đổi loại input (Text, Email, Password, Number, Phone number, Date, Time, hoặc Date and time). - **Element ID:** Định danh dùng để tham chiếu giá trị của input ở nơi khác trong flow. Xem [Sử dụng giá trị input](#use-input-values) bên dưới. - **Placeholder:** Văn bản gợi ý hiển thị bên trong trường khi trống. - **State:** Xác định giao diện của input trong các tình huống khác nhau. Chuyển đổi giữa **Default**, **Active**, **Invalid** và **Disabled** rồi áp dụng các kiểu hiển thị khác nhau cho từng trạng thái. - **Typography:** Định dạng chữ cho giá trị hiển thị trong trường. - **Leading and trailing icons:** Thêm icon bên trong trường input. Một số cài đặt chỉ áp dụng cho một số loại input nhất định: | Cài đặt | Loại input | |---------------------------------|-----------------------------------| | Nút xóa | Text, Email | | Xác thực định dạng email | Email | | Hiển thị biểu tượng mật khẩu | Password | | Chỉnh sửa yêu cầu mật khẩu | Password | | Định dạng số | Number | | Định dạng ngày/giờ | Date, Time, Date and time | | Ngày tối thiểu và tối đa | Date, Date and time | ## Sử dụng giá trị đầu vào \{#use-input-values\} Mỗi trường nhập liệu đều tự động trở thành một biến — bạn không cần thiết lập hay thêm action **On Submit** nào cả. Giá trị được tham chiếu thông qua **Element ID** của trường đó, do bạn đặt trong **Input Settings**. Để sử dụng giá trị đầu vào ở nơi khác trong flow (ví dụ: cá nhân hóa nội dung, điền vào trường khác, hoặc điều hướng có điều kiện), hãy chèn một biến và chọn: **Element > Screen > `<elementId>.value`** :::link Xem các hướng dẫn liên quan để hiểu cách sử dụng các giá trị đầu vào đã lưu: - [Điều hướng có điều kiện](onboarding-navigation-branching) - [Biến](onboarding-variables) ::: ## Xác thực đầu vào \{#input-validation\} Hành vi xác thực phụ thuộc vào loại đầu vào. Mỗi đầu vào đều có một biến Boolean chỉ đọc là `<elementId>.isValid`, phản ánh liệu giá trị đã nhập có vượt qua các quy tắc xác thực của đầu vào đó không. Dùng nó trong các hành động có điều kiện hoặc hiển thị có điều kiện — ví dụ, để ẩn nút Tiếp theo cho đến khi định dạng email hợp lệ. :::note - Biến `isValid` là chỉ đọc — bạn không thể tự đặt giá trị cho nó. - Đầu vào trống luôn được coi là hợp lệ. - Đầu vào văn bản không có quy tắc xác thực. `textInput.isValid` luôn trả về `True`. ::: | Loại đầu vào | Hành vi kiểm tra | |---|---| | Text | Không có quy tắc kiểm tra tích hợp. | | Email | Tùy chọn. Bật **Validate email format** trong bảng **Design** để kiểm tra giá trị đã nhập theo định dạng email. | | Phone number | Kiểm tra định dạng số điện thoại tích hợp. Không thể cấu hình trong Builder — quy tắc được đánh giá tại thời điểm chạy. | | Password | Có thể cấu hình. Xem [Yêu cầu mật khẩu](#password-requirements) bên dưới. | | Number | Dựa trên định dạng. Giá trị đã nhập phải khớp với định dạng số đã chọn. Xem [Định dạng số](#number-format) bên dưới. | | Date, Time, Date and time | Tích hợp sẵn. Bộ chọn chỉ chấp nhận các giá trị ngày hoặc giờ hợp lệ. | **Trạng thái hiển thị** [Invalid](builder-styling#input-states) được kích hoạt khi người dùng gửi form — ví dụ, bằng cách nhấn Enter hoặc Done trên bàn phím. Cho đến lúc đó, ô nhập liệu sẽ hiển thị trạng thái **Active** hoặc **Default**. ### Yêu cầu về mật khẩu \{#password-requirements\} Trường nhập mật khẩu hỗ trợ các quy tắc kiểm tra có thể tùy chỉnh. Nhấn **Edit password requirements** trong bảng **Design** để mở trình chỉnh sửa quy tắc. Các quy tắc đã bật sẽ hiển thị dưới dạng danh sách kiểm tra trực tiếp bên dưới trường nhập — mỗi mục sẽ được đánh dấu ngay khi quy tắc tương ứng được thỏa mãn. Các quy tắc khả dụng: - **Min length** — số ký tự tối thiểu. Mặc định: 8. - **Max length** — số ký tự tối đa. Mặc định: 32. - **Uppercase letter** — ít nhất một ký tự A–Z. - **Lowercase letter** — ít nhất một ký tự a–z. - **Number** — ít nhất một chữ số. - **Special character** — ít nhất một ký tự không phải chữ-số (ví dụ: `!@#$%`). Mật khẩu chỉ hợp lệ khi tất cả các quy tắc đã bật đều được thỏa mãn. ### Định dạng số \{#number-format\} Dropdown **Format** trong cài đặt input **Number** kiểm soát cách phân tích giá trị nhập vào: - **Integer** — chỉ chấp nhận số nguyên (ví dụ: `4`). - **Decimal (Point)** — số thập phân với dấu chấm làm dấu phân cách (ví dụ: `4.89`). - **Decimal (Comma)** — số thập phân với dấu phẩy làm dấu phân cách (ví dụ: `4,89`). Các giá trị không khớp với định dạng đã chọn sẽ được coi là không hợp lệ. ## Kích hoạt hành động theo sự kiện input \{#trigger-actions-on-input-events\} :::link Bài viết chính: [Hành động](onboarding-actions) ::: Bạn có thể chạy các hành động phản hồi lại thao tác nhập của người dùng thông qua bảng **Interactions**: - **On changed** — kích hoạt khi người dùng thay đổi giá trị của input. Có sẵn trên tất cả loại input. - **On submit** — kích hoạt khi người dùng submit một text input bằng cách nhấn Enter hoặc Done trên bàn phím. Bộ chọn date và time không có trigger này. --- # File: onboarding-navigation-branching --- --- title: "Điều hướng và phân nhánh" description: "Hướng dẫn người dùng qua các màn hình bằng các tuyến tĩnh và phân nhánh động." --- Điều hướng và phân nhánh cho phép bạn dẫn dắt người dùng qua từng bước trong flow: dùng tuyến tĩnh để đưa tất cả mọi người đến các màn hình chính, và điều hướng động để điều chỉnh flow dựa trên lựa chọn của người dùng. :::link Điều hướng là một loại action. Để tìm hiểu thêm về các action, xem [Actions](onboarding-actions). ::: <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/OLl-WziDMhU?si=_eUtsmbEuFAaLj1r" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ## Điều hướng giữa các màn hình \{#navigate-between-screens\} Bạn có thể cấu hình điều hướng tĩnh và động bằng các thành phần khác nhau trong flow. ### Điều hướng tĩnh \{#static-navigation\} Điều hướng tĩnh đưa tất cả người dùng đến cùng một màn hình đích. Để thiết lập: 1. Chọn bất kỳ thành phần nào người dùng có thể nhấn — nút, câu trả lời quiz, hoặc toggle. 2. Mở bảng **Interactions** ở bên phải. Ở đó, nhấn **Add trigger**. Để điều hướng người dùng ngay khi họ nhấn vào một tùy chọn quiz — mà không cần nhấn thêm một nút riêng — hãy chọn thành phần tùy chọn quiz ở đây thay vì nút. 3. Thiết lập trigger **On tap**: - **Action**: Chọn **Navigate to screen**. - **Destination**: Chọn màn hình đích. ### Điều hướng động \{#dynamic-navigation\} Điều hướng động định tuyến người dùng dựa trên câu trả lời quiz, trạng thái của các thành phần toggle, và các thuộc tính tùy chỉnh. Bất kỳ [thành phần có thể chọn](flow-selectable-elements) nào đều có thể là điều kiện cho điều hướng động. Để thiết lập: 1. Chọn thành phần sẽ điều hướng người dùng. 2. Mở bảng **Interactions** ở bên phải. Ở đó, nhấn **Add trigger**. Để điều hướng người dùng ngay khi họ nhấn vào một tùy chọn quiz — mà không cần nhấn thêm một nút riêng — hãy chọn thành phần tùy chọn quiz ở đây thay vì nút. 3. Thiết lập trigger **On tap**: - **Action**: Chọn **Conditional**. - **Conditions**: Đặt các action điều hướng có điều kiện. Tìm hiểu thêm [tại đây](onboarding-actions#conditional-actions). ## Đóng flow \{#close-flow\} Nếu hành trình người dùng của bạn cần đóng flow, bạn có thể thiết lập bằng nút hoặc quiz một lựa chọn: 1. Thêm và chọn thành phần phải đóng flow khi nhấn. 2. Mở bảng **Interactions** ở bên phải. Ở đó, nhấn **Add trigger**. 3. Thiết lập trigger **On tap**: - **Action**: Chọn **Close flow**. --- # File: onboarding-actions --- --- title: "Hành động" description: "Xác định các hành động được kích hoạt bởi tương tác của người dùng trong builder." --- Panel **Interactions** cho phép bạn xác định cách các phần tử trong flow phản hồi với các sự kiện — chẳng hạn như chạm, xuất hiện phần tử và gửi form. Với mỗi sự kiện, bạn gán một hoặc nhiều hành động: điều hướng giữa các màn hình, hiển thị hoặc ẩn các phần tử, mở URL, đặt biến và nhiều hơn nữa. Sử dụng điều kiện để tùy chỉnh flow dựa trên dữ liệu người dùng. Mỗi tương tác tuân theo một chuỗi ba phần: 1. **Element**: Thành phần màn hình khởi đầu tương tác — một nút, câu trả lời quiz, trường nhập liệu, hoặc bất kỳ thứ gì khác. 2. **Trigger**: Sự kiện kích hoạt logic, chẳng hạn như thao tác chạm, một phần tử xuất hiện, hoặc việc gửi form. 3. **Action**: Tác vụ mà flow thực hiện để phản hồi. Một trigger có thể chạy nhiều action theo thứ tự. ## Thiết lập tương tác \{#set-up-interactions\} Để thiết lập một tương tác: 1. Chọn một phần tử trên màn hình hoặc trong panel **Layers**. 2. Ở bên phải, chuyển sang panel **Interactions** và nhấn **Add trigger**. 3. Trong phần **Button triggers**, chọn [loại trigger](#trigger-types). 4. Nhấn **Add action**, nhấn vào tên action, rồi chọn [loại action](#action-types) từ dropdown trong cửa sổ **Edit action**. 5. Cấu hình thuộc tính của action dựa trên [loại action](#action-types) bạn đã chọn. 6. Nếu cần, nhấp **Add action** để thêm nhiều action cho cùng một trigger. ## Các loại trigger \{#trigger-types\} Trigger kích hoạt theo hành vi của người dùng, trạng thái thay đổi của phần tử, hoặc khi màn hình tải. **On screen appear** là loại phổ quát; các loại còn lại dành riêng cho từng phần tử. | Trigger | Kích hoạt khi... | Hỗ trợ trên | |---|---|---| | **On screen appear** | Màn hình tải | Tất cả các phần tử | | **On tap** | Người dùng nhấn vào phần tử | [Buttons](paywall-buttons), [quiz options](onboarding-quizzes), [toggles](builder-toggles), [countdowns](flow-timer), [videos](custom-media) | | **On changed** | Người dùng thay đổi giá trị của input (gõ phím, chọn ngày hoặc giờ) | Tất cả [input elements](builder-inputs-and-forms) | | **On submit** | Người dùng gửi text input bằng cách nhấn Enter hoặc Done trên bàn phím | [Text-based inputs](builder-inputs-and-forms) | | **On timer end** | Một phần tử [Countdown](flow-timer) chạm về không | [Countdown](flow-timer) | | **On playback finished** | Một [Video](custom-media) phát đến hết | [Video](custom-media) | Đối với các phần tử không có tương tác tích hợp sẵn (như [Loader](builder-loaders-and-progress-bars)), **On screen appear** là trigger duy nhất có sẵn. ## Các loại hành động \{#action-types\} :::important **Bất kỳ hành động điều hướng nào** đưa người dùng đến màn hình khác phải luôn là hành động cuối cùng trong danh sách. Các hành động đặt sau nó (như "Set Variable") có thể không thực thi vì ứng dụng đã chuyển sang màn hình khác rồi. ::: ### Điều hướng đến màn hình \{#navigate-to-screen\} Đây là hành động chính để chuyển người dùng giữa các màn hình. Hành động này đưa người dùng đến màn hình đích đã được chỉ định. Với hành động này, bạn chỉ cần thiết lập màn hình đích. Nếu muốn bật tính năng điều hướng động, hãy xem [Điều hướng và phân nhánh](onboarding-navigation-branching) hoặc phần [Hành động có điều kiện](#conditional-actions). ### Điều hướng đến màn hình tiếp theo \{#navigate-next\} Chuyển người dùng đến màn hình tiếp theo theo thứ tự màn hình trong flow. Dùng tính năng này cho các flow tuyến tính, nơi thứ tự màn hình trong trình chỉnh sửa khớp với thứ tự bạn muốn người dùng xem. ### Quay lại \{#navigate-back\} Đưa người dùng trở lại màn hình trước đó trong lịch sử điều hướng, thay vì màn hình trước đó trong chuỗi trình tự. ### Mở URL \{#open-url\} :::tip Dùng [liên kết nội tuyến](onboarding-text#inline-link) để chèn liên kết vào văn bản thông thường. ::: Mở một địa chỉ web cụ thể. Dùng tính năng này để đưa người dùng đến các trang web, bài viết, hoặc trang mạng xã hội bên ngoài màn hình native của ứng dụng. Với hành động này, bạn có thể cấu hình hai thiết lập: - **URL address**: Đặt địa chỉ URL. Ngoài ra, bạn có thể làm cho URL trở nên động — ví dụ: điều hướng người dùng đến các trang khác nhau dựa trên câu trả lời quiz hoặc dữ liệu họ đã nhập. Để làm điều này, nhấp vào Variable icon và chọn biến bạn muốn sử dụng. - **Open in external browser**: Xác định nơi bạn muốn mở các liên kết bên ngoài. Mặc định, các liên kết sẽ mở trong trình duyệt trong ứng dụng để giữ người dùng ở lại trong app. Chọn hộp kiểm **Open in external browser** nếu bạn muốn mở liên kết trong trình duyệt bên ngoài. ### Đóng flow \{#close-flow\} Đóng flow hiện tại. ### Hiển thị/ẩn phần tử \{#showhide-elements\} Hiển thị hoặc ẩn một phần tử cụ thể trên màn hình. Hành động này ghi đè trạng thái ban đầu được thiết lập trong **Visibility** ở bảng **Design**. Nếu **Visibility** được đặt thành **Hide**, hành động **Show** sẽ làm cho phần tử đó xuất hiện. :::important Hành động **Show** hoặc **Hide** không có phần tử mục tiêu sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). Hãy chọn một mục tiêu hoặc xóa hành động đó. ::: <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/3w3YSOmI3tQ?si=vPhoQGt44SI285Ru" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> ### Hiển thị cảnh báo \{#show-alert\} Hiển thị một cửa sổ pop-up hệ thống gốc. Người dùng phải nhấn **Ok** để tiếp tục. Với cảnh báo, bạn cần thiết lập **Title** và **Message**. Trong cả hai, bạn có thể dùng biến để làm nội dung động. Để thực hiện, nhấn Variable icon và chọn biến bạn muốn dùng. :::important Một hành động **Show alert** có cấu hình trống hoặc chưa hoàn chỉnh sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). Hãy điền đầy đủ cả hai trường hoặc xóa hành động này đi. ::: ### Đặt biến \{#set-variable\} Cập nhật giá trị của một biến trong flow. Trước khi thêm action này, hãy tạo các biến trong bảng **Variables** ở bên trái (xem [Variables](onboarding-variables)). Nhấn **Add variable** và thiết lập số lượng biến cùng giá trị tương ứng tùy theo nhu cầu. :::important Một action **Set variable** không có phép gán nào sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). Hãy cấu hình ít nhất một phép gán hoặc xóa action này đi. ::: ### Mua hàng \{#purchase\} Kích hoạt flow mua hàng trực tiếp từ một nút hoặc tương tác trong onboarding của bạn. Dùng tính năng này để cho phép người dùng đăng ký hoặc mua sản phẩm mà không cần rời khỏi flow. Bạn có thể cấu hình hai hành vi cho action này: - **In-app store**: Khởi động một giao dịch mua hàng native. Đặt **Product** thành một sản phẩm cụ thể, hoặc `products.selectedProduct` cho lựa chọn hiện tại của người dùng trên màn hình. - **Web payment**: Chuyển người dùng đến [web paywall](web-paywall) thay vì kích hoạt giao dịch mua hàng native. Dùng tùy chọn này khi bạn muốn xử lý giao dịch bên ngoài ứng dụng, chẳng hạn như các ưu đãi gói đăng ký qua web. :::important Một hành động **Purchase** không có **Product** hoặc **Web Paywall URL** đích sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). Hãy gán mục tiêu hoặc xóa hành động đó. ::: ### Khôi phục giao dịch mua \{#restore-purchases\} Kích hoạt flow khôi phục giao dịch mua trên thiết bị. Người dùng nhấn vào đây khi đã mua gói đăng ký trên thiết bị khác hoặc sau khi cài đặt lại ứng dụng và cần lấy lại quyền truy cập. Không có gì cần cấu hình cho action này — Adapty xử lý việc khôi phục thông qua flow native của cửa hàng. Action **Restore purchases** cũng được cấu hình sẵn trên liên kết **Restore** trong bộ preset nút **Links** (xem [Thiết lập giao dịch mua](paywall-product-block#restore-purchases)). ## Hành động tùy chỉnh \{#custom-actions\} Hành động tùy chỉnh kích hoạt một **Action ID** được đặt tên mà chính code ứng dụng của bạn xử lý. Dùng khi các loại hành động tích hợp sẵn không đáp ứng được nhu cầu. Adapty cung cấp trigger; ứng dụng của bạn thực thi hành vi: 1. Trong builder, bạn gán một **Action ID** cho tương tác của một phần tử. 2. Khi người dùng kích hoạt tương tác, flow sẽ truyền ID đó đến ứng dụng của bạn. 3. Ứng dụng của bạn khớp ID và chạy code của bạn. ### Thiết lập hành động tùy chỉnh \{#set-up-a-custom-action\} 1. Trong cửa sổ **Edit action**, đặt **Action ID** — một chuỗi mà ứng dụng của bạn sẽ nhận diện (ví dụ: `show_discount`). 2. Trong code ứng dụng, hãy triển khai một handler cho Action ID này. Xem [Xử lý hành động paywall](handle-paywall-actions) để biết chi tiết triển khai và ví dụ code. :::important Một hành động **Custom** không có **Action ID** sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). Hãy gán Action ID hoặc xóa hành động đó. ::: ### Những gì bạn có thể làm với custom action \{#what-you-can-do-with-custom-actions\} Custom action tự nó không làm gì cả. Bạn đặt một Action ID tĩnh trong builder, rồi code trong app của bạn xử lý khi nhận được ID đó. Mọi trường hợp sử dụng bên dưới đều theo cùng một mẫu: gán ID trong flow, sau đó xử lý nó trong code của bạn. - **Kích hoạt một sự kiện in-app**: Gắn ID như `viewed_special_offer`, sau đó ghi lại sự kiện vào hệ thống analytics khi ứng dụng nhận được nó. - **Yêu cầu quyền hệ thống**: Gắn ID như `request_location`, sau đó gọi hộp thoại xin quyền từ hệ điều hành trong ứng dụng. Với những quyền mà hộp thoại không thể cấp, hãy mở phần cài đặt hệ thống của điện thoại thay thế. Adapty không hiển thị hộp thoại đó — ứng dụng của bạn mới là bên thực hiện. - **Bắt đầu xác thực gốc**: Gắn ID như `login_google`, sau đó hiển thị màn hình đăng nhập của riêng bạn. Flow không thể tự đăng nhập người dùng. - **Áp dụng logic nghiệp vụ**: Gắn ID như `apply_discount`, sau đó mở khóa nội dung hoặc thay đổi trạng thái ứng dụng từ phía bạn. - **Truyền câu trả lời quiz về ứng dụng**: Gán một Action ID khác nhau cho mỗi lựa chọn (ví dụ: `goal_weight_loss` và `goal_muscle`), sau đó đọc ID đó trong code của bạn. Dùng ID này để đặt [thuộc tính người dùng tùy chỉnh](setting-user-attributes#custom-user-attributes) mà bạn có thể phân khúc sau này. Vì action chỉ mang một ID cố định, đây là cách duy nhất để ghi lại lựa chọn — flow không thể gửi giá trị đã chọn. :::important Một custom action được kích hoạt ngay khi người dùng chọn một tùy chọn. Nếu người dùng thay đổi câu trả lời, flow sẽ kích hoạt Action ID mới. Ứng dụng của bạn sẽ nhận cả hai theo thứ tự — ví dụ: `goal_weight_loss`, rồi `goal_muscle`. Hãy đảm bảo handler của bạn có tính idempotent để tín hiệu mới nhất luôn được ưu tiên. ::: ### Những gì custom action không thể làm \{#what-custom-actions-cant-do\} Custom action là tĩnh. Action ID được cố định ngay khi bạn build flow — nó không thể đọc [biến](onboarding-variables) hay [dữ liệu nhập của người dùng](builder-inputs-and-forms). Khi action được kích hoạt, ứng dụng của bạn chỉ nhận được ID đó, không bao giờ là email, số điện thoại hay dữ liệu nhập nào khác mà người dùng đã điền. Các trường nhập liệu vẫn nằm trong flow dưới dạng biến để phân nhánh và cá nhân hóa. Để sử dụng những giá trị đó trong ứng dụng, hãy thu thập chúng thông qua UI hoặc API của riêng bạn. Custom actions cũng là một chiều. App của bạn không thể trả kết quả về cho flow, và flow cũng không chờ code của bạn chạy xong. Nếu sau custom action có action **Navigate next**, người dùng sẽ chuyển sang màn hình tiếp theo ngay cả khi code của bạn thất bại — ví dụ như khi người dùng đóng màn hình đăng nhập mà chưa đăng nhập. Kết hợp với Action ID cố định, điều này loại trừ khả năng xác thực dữ liệu nhập của người dùng trong app — ví dụ như kiểm tra mã SMS người dùng nhập và phân nhánh dựa trên kết quả. Nếu phần còn lại của flow phụ thuộc vào những gì code của bạn đã làm, hãy [chia flow thành hai placement](#continue-the-flow-based-on-the-result). ### Tiếp tục flow dựa trên kết quả \{#continue-the-flow-based-on-the-result\} Nếu một số màn hình chỉ nên xuất hiện sau khi một custom action thành công — ví dụ, các màn hình xuất hiện sau khi đăng nhập — hãy chia flow thành hai [placement](placements) và để ứng dụng của bạn quyết định khi nào hiển thị phần thứ hai: 1. Trong placement đầu tiên, tạo một flow kết thúc bằng một custom action (ví dụ: `login`). 2. Trong ứng dụng của bạn, xử lý Action ID: hiển thị màn hình đăng nhập và kiểm tra xem người dùng đã đăng nhập chưa. 3. Nếu người dùng đã đăng nhập, hiển thị flow từ placement thứ hai với các màn hình tiếp theo. Bằng cách này, ứng dụng của bạn kiểm soát việc chuyển màn hình dựa trên kết quả thực tế, thay vì flow tự điều hướng tiến lên bất kể kết quả như thế nào. ## Hành động có điều kiện \{#conditional-actions\} <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/xmWSEPxnI0s?si=mazHQHE89qEDxvPA" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Sử dụng các hành động có điều kiện để phân chia flow thành các nhánh khác nhau dựa trên dữ liệu người dùng. Một số trường hợp sử dụng phổ biến bao gồm: - Bạn có một bài quiz trên màn hình và muốn điều hướng người dùng đến các màn hình khác nhau dựa trên câu trả lời của họ. Trong trường hợp này, hãy thêm hành động có điều kiện vào một nút. - Bạn muốn cung cấp các sản phẩm và ưu đãi khác nhau cho các nhóm người dùng khác nhau. Đặt chúng trên các màn hình riêng biệt và thiết lập điều kiện cho nút điều hướng. - Bạn muốn bỏ qua một số bước cho người dùng đã hoàn thành hướng dẫn trong phiên ứng dụng trước đó. Các hành động có điều kiện hoạt động như một chuỗi if / else-if / else. Ứng dụng đọc các quy tắc từ trên xuống dưới và dừng lại ở kết quả khớp đầu tiên: 1. **IF**: Flow kiểm tra điều kiện chính. - Kết quả là True? Flow thực thi ngay các hành động THEN và dừng lại. - Kết quả là False? Flow bỏ qua và chuyển sang phần tiếp theo. 2. **ELSE IF**: Bạn có thể thêm các kiểm tra bổ sung tại đây (ví dụ: "Nếu không phải Premium, người dùng có đang dùng Trial không?"). 3. **ELSE** (Dự phòng): Nếu không có quy tắc nào ở trên khớp, flow sẽ thực thi các hành động trong phần cuối này. :::important - Nếu một quy tắc được thêm vào nhưng chưa có hành động nào được gán, việc khớp điều kiện sẽ không làm gì cả. - Một quy tắc chưa hoàn chỉnh (chưa có toán tử hoặc giá trị) sẽ [chặn việc xem trước và xuất bản](builder-save-publish#troubleshooting). ::: Với mỗi quy tắc, chọn một biến để đánh giá và một hành động để thực thi. Bạn có thể đặt nhiều hơn một hành động cho mỗi quy tắc. :::important Flow chỉ thực thi một quy tắc duy nhất — quy tắc đầu tiên khớp với điều kiện. Nếu bạn cần thực thi cả **IF** và **ELSE IF** cùng lúc, hãy thêm cả hai hành động vào **IF**. ::: Để tìm hiểu cách làm cho các phần tử có thể chọn được và tổ chức chúng thành các nhóm để sử dụng trong điều kiện, xem [Phần tử có thể chọn và nhóm](flow-selectable-elements). ## Khắc phục sự cố \{#troubleshooting\} Mọi thao tác có trường bắt buộc bị thiếu sẽ chặn việc xem trước và xuất bản. Xem [Lưu & xuất bản flow](builder-save-publish#troubleshooting) để biết danh sách đầy đủ. --- # File: builder-loaders-and-progress-bars --- --- title: "Thanh tiến trình và loader" description: "Hiển thị tiến trình từng bước và trạng thái bận trong một flow." --- Danh mục **Progress** cung cấp hai loại phần tử — một loại theo dõi tiến trình từng bước qua flow nhiều màn hình, và một loại hiển thị trạng thái đang xử lý tại chỗ. ## Thanh tiến trình \{#progress-indicators\} ### Kiểu hiển thị \{#indicator-styles\} Phần tử **Progress** hiển thị vị trí của người dùng trong một flow nhiều màn hình. Danh mục này có ba kiểu hiển thị: - **Linear** — Một thanh đơn tự điền khi người dùng tiến lên. - **Segmented** — Các thanh riêng biệt cho từng bước, điền lần lượt. - **Connectors** — Các vòng tròn có nhãn nối với nhau bằng đường thẳng (ví dụ: Bước 1, Bước 2, Bước 3 theo thứ tự). ### Khớp bước với màn hình \{#match-steps-to-screens\} Mặc định, thanh tiến trình theo dõi tất cả màn hình trong flow. Để giới hạn chỉ theo dõi một số màn hình, chọn từ dropdown **Screens**. Hoặc, mở màn hình bạn muốn loại trừ và bỏ chọn ô **Include screen in progress indicator**. Tắt **One segment per screen** nếu bạn cần kiểm soát chi tiết hơn số bước hiển thị. :::warning Vị trí bước được xác định theo danh sách màn hình — không phải theo thứ tự người dùng thực sự xem. Trong các flow không tuyến tính, bước hiển thị trên thanh tiến trình có thể nhảy lên hoặc lùi lại. ::: ### Trạng thái bước \{#step-states\} Mỗi bước có ba trạng thái — **Completed**, **Current**, và **Upcoming**. Chọn một bước bên trong thanh tiến trình để chỉnh kiểu dáng của từng trạng thái trong bảng bên phải. Dùng **Apply changes to all states** để sao chép chỉnh sửa sang hai trạng thái còn lại. Chỉnh sửa một bước sẽ ảnh hưởng đến tất cả các bước trong cùng một thanh tiến trình. ### Bố cục và vị trí \{#layout-and-positioning\} Thanh tiến trình là phần tử toàn cục, nên bạn không thể đặt nó bên trong một [container](builder-containers). Bạn cũng không thể tự đặt vị trí cho nó — nó dùng định vị tuyệt đối theo mặc định. Khi người dùng cuộn màn hình, thanh tiến trình giữ nguyên vị trí còn nội dung cuộn bên dưới. Để kiểm soát khoảng cách xung quanh thanh tiến trình, dùng các điều khiển **Margin** và **Padding** trong phần **Spacing** thay vì di chuyển phần tử. Nếu bố cục trông không ổn, hãy điều chỉnh margin hoặc padding trên cả thanh tiến trình lẫn phần tử liền kề. ## Loader \{#loaders\} **Loader** là phần tử có animation gợi ý rằng hệ thống đang xử lý gì đó: ví dụ, xử lý câu trả lời quiz của người dùng để chuẩn bị kế hoạch cá nhân hóa. Danh mục này có ba mẫu: - **Spinner** — Vòng tròn xoay. - **Spinner with label** — Vòng tròn xoay kèm chú thích (ví dụ: "Loading..."). - **Loader** — Thanh ngang điền dần khi công việc tiến triển. {/* - **Loader with label** — A horizontal bar with a caption and percentage (e.g., "Analyzing... 47%"). */} :::warning Loader cần có **trigger** để xuất hiện và biến mất. Mở tab **Interactions** để thiết lập logic này — ví dụ, hiển thị loader sau khi người dùng gửi quiz. ::: --- # File: onboarding-variables --- --- title: "Biến (Variables)" description: "Sử dụng biến để hiển thị dữ liệu động trong flow của bạn." --- Biến cho phép bạn hiển thị nội dung động trong flow — giá sản phẩm, thông tin ưu đãi, và các dữ liệu khác cập nhật theo ngữ cảnh của từng người dùng. Dùng chúng để kiểm soát khả năng hiển thị của phần tử và cá nhân hóa nội dung màn hình. Để mở bảng biến, nhấn vào biểu tượng **{ }** ở bảng bên trái. Bảng có ba tab: - **[Custom](#custom-variables)**: Biến bạn tự tạo và quản lý. - **[Product](#product-variables)**: Biến tích hợp sẵn, tự động lấy dữ liệu sản phẩm và ưu đãi đã được bản địa hóa từ cửa hàng. - **[Element](#element-variables)**: Biến gắn với trạng thái của các phần tử trên canvas. ## Biến tùy chỉnh (Custom variables) \{#custom-variables\} ### Tạo biến tùy chỉnh \{#create-a-custom-variable\} 1. Trong bảng biến, nhấn **+**. 2. Nhập tên cho biến. 3. Chọn kiểu dữ liệu: String, Number, hoặc Boolean. 4. Đặt giá trị khởi tạo. Đây là giá trị biến sẽ giữ khi flow bắt đầu. 5. Nhấn **Create variable**. :::tip Dùng dấu chấm trong tên để nhóm các biến liên quan — ví dụ: `user.score` hoặc `user.goal`. ::: ### Cập nhật biến thông qua tương tác \{#update-a-variable-via-an-interaction\} :::link Xem bài viết [Actions](onboarding-actions) để biết thêm chi tiết. ::: Bạn có thể cập nhật giá trị biến trong lúc chạy bằng cách thêm hành động **Set up variables** vào bất kỳ phần tử nào. 1. Chọn một phần tử trên canvas. 2. Trong tab **Interactions**, nhấn **Add trigger**. 3. Chọn **On tap** rồi nhấn **Add action**. Từ dropdown **Action type**, chọn **Set up variables**. 4. Nhấn **Add variable**. Chọn biến và đặt giá trị mới. :::tip Ví dụ, bạn có thể gán giá trị khác nhau cho `user.goal` tùy theo câu trả lời bài kiểm tra mà người dùng chọn, rồi dùng biến đó để điều hướng họ đến màn hình khác nhau. ::: ## Biến sản phẩm (Product variables) \{#product-variables\} Biến sản phẩm tự động lấy dữ liệu đã bản địa hóa trực tiếp từ các cửa hàng ứng dụng. Dùng chúng trong các trường văn bản để hiển thị giá, tiêu đề và thông tin ưu đãi đã được bản địa hóa, hoặc trong các điều kiện để hiện/ẩn nội dung dựa trên điều kiện đủ điều kiện nhận ưu đãi của người dùng. | Biến | Mô tả | Ví dụ | | :--- | :--- | :--- | | `prod_title` | Tiêu đề sản phẩm đã bản địa hóa | Premium Subscription | | `prod_price` | Giá đã bản địa hóa cho một chu kỳ thanh toán | $9.99 | | `prod_price_per_day` | Giá gói đăng ký chia cho số ngày trong chu kỳ thanh toán. Để trống nếu không phải gói đăng ký. | $0.33 | | `prod_price_per_week` | Giá gói đăng ký chia cho số tuần trong chu kỳ thanh toán. Để trống nếu không phải gói đăng ký. | $2.33 | | `prod_price_per_month` | Giá gói đăng ký quy đổi theo tháng. Để trống nếu không phải gói đăng ký. | $9.99 | | `prod_price_per_year` | Giá gói đăng ký quy đổi theo năm. Để trống nếu không phải gói đăng ký. | $119.88 | | `offer_price` | Giá đã bản địa hóa của ưu đãi giới thiệu hoặc ưu đãi. Để trống nếu người dùng không đủ điều kiện nhận bất kỳ ưu đãi nào. | $0.99 | | `offer_billing_period` | Chu kỳ thanh toán đã bản địa hóa của ưu đãi. Giống `offer_full_duration` đối với ưu đãi dùng thử miễn phí và ưu đãi trả trước. Để trống nếu người dùng không đủ điều kiện. | 1 week | | `offer_full_duration` | Toàn bộ thời hạn đã bản địa hóa của ưu đãi. Để trống nếu người dùng không đủ điều kiện. | 1 month | | `is_free_trial` | Trả về `true` nếu người dùng đủ điều kiện nhận ưu đãi dùng thử miễn phí. | true | | `is_pay_up_front` | Trả về `true` nếu người dùng đủ điều kiện nhận ưu đãi trả trước. | true | | `is_pay_as_you_go` | Trả về `true` nếu người dùng đủ điều kiện nhận ưu đãi trả theo kỳ. | true | :::tip Dùng `is_free_trial`, `is_pay_up_front` và `is_pay_as_you_go` với tính năng hiển thị có điều kiện để hiện hoặc ẩn phần tử dựa trên ưu đãi mà người dùng đủ điều kiện nhận. Ví dụ, chỉ hiển thị dòng thời gian dùng thử miễn phí khi `is_free_trial` là `true`. ::: Giá trị của biến ưu đãi phụ thuộc vào loại ưu đãi mà người dùng đủ điều kiện nhận. Để minh họa, hãy lấy một gói đăng ký hàng tuần tên "Premium Subscription" với giá $5, có ba ưu đãi khả dụng: - **Pay As You Go**: 3 tuần đầu với giá $3 (tính theo tuần), sau đó $5/tuần. - **Pay Up Front**: 3 tuần đầu với giá $8 (tính một lần ngay lập tức), sau đó $5/tuần. - **Free Trial**: Tuần đầu miễn phí, sau đó $5/tuần. Trong ví dụ này, `prod_title` trả về "Premium Subscription" và `prod_price` trả về $5. Giá trị của biến ưu đãi phụ thuộc vào ưu đãi mà người dùng đủ điều kiện nhận: | Biến | Pay As You Go | Pay Upfront | Free Trial | | :--- | :--- | :--- | :--- | | `offer_price` | $3 | $8 | $0 | | `offer_billing_period` | 1 week | 3 weeks | 1 week | | `offer_full_duration` | 3 weeks | 3 weeks | 1 week | Với ưu đãi Pay Upfront và Free Trial, `offer_billing_period` và `offer_full_duration` trả về cùng một giá trị. Với Pay As You Go, hai giá trị này khác nhau vì chu kỳ thanh toán là một tuần nhưng tổng thời hạn là ba tuần. :::note Để tìm hiểu thêm về ưu đãi và cách cấu hình, xem [Offers](offers). ::: ## Biến phần tử (Element variables) \{#element-variables\} Biến phần tử ghi lại lựa chọn của người dùng — những gì họ đã chọn trong bài kiểm tra, tab nào họ đang xem, và nút bật/tắt dùng thử có đang bật không. Kiểu biến phần tử phụ thuộc vào nhóm: - **Single choice**: Bài kiểm tra chọn một đáp án và tab: - `selected_id`: ID phần tử để dùng trong điều kiện - `selected_title`: Tiêu đề phần tử để dùng trong văn bản động - **Multi-choice**: Bài kiểm tra chọn nhiều đáp án: - `selected_ids`: Các ID phần tử để dùng trong điều kiện - `selected_titles`: Các tiêu đề phần tử để dùng trong văn bản động - **Toggle**: Nút bật/tắt dùng thử: - `is_selected`: Giá trị Boolean Các trường hợp sử dụng phổ biến bao gồm: - Hiển thị nội dung khác nhau tùy theo nút bật/tắt dùng thử có đang bật hay không. - [Điều hướng người dùng đến các màn hình khác nhau](onboarding-navigation-branching) dựa trên câu trả lời bài kiểm tra của họ ## Dùng biến trong văn bản \{#use-variables-in-text\} Để chèn biến vào phần tử văn bản: 1. Chọn một phần tử văn bản trên canvas. 2. Trong tab **Design**, tìm trường **Content** và nhập văn bản của bạn. 3. Nhấn vào biểu tượng **{ }** trong trường đó. 4. Chọn biến từ danh sách. :::tip Bạn cũng có thể dùng biến trong các phần tử khác: - Dùng biến trong liên kết và cảnh báo để tạo nội dung động - Tạo điều kiện động dựa trên biến. Ví dụ, điều kiện có thể là `if experience.current > experience.target, navigate to...` ::: ### Định dạng biến \{#style-variables\} Bạn không thể áp dụng định dạng văn bản phong phú cho một biến riêng lẻ. Việc chọn biến trong trường **Content** và áp dụng in đậm, in nghiêng, gạch chân, gạch ngang, hoặc thay đổi màu sắc sẽ không có hiệu lực. Cài đặt văn bản phong phú chỉ áp dụng cho toàn bộ khối văn bản. Để định dạng văn bản, dùng phần **Typography** trong tab **Design**, hoặc áp dụng [kiểu văn bản](onboarding-text#set-up-text-styles) đã lưu. ### Tái sử dụng nội dung trên nhiều màn hình \{#reuse-content-across-screens\} Một số nội dung lặp lại trong flow của bạn — nhãn nút như "Continue", lời kêu gọi hành động thường xuyên, hoặc một tuyên bố miễn trách hiển thị trên nhiều màn hình. Điều tương tự áp dụng cho văn bản dài hơn, chẳng hạn như mô tả tính năng được tái sử dụng trên nhiều màn hình. Thay vì nhập nội dung này vào từng phần tử, hãy lưu vào một biến tùy chỉnh. Điều này hữu ích khi bạn điều hướng người dùng khác nhau đến các màn hình khác nhau nhưng muốn cách diễn đạt nhất quán trên tất cả. 1. [Tạo một biến tùy chỉnh](#create-a-custom-variable) kiểu String và đặt giá trị khởi tạo là văn bản bạn muốn tái sử dụng. Ví dụ, đặt tên là `button.navigation` và giá trị là `Continue`. 2. Chèn biến này vào trường **Content** của từng phần tử mà văn bản cần xuất hiện. Để thay đổi văn bản ở mọi nơi, chỉ cần cập nhật giá trị khởi tạo của biến một lần. Mọi phần tử sử dụng biến đó sẽ tự động cập nhật, giúp bạn không phải chỉnh sửa từng màn hình thủ công. --- # File: onboarding-element-visibility --- --- title: "Hiển thị có điều kiện" description: "Hiển thị hoặc ẩn các phần tử dựa trên điều kiện." --- <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/3w3YSOmI3tQ?si=vPhoQGt44SI285Ru" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Bạn có thể kiểm soát việc một phần tử có hiển thị hay không bằng cách thêm điều kiện vào nó. Phần tử có điều kiện chỉ hiển thị với những người dùng đáp ứng tiêu chí đã chỉ định. :::important Nếu bạn hiển thị hoặc ẩn một phần tử bằng [action](onboarding-actions) **Show** hoặc **Hide**, action đó sẽ ghi đè điều kiện **Visibility** đã đặt trên phần tử đó. Hãy dùng điều kiện **Visibility** cho các phần tử cần luôn hiển thị hoặc ẩn dựa trên một tiêu chí cố định. Hãy dùng action khi khả năng hiển thị cần thay đổi dựa trên tương tác của người dùng — ví dụ: hiển thị một nút sau khi người dùng trả lời một câu hỏi trong quiz. ::: Để thêm điều kiện cho một phần tử: 1. Chọn phần tử trong canvas hoặc bảng layer. 2. Trong phần **Visibility** ở bảng bên phải, chọn **Conditional**. 3. Thiết lập điều kiện bằng cách chọn loại thuộc tính từ một trong ba tab: - **Custom**: Các biến bạn tự tạo và quản lý; giá trị của chúng có thể được cập nhật thông qua tương tác của người dùng. Xem [Variables](onboarding-variables) để biết thêm chi tiết. - **Products**: Thuộc tính của các sản phẩm trong flow của bạn, chẳng hạn như giá hoặc tên. - **Elements**: Trạng thái của các phần tử khác trong flow, chẳng hạn như toggle dùng thử có đang bật hay không. 4. Nhập **Value** cần khớp. 5. Nhấp vào toán tử để thay đổi nếu cần. 6. (Tùy chọn) Nhấp **Add condition** để thêm điều kiện. Dùng bộ chọn để yêu cầu tất cả điều kiện phải khớp, hoặc chỉ cần một trong số đó. --- # File: paywall-dark-mode --- --- title: "Chế độ tối" description: "Cấu hình chế độ tối cho các flow trong Adapty để cải thiện trải nghiệm người dùng." --- Các flow của Adapty hỗ trợ chế độ tối ngay từ đầu. Theo mặc định, mỗi style màu sắc đều có hai phiên bản — sáng và tối — khi bạn áp dụng một style màu cho một phần tử, flow sẽ tự động dùng giá trị phù hợp dựa trên chế độ hiện tại của thiết bị. Adapty cung cấp sẵn một bộ style màu đã được cấu hình, và bạn cũng có thể tạo thêm theo nhu cầu. ## Cấu hình style màu \{#configure-color-styles\} Mỗi **style màu** định nghĩa hai phiên bản màu — sáng và tối. Khi một phần tử sử dụng một style có tên, nó sẽ tự động chuyển đổi giữa hai phiên bản này. Bạn có thể quản lý các style màu tại **Style** > **Colors** ở bên trái. Để thêm một style màu: 1. Trong **Style** > **Colors**, nhấn **Create style**. 2. Chọn màu cho phiên bản sáng và tối. Để đổi tên một style, nhấn **⋮** bên cạnh nó và chọn **Rename**. ## Đặt theme cho thanh trạng thái \{#set-the-status-bar-theme\} Nếu **Status bar** được bật trong bảng **Screen settings**, bạn có thể đặt theme cho nó một cách độc lập: chọn **Light**, **Dark**, hoặc **Auto** trong các tùy chọn **Status bar theme**. ## Xem trước chế độ sáng & tối \{#preview-light--dark-modes\} Để xem trước flow của bạn trông như thế nào ở mỗi chế độ, hãy dùng nút chuyển đổi biểu tượng mặt trời/mặt trăng ở phía dưới khu vực xem trước. ## Xóa chế độ tối \{#remove-dark-mode\} Để xóa hoàn toàn hỗ trợ chế độ tối, trong bảng **Style** > **Colors**, nhấn **⋮** > **Delete dark theme**. --- # File: add-paywall-locale-in-adapty-paywall-builder --- --- title: "Thêm ngôn ngữ trong Flow Builder" description: "Thêm nội dung đã được bản địa hóa trong Flow Builder của Adapty để tiếp cận người dùng trên toàn thế giới bằng ngôn ngữ của họ." --- Bản địa hóa flow giúp flow của bạn hiển thị được bằng nhiều ngôn ngữ khác nhau. Trong Flow Builder, việc bản địa hóa được tổ chức theo từng màn hình, mỗi màn hình hiển thị tỷ lệ phần trăm hoàn thành để theo dõi tiến trình dịch thuật. :::tip Hãy hoàn thiện flow của bạn ở ngôn ngữ mặc định trước khi thêm các ngôn ngữ khác. ::: ## Thêm và thiết lập bản địa hóa \{#add-and-set-up-localization\} 1. Trong bảng điều khiển bên trái, nhấp vào Localizations. Sau đó nhấp vào **Add locale**. Chọn các ngôn ngữ muốn thêm. 2. Mỗi ngôn ngữ được thêm vào sẽ xuất hiện dưới dạng một cột trong bảng bản địa hóa, được điền sẵn các giá trị từ ngôn ngữ mặc định. 3. Để chỉ tập trung vào những phần còn thiếu, bật nút chuyển **Missing only** ở bảng điều khiển bên trái. Bảng sẽ lọc và chỉ hiển thị các hàng chưa được dịch. ## Xuất và nhập cho dịch thuật bên ngoài \{#export-and-import-for-external-translation\} Bạn có thể xuất file bản địa hóa để chia sẻ với người dịch và nhập lại kết quả sau khi dịch xong. Trong thanh công cụ trên cùng, nhấp vào **Import / Export**. ### Định dạng file xuất \{#export-file-format\} Khi xuất, bạn sẽ nhận được file `.tsv` (phân tách bằng tab) với mỗi hàng tương ứng một phần tử có thể dịch. Các cột bao gồm: | Cột | Mô tả | |--------|-------------| | `Screen` | Màn hình chứa phần tử đó (ví dụ: `Welcome`, `Quiz`) | | `Element` | Định danh phần tử được tạo tự động trong màn hình đó. Bạn có thể thay đổi trong **Interactions** > **Element ID**. | | `Property` | Loại thuộc tính (ví dụ: `content`) | | `[default_locale]` | Mã ngôn ngữ mặc định (ví dụ: `en`) | | `[locale]` | Một cột cho mỗi ngôn ngữ đã thêm (ví dụ: `fr`, `es`) | Ví dụ: ``` Screen Element Property en fr es Welcome title content Turn words into art Transformez les mots en art Welcome subtitle content Create stunning images in seconds with AI Créez des images en quelques secondes Quiz quiz-title content What will you create? ``` :::note Để trống các cột ngôn ngữ với những dòng chưa dịch — Adapty sẽ coi chúng là còn thiếu. ::: ### Yêu cầu đối với file nhập \{#import-file-requirements\} - **Định dạng**: `.tsv` (giá trị phân tách bằng tab) - **Header**: phải bao gồm `Screen`, `Element`, `Property` và ít nhất một cột ngôn ngữ - **Tên cột ngôn ngữ**: phải khớp với mã ngôn ngữ của các ngôn ngữ đã được thêm vào flow. Nhập file có mã ngôn ngữ chưa có trong flow sẽ dẫn đến lỗi. - **Nhập một phần**: bạn có thể chỉ đưa vào một tập hợp con các hàng; các hàng không có trong file sẽ giữ nguyên giá trị hiện tại ## Dịch thủ công \{#translate-manually\} Bạn cũng có thể nhập trực tiếp bản dịch vào bất kỳ ô nào trong bảng bản địa hóa. Để quản lý một hàng cụ thể, mở menu ngữ cảnh của hàng đó (**⋮**): - **Reset to default**: Khôi phục bản dịch của hàng về giá trị ngôn ngữ mặc định. ## Xem trước bản địa hóa \{#preview-the-localization\} Để kiểm tra các bản dịch, hãy chuyển ngôn ngữ đang hoạt động trong Flow Builder và xem lại từng màn hình. --- # File: add-flow-remote-config-locale --- --- title: "Bản địa hóa flow bằng remote config" description: "Thêm locale vào remote config của flow để phục vụ các giá trị khác nhau theo ngôn ngữ hoặc khu vực." --- Remote config của một flow có thể chứa một payload JSON riêng cho từng locale. Tại runtime, SDK trả về payload phù hợp với locale của người dùng, nhờ đó bạn có thể cung cấp nội dung đã dịch, hình ảnh khác nhau, hoặc các giá trị theo locale cụ thể mà không cần phát hành phiên bản app mới. ## Thêm locale \{#add-a-locale\} Để thêm locale vào remote config của flow: 1. Mở flow trong Flow Builder. 2. Nhấp vào biểu tượng remote config phía trên phần xem trước màn hình. 3. Nhấp vào **Add locale** phía trên editor. 4. Điền vào hộp thoại: - **Code**: Mã locale, ví dụ `en`, `fr`, hoặc `de`. - **Name**: Tên hiển thị, ví dụ English hoặc French. Adapty sẽ thêm một cột mới vào JSON editor cho locale đó. ## Chỉnh sửa giá trị theo từng locale \{#edit-values-per-locale\} Cột của mỗi locale nhận dữ liệu định dạng JSON riêng. Sử dụng cùng một bộ key cho tất cả các cột và dịch các giá trị tương ứng cho từng locale. Ví dụ, cột tiếng Anh: ```json showLineNumbers { "title": "Try for free!", "cta": "Continue", "trial_days": 7 } ``` Và cột tiếng Tây Ban Nha: ```json showLineNumbers { "title": "¡Prueba gratis!", "cta": "Continuar", "trial_days": 7 } ``` Các cột hoàn toàn độc lập — chỉnh sửa một cột không ảnh hưởng đến các cột còn lại. ## Đọc locale phù hợp trong app \{#read-the-matching-locale-in-your-app\} SDK hiển thị một mục `AdaptyRemoteConfig` cho mỗi locale trong `AdaptyFlow.remoteConfigs`. Chọn mục có `locale` khớp với người dùng của bạn, sau đó đọc `dictionary` hoặc `jsonString` để sử dụng các giá trị tại runtime. ## Sao lưu hoặc chuyển locale \{#back-up-or-move-locales\} Sử dụng menu **Import/Export** phía trên editor để sao lưu remote config hoặc sao chép nó sang các flow khác. File JSON được xuất ra chứa payload của tất cả các locale cùng một lúc. Xem [Tùy chỉnh flow bằng remote config](customize-flow-with-remote-config) để biết định dạng file. --- # File: customize-flow-with-remote-config --- --- title: "Tùy chỉnh flow bằng Remote Config" description: "Tùy chỉnh flow trong Flow Builder của bạn bằng payload JSON từ Remote Config." --- :::important Hướng dẫn này đề cập đến Remote Config cho Flow Builder. Đối với các paywall thông thường được tạo mà không dùng Flow Builder, xem [Thiết kế paywall bằng Remote Config](customize-paywall-with-remote-config). ::: Remote Config cho phép bạn lưu trữ một JSON payload tùy chỉnh mà SDK đọc lúc runtime. Dùng tính năng này để thiết lập các giá trị như tiêu đề, hình ảnh, font chữ, màu sắc hay feature flag mà không cần phát hành phiên bản app mới. ## Làm việc với Remote Config \{#work-with-remote-config\} Để mở Remote Config của một flow, nhấp vào biểu tượng Remote Config phía trên phần xem trước màn hình trong trình chỉnh sửa flow. Trong chế độ xem **JSON**, bạn có thể nhập bất kỳ dữ liệu nào theo định dạng JSON. Trình chỉnh sửa hiển thị một cột cho mỗi ngôn ngữ bạn đã thêm: :::warning Nếu remote config chứa JSON không hợp lệ, bạn sẽ không thể **lưu** hoặc **xuất bản** flow. Xem [Lưu & xuất bản flow](builder-save-publish#troubleshooting) để biết danh sách đầy đủ các vấn đề có thể chặn việc xem trước và xuất bản. ::: Bạn có thể truy cập dữ liệu này sau đó từ SDK thông qua mảng `remoteConfigs` trên `AdaptyFlow`. Adapty lưu trữ một mục `AdaptyRemoteConfig` cho mỗi ngôn ngữ; chọn mục khớp với ngôn ngữ của người dùng và đọc `dictionary` đã được phân tích cú pháp hoặc `jsonString` thô để điều chỉnh flow của bạn khi chạy. Dưới đây là một số ví dụ về cách bạn có thể sử dụng remote config. <Tabs> <TabItem value="Titles" label="Titles" default> ```json showLineNumbers { "screen_title": "Today only: Subscribe, and get 7 days for free!" } # Test titles or other texts ``` </TabItem> <TabItem value="Images" label="Images"> ```json showLineNumbers { "background_image": "https://adapty.io/media/paywalls/bg1.webp" } # Test images on your flow ``` </TabItem> <TabItem value="Fonts" label="Fonts"> ```json showLineNumbers { "font_family": "San Francisco", "font_size": 16 } # Test fonts ``` </TabItem> <TabItem value="Color" label="Color"> ```json showLineNumbers { "subscribe_button_color": "purple" } # Test colors of buttons, texts etc. ``` </TabItem> <TabItem value="HTML" label="HTML"> ```json showLineNumbers { "photo_gallery": "https://adapty.io/media/paywalls/link-to-html-snippet.html" } # Any HTML code that can be displayed in the flow ``` </TabItem> <TabItem value="Soft/Hard Paywall" label="Soft/Hard Paywall"> ```json showLineNumbers { "hard_paywall": true } # By setting it to true, you disallow skipping the paywall without subscribing # You have to handle this logic in your app ``` </TabItem> <TabItem value="Translations" label="Translations"> ```json showLineNumbers { "title": { "en": "Try for free!", "es": "¡Prueba gratis!", "ru": "Попробуй бесплатно!" } } ``` </TabItem> </Tabs> Bạn có thể kết hợp bất kỳ mẫu nào trong số này, hoặc tự định nghĩa các key riêng để thử nghiệm các bản sao nội dung, bố cục hoặc hành vi khác nhau. Tiếp theo, [tạo một placement](create-placement) và thêm flow vào đó. Sau đó hiển thị flow trong ứng dụng của bạn: [iOS](present-remote-config-paywalls) hoặc [Android](present-remote-config-paywalls-android). ## Thêm ngôn ngữ \{#add-a-locale\} Để bản địa hóa flow, nhấp vào **Add locale** phía trên trình chỉnh sửa và chọn ngôn ngữ. Adapty thêm một cột mới vào trình chỉnh sửa cho ngôn ngữ đó. Chỉnh sửa từng cột một cách độc lập — tại runtime, SDK trả về entry `AdaptyRemoteConfig` có `locale` khớp với lựa chọn ngôn ngữ của người dùng. ## Nhập và xuất JSON \{#import-and-export-json\} Dùng menu **Import/Export** phía trên editor để sao lưu, chia sẻ, hoặc chỉnh sửa hàng loạt Remote Config trên tất cả các ngôn ngữ cùng một lúc. - **Export JSON**: Tải về một file JSON duy nhất chứa toàn bộ các ngôn ngữ. - **Import JSON**: Tải lên một file JSON theo cùng định dạng. File được tải lên sẽ thay thế Remote Config hiện tại. File sử dụng mã ngôn ngữ làm key cấp cao nhất, với payload của từng ngôn ngữ là giá trị tương ứng: ```json showLineNumbers { "en": { "title": "Get Premium", "cta": "Continue", "trial_days": 7, "features": ["sync", "export", "ai"] }, "fr": { "title": "Passez à Premium", "cta": "Continuer", "trial_days": 7, "features": ["synchronisation", "exportation", "IA"] } } ``` Mỗi khối ngôn ngữ tuân theo cùng một cấu trúc JSON mà bạn nhập trực tiếp vào cột locale. --- # File: paywall-device-compatibility-preview --- --- title: "Xem trước flow" description: "Xem trước khả năng tương thích của flow trên các thiết bị để có trải nghiệm tối ưu." --- Bạn có hai cách để xem trước flow trên các loại màn hình khác nhau: - **Xem trước trên thiết bị**: Kiểm tra flow trông như thế nào trên thiết bị thực ở bất kỳ giai đoạn nào trong quá trình phát triển. - **Xem trước trên Adapty Dashboard**: Xem trước flow trong khi đang thiết kế. ## Xem trước trên thiết bị \{#preview-on-devices\} Để xem trước flow trên thiết bị thực: 1. [Tải ứng dụng Adapty từ App Store](https://apps.apple.com/us/app/adapty/id6739359219). 2. Trong flow builder, nhấp vào **Test on device**. 3. Chọn ngôn ngữ của flow. 4. Quét mã QR bằng camera thiết bị hoặc mở liên kết. Thao tác này sẽ mở flow của bạn trong ứng dụng di động Adapty. :::note Ở chế độ thử nghiệm, Adapty không thể truy cập sản phẩm của bạn trong các cửa hàng, vì vậy giá hiển thị trong flow không phải là giá thực. ::: ### Khắc phục sự cố \{#troubleshooting\} Bạn không thể xuất bản hoặc xem trước flow nếu có bất kỳ vấn đề nào sau đây. - Một tương tác chưa được cấu hình đầy đủ. Các trường hợp thường gặp: - Hành động **Open URL** chưa có URL đích. - Hành động **Navigate to screen** chưa có màn hình đích — cũng xảy ra khi màn hình đích bị xóa sau khi hành động đã được thiết lập. - Một **hành động có điều kiện** chưa có toán tử hoặc giá trị. - Hành động **Set Variable** chưa có biến/giá trị được gán. - Hành động **Purchase** chưa có sản phẩm (in-app store) hoặc chưa có Web Paywall URL (thanh toán qua web). - Hành động **Custom** chưa có Action ID. - Hành động **Show alert** có Tiêu đề hoặc Nội dung để trống. - Hành động **Show** hoặc **Hide** chưa chọn phần tử nào. - Một **màn hình không có phần tử nào**. - Một phần tử sản phẩm **chưa được gắn sản phẩm** — có thể xảy ra khi bạn xóa sản phẩm đang được tham chiếu. - **Remote config** JSON không hợp lệ sẽ làm gián đoạn toàn bộ quá trình phân phối — bạn thậm chí không thể lưu bản nháp. ## Xem trước trên Adapty Dashboard \{#preview-in-the-adapty-dashboard\} :::tip Để đảm bảo flow sẵn sàng để xuất bản, hãy [xem trước trên thiết bị thực](#preview-on-devices) và xác nhận rằng flow hiển thị mà không có lỗi. ::: Bạn có thể xem trước flow trên các loại màn hình khác nhau bằng khu vực xem trước trong flow builder. Điều này giúp đảm bảo flow của bạn trông đẹp trên nhiều thiết bị và kích thước màn hình khác nhau. Sử dụng các điều khiển xem trước bên dưới khu vực xem trước, bạn có thể: - Chọn thiết bị để xem trước flow. - Chuyển đổi giữa chế độ xem ngang và dọc. - Chuyển đổi giữa chế độ sáng và tối. - Chuyển đổi giữa các ngôn ngữ. :::tip - Luôn xem trước các ngôn ngữ khác nhau, vì các ngôn ngữ khác nhau có thể có độ dài từ khác nhau và bố cục màn hình có thể trông khác nhau. - Để xem trước các biến tùy chỉnh, hãy đặt giá trị khởi tạo cho chúng. Ví dụ: nếu bạn thêm biến `name`, bạn có thể đặt giá trị khởi tạo là `Jane Doe` để xem trước. ::: --- # File: builder-save-publish --- --- title: "Lưu & xuất bản flow" description: "Lưu flow dưới dạng bản nháp và xuất bản cho người dùng" --- [Flow Builder](adapty-flow-builder) tách biệt việc lưu và xuất bản. Bản nháp giữ nguyên công việc của bạn trên Adapty Dashboard, còn xuất bản sẽ đưa phiên bản hiện tại đến người dùng thông qua SDK. Bài viết này hướng dẫn cả hai thao tác và khi nào nên dùng từng cái. ## Lưu flow dưới dạng bản nháp \{#save-a-flow-as-a-draft\} :::warning [Remote Config](customize-flow-with-remote-config) không hợp lệ sẽ ngăn bạn lưu bản nháp. ::: Flow Builder tự động lưu tiến trình của bạn mỗi một phút. Để lưu bản nháp thủ công, nhấp **Save draft** ở góc trên bên phải của Flow Builder, hoặc nhấn **Cmd/Ctrl + S**. Bản nháp chỉ hiển thị trong Dashboard. Chúng không ảnh hưởng đến những gì người dùng thấy trong ứng dụng, kể cả khi flow đã được gán vào một [placement](placements). ## Xuất bản một flow \{#publish-a-flow\} Xuất bản sẽ làm cho phiên bản hiện tại của flow khả dụng với người dùng thông qua SDK. Sau khi xuất bản, phiên bản mới sẽ thay thế phiên bản đã xuất bản trước đó của cùng một flow. :::note Để thêm một flow vào một [placement](placements), hãy xuất bản nó trước. Flow ở trạng thái Draft không thể được thêm vào. ::: Để xuất bản flow của bạn, nhấp vào **Publish to Live** ở góc trên bên phải của Flow Builder. Điều xảy ra tiếp theo phụ thuộc vào việc flow đã được gán cho một placement hay chưa: - **Flow đã có trong một placement**: Người dùng sẽ thấy phiên bản mới vào lần yêu cầu tiếp theo đến placement đó. - **Flow chưa có trong placement nào**: Thêm flow vào một [placement](create-placement) để bắt đầu hiển thị cho người dùng. :::tip Một flow sẵn sàng để xuất bản khi mọi action, màn hình và phần tử sản phẩm đều được cấu hình đầy đủ. Xem [Xử lý sự cố](#troubleshooting) để biết các lỗi thường gặp. ::: :::warning [Font tùy chỉnh](using-custom-fonts-in-flow-builder) không đi kèm với flow — bạn phải thêm từng file font vào bundle của app. Nếu thiếu file, người dùng sẽ thấy font hệ thống thay thế. Để thay đổi font trên một flow đã xuất bản mà không làm hỏng các phiên bản cũ: hãy nhân bản flow, thay đổi font trong bản sao, rồi nhắm bản sao đó đến một [đối tượng](add-audience-paywall-ab-test) trên các phiên bản app đã có font đó. ::: ## Trạng thái flow \{#flow-status\} Mỗi flow hiển thị một trạng thái trong danh sách Flows. Trạng thái phản ánh flow đang ở giai đoạn nào trong vòng đời lưu và xuất bản. | Trạng thái | Ý nghĩa | | :----- | :------ | | **Draft** | Flow chưa bao giờ được xuất bản. Chỉ có bản nháp tồn tại, nên người dùng chưa thấy nó. Bạn cần xuất bản bản nháp trước để thêm vào [placement](placements). | | **Dirty** | Flow đã được xuất bản, nhưng có các chỉnh sửa đã lưu chưa được xuất bản. Người dùng vẫn thấy phiên bản đã xuất bản lần cuối cho đến khi bạn xuất bản lại. | | **Publishing** | Đang trong quá trình xuất bản. | | **Failed** | Lần xuất bản gần nhất đã thất bại. Người dùng vẫn thấy phiên bản đã xuất bản lần cuối, nếu có. | | **Published** | Phiên bản đã lưu mới nhất đang hoạt động. Không có chỉnh sửa nào chưa được xuất bản. | | **Archived** | Flow đã bị xóa. | ## Xử lý sự cố \{#troubleshooting\} Bạn không thể xuất bản hoặc xem trước flow nếu có bất kỳ vấn đề nào sau đây. - Một tương tác chưa được cấu hình đầy đủ. Các trường hợp thường gặp: - Hành động **Open URL** chưa có URL đích. - Hành động **Navigate to screen** chưa có màn hình đích — cũng xảy ra khi màn hình đích bị xóa sau khi hành động đã được thiết lập. - Một **hành động có điều kiện** chưa có toán tử hoặc giá trị. - Hành động **Set Variable** chưa có biến/giá trị được gán. - Hành động **Purchase** chưa có sản phẩm (in-app store) hoặc chưa có Web Paywall URL (thanh toán qua web). - Hành động **Custom** chưa có Action ID. - Hành động **Show alert** có Tiêu đề hoặc Nội dung để trống. - Hành động **Show** hoặc **Hide** chưa chọn phần tử nào. - Một **màn hình không có phần tử nào**. - Một phần tử sản phẩm **chưa được gắn sản phẩm** — có thể xảy ra khi bạn xóa sản phẩm đang được tham chiếu. - **Remote config** JSON không hợp lệ sẽ làm gián đoạn toàn bộ quá trình phân phối — bạn thậm chí không thể lưu bản nháp. Xem trước flow của bạn trong [ứng dụng Adapty](paywall-device-compatibility-preview) để phát hiện các vấn đề trước khi xuất bản. Nếu flow không tải được trong phần xem trước, hãy xem thông báo lỗi để biết thêm chi tiết. --- # File: flow-metrics --- --- title: "Chỉ số flow" description: "Theo dõi và phân tích các chỉ số hiệu suất flow để cải thiện doanh thu gói đăng ký." --- Adapty thu thập một loạt các chỉ số để giúp bạn đo lường hiệu suất của các flow. Khác với chỉ số paywall, chỉ số flow bao gồm theo dõi completion, giúp bạn thấy người dùng rời bỏ ở màn hình nào trong flow. Tất cả các chỉ số được cập nhật theo thời gian thực, ngoại trừ lượt xem được cập nhật vài phút một lần. Tài liệu này mô tả các chỉ số hiện có, định nghĩa và cách tính của chúng. :::important Doanh thu flow được tính từ tất cả các giao dịch xảy ra sau khi flow được hiển thị. ::: Chỉ số flow có thể xem trong danh sách flow, cung cấp tổng quan về hiệu suất của tất cả các flow. Chế độ xem tổng hợp này hiển thị các chỉ số tổng hợp cho từng flow, cho phép bạn so sánh hiệu quả và xác định các điểm cần cải thiện. Để phân tích chi tiết hơn cho từng flow, hãy điều hướng đến chỉ số chi tiết flow. Tại đó bạn sẽ tìm thấy các chỉ số toàn diện dành riêng cho flow được chọn, cung cấp góc nhìn sâu hơn về hiệu suất của flow đó. ## Điều khiển chỉ số \{#metrics-controls\} Hệ thống hiển thị các chỉ số dựa trên khoảng thời gian được chọn và sắp xếp chúng theo tham số cột bên trái với ba mức thụt đầu dòng. Đối với các flow đã xuất bản, chỉ số bao gồm khoảng thời gian từ ngày xuất bản flow đến ngày hiện tại. Các flow nháp và lưu trữ được đưa vào bảng chỉ số, nhưng nếu không có dữ liệu, chúng sẽ hiển thị mà không có chỉ số nào. ### Tùy chọn xem dữ liệu chỉ số \{#view-options-for-metrics-data\} Trang flow cung cấp hai tùy chọn xem dữ liệu chỉ số: - Xem theo placement: Chỉ số được nhóm theo các [placement](placements) liên kết với flow. Dùng chế độ xem này để so sánh hiệu suất của cùng một flow trên các placement khác nhau. - Xem theo đối tượng: Chỉ số được nhóm theo [đối tượng](audience) mục tiêu của flow. Dùng chế độ xem này để đánh giá chỉ số theo từng phân khúc đối tượng khác nhau. Dropdown ở đầu trang flow cho phép bạn chọn chế độ xem ưa thích. ### Lọc chỉ số theo ngày cài đặt \{#filter-metrics-by-install-date\} Hộp kiểm **Filter metrics by install date** cho phép bạn phân tích dữ liệu dựa trên thời điểm người dùng cài đặt ứng dụng, thay vì khi các giao dịch hoặc lượt xem xảy ra. Điều này hữu ích để đo lường hiệu suất thu hút người dùng cho một cohort cụ thể. ### Khoảng thời gian \{#time-ranges\} Bạn có thể phân tích dữ liệu chỉ số theo khoảng thời gian, cho phép tập trung vào các khoảng cụ thể như ngày, tuần, tháng hoặc phạm vi ngày tùy chỉnh. ### Bộ lọc và nhóm \{#filters-and-groups\} Adapty cung cấp các công cụ lọc và tùy chỉnh phân tích chỉ số phù hợp với nhu cầu của bạn. Trang chỉ số cho phép truy cập vào nhiều khoảng thời gian, tùy chọn nhóm và khả năng lọc. - Lọc theo: Attribution (nguồn, nhóm quảng cáo, bộ quảng cáo, sáng tạo, chiến dịch), Quốc gia, Cửa hàng. - Nhóm theo: Flow (mặc định), Quốc gia hoặc Cửa hàng. Các tùy chọn nhóm chỉ xuất hiện trong dropdown khi có dữ liệu cho chiều đó — ví dụ: nếu tất cả lượt xem flow đến từ một quốc gia duy nhất, Quốc gia sẽ không được hiển thị. Bạn có thể tìm thêm thông tin về các điều khiển, bộ lọc, tùy chọn nhóm và cách sử dụng chúng trong [tài liệu này](controls-filters-grouping-compare-proceeds). ### Biểu đồ chỉ số đơn \{#single-metric-chart\} Phần biểu đồ hiển thị dữ liệu của bạn dưới dạng biểu đồ thanh đơn giản. Biểu đồ giúp bạn nhanh chóng xem: - Số liệu chính xác cho từng chỉ số. - Dữ liệu theo từng khoảng thời gian. Tổng số xuất hiện bên cạnh biểu đồ, cung cấp cho bạn bức tranh toàn cảnh chỉ trong một lần nhìn. Nhấp vào biểu tượng mũi tên để mở rộng biểu đồ. ### Tóm tắt tổng chỉ số \{#total-metrics-summary\} Bên cạnh biểu đồ chỉ số đơn là phần tóm tắt tổng chỉ số. Phần này hiển thị các giá trị tích lũy cho các chỉ số được chọn tại một thời điểm cụ thể. Bạn có thể thay đổi chỉ số hiển thị bằng menu dropdown. ## Định nghĩa chỉ số \{#metrics-definitions\} ### Lượt xem & lượt xem duy nhất \{#views--unique-views\} **Lượt xem** đếm số lần người dùng bắt đầu flow của bạn (đến màn hình đầu tiên). Nếu ai đó bắt đầu hai lần, đó là hai lượt xem nhưng chỉ một lượt xem duy nhất. Chỉ số này giúp bạn hiểu flow đã được hiển thị bao nhiêu lần. ### Completion & completion duy nhất \{#completions--unique-completions\} **Completion** đếm số lần người dùng đến màn hình cuối cùng của flow. Nếu ai đó hoàn thành hai lần, đó là hai completion nhưng chỉ một completion duy nhất. ### Tỷ lệ hoàn thành duy nhất \{#unique-completions-rate\} Số lần hoàn thành duy nhất chia cho số lần xem duy nhất. Dùng chỉ số này để hiểu cách người dùng tiến qua flow và xác định điểm họ bỏ dở. :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: ### Doanh thu \{#revenue\} **Doanh thu** hiển thị tổng thu nhập của bạn tính bằng USD từ các giao dịch mua và gia hạn được quy cho flow. Đây là số tiền trước khi trừ bất kỳ khoản nào, bao gồm cả hoa hồng App Store / Play Store. ### Proceeds \{#proceeds\} [**Proceeds**](analytics-cohorts#revenue-vs-proceeds) là khoản bạn nhận được sau khi App Store / Play Store trừ hoa hồng, nhưng trước thuế. :::important Hãy thông báo cho Adapty nếu ứng dụng của bạn đã đăng ký chương trình hoa hồng giảm. Để đảm bảo tính toán chính xác, hãy chỉ định trạng thái [Small Business Program](app-store-small-business-program) và [Reduced Service Fee program](google-reduced-service-fee) trong [cài đặt ứng dụng](general) của bạn. ::: ### Net proceeds \{#net-proceeds\} Thu nhập cuối cùng của bạn sau khi đã trừ cả hoa hồng cửa hàng và thuế. ### ARPPU \{#arppu\} ARPPU là doanh thu trung bình trên mỗi người dùng có trả tiền. Được tính bằng tổng doanh thu chia cho số người dùng trả tiền duy nhất. Ví dụ: $15,000 doanh thu / 1,000 người dùng trả tiền = $15 ARPPU. ### ARPU \{#arpu\} ARPU là doanh thu trung bình trên mỗi người dùng đã xem flow. Được tính bằng tổng doanh thu chia cho số người xem duy nhất. ### ARPAS \{#arpas\} ARPAS là doanh thu trung bình trên mỗi người đăng ký đang hoạt động. Được tính bằng cách chia tổng doanh thu cho số người đăng ký đã kích hoạt dùng thử hoặc gói đăng ký. Ví dụ: $5,000 doanh thu / 1,000 người đăng ký = $5 ARPAS. ### CR purchases & Unique CR purchases \{#cr-purchases--unique-cr-purchases\} **Tỷ lệ chuyển đổi thành giao dịch mua** cho thấy tỷ lệ phần trăm lượt xem flow dẫn đến giao dịch mua. Ví dụ: 10 giao dịch mua từ 100 lượt xem là tỷ lệ chuyển đổi 10%. **Unique CR purchases** đo lường tỷ lệ phần trăm người dùng duy nhất xem flow của bạn và thực hiện giao dịch mua, mỗi người dùng chỉ được tính một lần bất kể họ xem bao nhiêu lần. ### CR trials & Unique CR trials \{#cr-trials--unique-cr-trials\} **Tỷ lệ chuyển đổi thành dùng thử** cho thấy tỷ lệ phần trăm lượt xem flow dẫn đến bắt đầu dùng thử. Ví dụ: 10 lần dùng thử từ 100 lượt xem là tỷ lệ chuyển đổi 10%. **Unique CR trials** đo lường tỷ lệ phần trăm người dùng duy nhất xem flow của bạn và bắt đầu dùng thử, mỗi người dùng chỉ được tính một lần bất kể họ xem bao nhiêu lần. ### Purchases \{#purchases\} **Purchases** đếm tất cả các giao dịch trên flow của bạn, ngoại trừ các lần gia hạn. Bao gồm: - Giao dịch mua trực tiếp mới. - Chuyển đổi từ dùng thử được kích hoạt trên flow. - Thay đổi gói (nâng cấp, hạ cấp, chuyển gói). - Khôi phục gói đăng ký trên flow, chẳng hạn khi gói đăng ký được khôi phục sau khi hết hạn mà không có tự động gia hạn. Chỉ số này cung cấp cho bạn bức tranh đầy đủ về hoạt động giao dịch mới từ flow của bạn. ### Trials \{#trials\} **Trials** đếm số người dùng đã bắt đầu giai đoạn dùng thử miễn phí qua flow của bạn. Dùng chỉ số này để theo dõi mức độ thu hút người dùng của ưu đãi dùng thử trước khi họ quyết định trả tiền. ### Trials cancelled \{#trials-cancelled\} **Trials cancelled** cho biết có bao nhiêu người dùng đã tắt tự động gia hạn trong giai đoạn dùng thử. Điều này cho bạn biết bao nhiêu người quyết định không tiếp tục gói đăng ký có trả phí sau khi dùng thử dịch vụ. ### Refunds \{#refunds\} **Refunds** đếm số giao dịch mua và gói đăng ký đã được hoàn tiền, bất kể lý do. ### Refund rate \{#refund-rate\} **Refund rate** cho thấy tỷ lệ phần trăm giao dịch mua lần đầu được hoàn tiền. Ví dụ: 5 lần hoàn tiền từ 1,000 giao dịch mua lần đầu = tỷ lệ hoàn tiền 0.5%. Các lần gia hạn không được tính trong phép tính này. --- # File: fallback-flows --- --- title: "Flow dự phòng" description: "Thiết lập flow dự phòng cục bộ trong Adapty để giữ cho flow của bạn hiển thị khi thiết bị ngoại tuyến." --- Để duy trì trải nghiệm người dùng mượt mà, bạn cần thiết lập **phiên bản dự phòng** cho các [flow](adapty-flow-builder) của mình. Khi ứng dụng của bạn yêu cầu một flow, Adapty SDK liên hệ với máy chủ của chúng tôi để tải cấu hình. Nếu thiết bị không thể kết nối với Adapty (sự cố mạng, máy chủ gián đoạn), SDK sẽ chuyển sang dùng dữ liệu cục bộ: - Nếu người dùng đã xem flow đó một lần, SDK sẽ phục vụ bản đã lưu trong cache. - Nếu chưa có cache, SDK sẽ tải file cấu hình dự phòng được đóng gói sẵn trong ứng dụng. Adapty tự động tạo các file dự phòng này. Gói dự phòng của flow được dùng chung với paywall — một file JSON duy nhất cho mỗi nền tảng chứa các biến thể dự phòng cho cả hai. SDK đọc phần nào cần thiết tùy theo ngữ cảnh. :::important Flow fallback được tích hợp trong gói **Adapty SDK 4.0+**. Nếu bạn chọn phiên bản SDK cũ hơn trong hộp thoại tải xuống, file sẽ chỉ chứa các biến thể paywall và onboarding — không có flow. Hãy đảm bảo ứng dụng của bạn đang dùng phiên bản SDK hỗ trợ flow trước khi sử dụng flow fallback. ::: ## Trước khi bắt đầu \{#before-you-start\} 1. Tạo một [flow](adapty-flow-builder) trong Flow Builder. 2. [Tạo một placement](create-placement) cho flow đó. ## Tải file dự phòng \{#download-the-fallback-file\} 1. Mở trang **[Placements](https://app.adapty.io/placements)**. 2. Nhấn nút **Fallbacks** ở góc trên bên phải. 3. Chọn nền tảng mục tiêu từ dropdown. 4. Chọn phiên bản SDK khớp với phiên bản bạn đã tích hợp trong app. Chọn **Adapty SDK v4.0.0 and higher** (hoặc tùy chọn mới hơn) để nhận bundle bao gồm cả flow. Trình duyệt sẽ tải xuống một file JSON cho mỗi nền tảng — ví dụ: `ios_4_0_0_fallback.json`. <details> <summary>Ví dụ về mục fallback flow (nhấn để mở rộng)</summary> ```json "PLACEMENT_ID": { "data": [ { "developer_id": "PLACEMENT_ID", "variation_id": "cb1c0ef8-aecd-4a53-a6f3-b98266e66884", "flow_id": "daf25858-3fa2-4981-8500-9c8a30e5b7e6", "flow_name": "FLOW_NAME", "flow_version_id": "FLOW_VERSION_ID", "placement_audience_version_id": "a9eb3ab8-3178-477d-84d4-ef9d3978e48b", "audience_name": "All Users", "ab_test_name": "", "cross_placement_info": null, "weight": 100, "variations": [ { "variation_id": "cb1c0ef8-aecd-4a53-a6f3-b98266e66884", "paywall_id": "PAYWALL_ID", "paywall_name": "PAYWALL_NAME", "ab_test_name": "", "products": [], "revision": 1, "custom_payload": null, "weight": 100 } ], "remote_configs": [] } ], "meta": { "placement": { "developer_id": "PLACEMENT_ID", "is_tracking_purchases": true, "audience_name": "All Users", "placement_audience_version_id": "a9eb3ab8-3178-477d-84d4-ef9d3978e48b", "revision": 0, "ab_test_name": "" } } } ``` Hình dạng chính xác có thể thay đổi giữa các phiên bản SDK. Hãy luôn dùng file mà Adapty đã tạo cho phiên bản SDK của bạn thay vì tự viết thủ công. </details> ## Sau khi tải xuống \{#after-the-download\} Thêm file vào code ứng dụng của bạn, sau đó làm theo hướng dẫn cài đặt theo từng nền tảng. Các API dùng để tải paywall dự phòng cũng sẽ tải flow dự phòng khi ứng dụng của bạn sử dụng phiên bản SDK hỗ trợ flow: - [iOS](ios-use-fallback-paywalls) - [Android](android-use-fallback-paywalls) - [React Native](react-native-use-fallback-paywalls) - [Capacitor](capacitor-use-fallback-paywalls) ## Hạn chế \{#limitations\} Các flow dự phòng được mã hóa cứng và lưu trữ cục bộ, do đó chúng không mang đầy đủ khả năng động của các flow trực tiếp: - **Một biến thể cho mỗi placement.** Nếu một placement có nhiều hơn một flow (các đối tượng khác nhau, các biến thể A/B test), file dự phòng sẽ sử dụng biến thể có trọng số cao nhất, hoặc đối tượng rộng nhất. - **Không có A/B testing.** Một A/B test flow đang chạy sẽ được xử lý trên server; fallback luôn phục vụ một biến thể duy nhất đã được chọn. - **Không có cập nhật từ xa.** Việc cập nhật fallback yêu cầu phải phát hành ứng dụng mới. Đối với các cập nhật runtime mà bạn thường thực hiện qua remote config, hãy đẩy chúng qua flow trực tiếp thay thế. - **Chỉ có locale mặc định.** Fallback sử dụng locale `en`; các biến thể đã được bản địa hóa không được đóng gói kèm theo. --- # File: create-product --- --- title: "Tạo sản phẩm" description: "Hướng dẫn từng bước về cách tạo sản phẩm gói đăng ký mới trong Adapty để quản lý doanh thu hiệu quả hơn." --- Cách bạn tạo sản phẩm trong Adapty phụ thuộc vào việc bạn đã có sản phẩm trong các cửa hàng hay chưa: - **[Nếu sản phẩm chưa tồn tại trên App Store và/hoặc Google Play, hãy tạo chúng trong Adapty và đẩy lên cửa hàng ngay lập tức](#create-product-and-push-to-store)**. - **[Nếu sản phẩm đã tồn tại trên App Store và/hoặc Google Play, hãy tạo chúng trong Adapty và kết nối với sản phẩm đã có trong cửa hàng.](#create-product-and-connect-existing-store-products)** :::tip Bạn cũng có thể tạo sản phẩm theo cách lập trình bằng [Developer CLI](developer-cli-reference#adapty-products-create). ::: ## Tạo sản phẩm và đẩy lên cửa hàng \{#create-product-and-push-to-store\} :::warning Trước khi bắt đầu, hãy đảm bảo bạn đã cấu hình tích hợp với các cửa hàng cần thiết: - [App Store](initial_ios) - [Google Play](initial-android) Nếu bạn đã cấu hình tích hợp App Store từ trước, hãy đảm bảo bạn đã [thêm App Store Connect API key](app-store-connection-configuration#step-6-add-app-store-connect-api-key). ::: <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/qUpC2XG-r5E?si=7Komyv4_PUQ4FaEH" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Để thêm sản phẩm mới vào ứng dụng: 1. Vào **[Products](https://app.adapty.io/products)** từ menu chính của Adapty. <img src="/assets/shared/img/products-tab.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp **Create product** ở góc trên bên phải. Adapty hỗ trợ tất cả các loại sản phẩm: gói đăng ký, non-consumable \(bao gồm cả lifetime\) và consumable. 3. Chọn **Create a new product and push to stores**. <img src="/assets/shared/img/push-to-stores.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhập các thông tin sau: - **Product name**: nhập tên sản phẩm để sử dụng trong Adapty dashboard. Tên này chủ yếu để bạn tham khảo, vì vậy hãy chọn tên phù hợp nhất với bạn khi sử dụng trên Adapty Dashboard. - **Access Level**: chọn [mức độ truy cập](access-level) mà sản phẩm thuộc về. Mức độ truy cập dùng để xác định các tính năng được mở khóa sau khi mua sản phẩm. Lưu ý danh sách này chỉ chứa các mức độ truy cập đã được tạo trước đó. Mức độ truy cập `premium` được tạo sẵn trong Adapty, nhưng bạn cũng có thể [thêm mức độ truy cập khác](access-level). - **Subscription duration**: chọn thời hạn gói đăng ký từ danh sách. - **Weekly/Monthly/2 Months/3 Months/6 Months/Annual**: Thời hạn gói đăng ký. - **Lifetime**: Dùng khi sản phẩm mở khóa tính năng premium của ứng dụng mãi mãi. - **Non-Subscriptions**: Dành cho các sản phẩm không phải gói đăng ký và do đó không có thời hạn. Có thể dùng để mở khóa tính năng bổ sung, consumable, v.v. - **Consumables**: Các mục consumable có thể mua nhiều lần và có thể được tiêu thụ trong quá trình sử dụng ứng dụng. Ví dụ như tiền tệ trong game và các vật phẩm thêm. Lưu ý rằng sản phẩm consumable không ảnh hưởng đến mức độ truy cập. Để cấp mức độ truy cập từ sản phẩm mua một lần, hãy dùng **Non-Subscriptions**. - **Price (USD)**: Giá sản phẩm tính bằng USD. Giá này sẽ được dùng làm cơ sở để tự động tính và thiết lập giá cho tất cả các quốc gia. Bạn có thể [tùy chỉnh giá cho từng quốc gia và khu vực](edit-product#set-country-specific-prices) sau. <img src="/assets/shared/img/create-product-push.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp **Save & Continue**. 6. Cấu hình thông tin sản phẩm cho App Store nếu bạn có kế hoạch xuất bản ở đó: - **Product ID**: Tạo ID duy nhất và cố định cho sản phẩm. - **Product group**: Chọn nhóm sản phẩm hiện có mà bạn đã tạo trong App Store Connect hoặc nhấp **Create new Product Group** và đặt tên cho nhóm đó. Sau khi Adapty tạo xong, bạn có thể chọn từ danh sách thả xuống. - **Screenshot**: Tải lên ảnh chụp màn hình của in-app purchase thể hiện rõ sản phẩm hoặc dịch vụ được cung cấp. Ảnh chụp màn hình này chỉ dùng cho quá trình xét duyệt App Store và không được hiển thị trên App Store. Xem yêu cầu về kích thước và định dạng ảnh chụp màn hình [tại đây](https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/). <img src="/assets/shared/img/push-app-store.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Nhấp **Push data to App Store**. :::warning Nếu đây là sản phẩm đầu tiên của ứng dụng này, bạn phải gửi thủ công để xét duyệt trong App Store Connect. Điều này sẽ không cần thiết về sau. Sau khi xét duyệt hoàn tất, trạng thái sản phẩm trong Adapty sẽ tự động cập nhật. ::: 8. Cấu hình thông tin sản phẩm cho Google Play nếu bạn có kế hoạch xuất bản ở đó: - **Base Product ID**: Tạo ID duy nhất và cố định cho sản phẩm. - **Subscription**: Chọn nhóm gói đăng ký hiện có mà bạn đã tạo trong Google Play Console hoặc nhấp **Create new Product Group** và đặt tên và ID cho nhóm đó. Sau khi Adapty tạo xong, bạn có thể chọn từ danh sách thả xuống. :::note Grace Period và Account Hold Period sẽ được tự động đặt về giá trị mặc định theo quy định của Play Store. Bạn có thể thay đổi chúng sau trong Google Play Console. ::: <img src="/assets/shared/img/push-google-play.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 9. Nhấp **Push data to Play Store**. 10. Đối với iOS, cấu hình ưu đãi giới thiệu – dùng thử miễn phí – bằng cách chọn **Free duration** từ danh sách thả xuống. Trong thiết lập ban đầu này, bạn có thể thêm thời gian dùng thử miễn phí giới thiệu. Sau khi sản phẩm chính được các cửa hàng phê duyệt, bạn có thể [thêm các ưu đãi khác](offers) (ví dụ: ưu đãi, ưu đãi thu hút khách hàng cũ) bằng cách liên kết ID hiện có từ console của cửa hàng. <img src="/assets/shared/img/intro.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important Ưu đãi giới thiệu không tự đồng bộ với Google Play. Không giống App Store, Google Play không có loại "introductory offer" riêng biệt — các ưu đãi dùng thử miễn phí và giảm giá đều được cấu hình dưới dạng **offers** trên base plan. [Tạo offer trong Google Play Console và liên kết với sản phẩm Adapty của bạn](google-play-offers). ::: 11. Cuối cùng, nhấp **Save** để xác nhận tạo sản phẩm. ## Tạo sản phẩm và kết nối sản phẩm đã có trong cửa hàng \{#create-product-and-connect-existing-store-products\} :::warning Trước khi bắt đầu, hãy đảm bảo bạn đã: - Cấu hình tích hợp với các cửa hàng cần thiết: - [App Store](initial_ios) - [Google Play](initial-android) - Tạo sản phẩm trong các cửa hàng cần thiết: - [App Store](app-store-products) - [Google Play](android-products) **Nếu bạn chưa có sản phẩm nào**, hãy xem hướng dẫn [Đẩy lên cửa hàng](#create-product-and-push-to-store) để tạo chúng cùng lúc trong Adapty và các cửa hàng. ::: <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/nlkdKCF0SwY?si=VVigzHcpv3waKJmI" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> Để thêm sản phẩm mới vào ứng dụng: 1. Vào **[Products](https://app.adapty.io/products)** từ menu chính của Adapty. <img src="/assets/shared/img/products-tab.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp **Create product** ở góc trên bên phải. Adapty hỗ trợ tất cả các loại sản phẩm: gói đăng ký, non-consumable \(bao gồm cả lifetime\) và consumable. 3. Chọn **Connect an existing store product**. <img src="/assets/shared/img/existing-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhập các thông tin sau: - **Product name**: nhập tên sản phẩm để sử dụng trong Adapty dashboard. Tên này chủ yếu để bạn tham khảo, vì vậy hãy chọn tên phù hợp nhất với bạn khi sử dụng trên Adapty Dashboard. - **Access Level ID**: chọn [mức độ truy cập](access-level) mà sản phẩm thuộc về. Mức độ truy cập dùng để xác định các tính năng được mở khóa sau khi mua sản phẩm. Lưu ý danh sách này chỉ chứa các mức độ truy cập đã được tạo trước đó. Mức độ truy cập `premium` được tạo sẵn trong Adapty, nhưng bạn cũng có thể [thêm mức độ truy cập khác](access-level). - **Subscription duration**: chọn thời hạn gói đăng ký từ danh sách. - **Weekly/Monthly/2 Months/3 Months/6 Months/Annual**: Thời hạn gói đăng ký. - **Lifetime**: Dùng khi sản phẩm mở khóa tính năng premium của ứng dụng mãi mãi. - **Non-Subscriptions**: Dành cho các sản phẩm không phải gói đăng ký và do đó không có thời hạn. Có thể dùng để mở khóa tính năng bổ sung, consumable, v.v. - **Consumables**: Các mục consumable có thể mua nhiều lần và có thể được tiêu thụ trong quá trình sử dụng ứng dụng. Ví dụ như tiền tệ trong game và các vật phẩm thêm. Lưu ý rằng sản phẩm consumable không ảnh hưởng đến mức độ truy cập. Để cấp mức độ truy cập từ sản phẩm mua một lần, hãy dùng **Non-Subscriptions**. - **Price (USD)**: Giá sản phẩm tính bằng USD. Nếu sản phẩm đã có trong cửa hàng, giá trị này sẽ không ảnh hưởng đến giá thực tế trong cửa hàng; bạn có thể chọn bất kỳ giá trị nào từ danh sách. Sau đó, bạn có thể [tùy chỉnh giá cho từng khu vực](edit-product#set-country-specific-prices) ngay trong Adapty dashboard. <img src="/assets/shared/img/product-info.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp **Continue**. 6. Cấu hình thông tin sản phẩm từ mỗi cửa hàng: - **App Store:** - **App Store Product ID:** Mã định danh duy nhất này được dùng để truy cập sản phẩm trên thiết bị. Chọn từ danh sách. Nếu không thấy trong danh sách, hãy kiểm tra cấu hình trong App Store Connect và đảm bảo nó chính xác và thuộc ứng dụng này. - **Play Store:** - **Google Play Product ID:** Đây là mã định danh sản phẩm từ Play Store. Chọn từ danh sách. Nếu không thấy trong danh sách, hãy kiểm tra cấu hình trong Google Play Console và đảm bảo nó chính xác và thuộc ứng dụng này. - **Base Plan ID:** ID này dùng để xác định base plan cho sản phẩm trên Play Store. Khi thêm Product ID của gói đăng ký trên Play Store, bạn cần cung cấp Base Plan ID. Một base plan xác định các chi tiết cơ bản của gói đăng ký, bao gồm chu kỳ thanh toán, loại gia hạn (tự động gia hạn hoặc trả trước) và giá tương ứng. Lưu ý rằng trong Adapty, mỗi sự kết hợp giữa cùng một gói đăng ký và các base plan khác nhau được coi là sản phẩm riêng biệt. - **Legacy fallback product**: Sản phẩm dự phòng này chỉ được dùng cho các ứng dụng sử dụng phiên bản SDK Adapty cũ hơn (phiên bản 2.5 trở xuống). Bằng cách đánh dấu sản phẩm là tương thích ngược trong Google Play Console, Adapty có thể xác định liệu nó có thể được mua bởi các phiên bản SDK cũ hay không. Đối với trường này, hãy chỉ định giá trị theo định dạng `<subscription_id>:<base_plan_id>`. - **Stripe**: - **Stripe Product ID**: Đây là mã định danh duy nhất cho sản phẩm trong Stripe. - **Stripe Price ID**: Trong Stripe, các đối tượng price bao gồm nhiều hơn chỉ là số tiền; chúng còn bao gồm cả hành vi thuế, bậc khối lượng và khoảng thời gian gói đăng ký. Vì một sản phẩm có thể có nhiều mức giá, hãy chỉ định đúng price ID khi tạo sản phẩm trong Adapty. - **Paddle**: - **Paddle Product ID**: Đây là mã định danh duy nhất cho sản phẩm trong Paddle. - **Paddle Price ID**: Trong Paddle, các đối tượng price bao gồm nhiều hơn chỉ là số tiền; chúng còn bao gồm cả hành vi thuế, bậc khối lượng và khoảng thời gian gói đăng ký. Vì một sản phẩm có thể có nhiều mức giá, hãy chỉ định đúng price ID khi tạo sản phẩm trong Adapty. 7. **Tùy chọn:** Bạn có thể thêm sản phẩm từ bất kỳ cửa hàng tùy chỉnh nào bằng cách nhấp **Add custom store**. Trong cửa sổ **Manage custom store info**, bạn có thể chọn cửa hàng tùy chỉnh hiện có hoặc thêm cửa hàng mới và liên kết sản phẩm với nó. Lưu ý rằng Adapty chỉ theo dõi giao dịch từ App Store, Google Play và Stripe. Đối với các cửa hàng tùy chỉnh, bạn cần gửi giao dịch bằng phương thức Set transaction của Adapty server-side API. 8. Nhấp **Save product** để hoàn tất việc tạo sản phẩm. Quá trình đồng bộ trạng thái sản phẩm có thể mất đến năm phút, vì vậy hãy đợi cho đến khi chúng cập nhật trong bảng. 9. Bạn có thể [tạo offer](create-offer) cho sản phẩm nếu cần. Để thêm offer, nhấp **Yes, add offers**. Nếu không, nhấp **No, thanks**. :::note Ưu đãi giới thiệu chỉ được tạo trong Adapty khi đẩy sản phẩm lên cửa hàng. Khi nhập hoặc với các sản phẩm đã tạo trước đó, ưu đãi giới thiệu không được đồng bộ và không hiển thị trong Adapty nhưng vẫn hoạt động đúng trong ứng dụng. ::: ## Các bước tiếp theo \{#next-steps\} Chúc mừng! Bạn đã thêm sản phẩm vào Adapty. Tiếp theo là gì? - Nếu bạn chưa cấu hình ưu đãi giới thiệu/ưu đãi, bạn có thể [thực hiện ngay](offers). - Nếu bạn không muốn làm việc đó hoặc đã làm xong, hãy tiến hành [thiết lập paywall](quickstart-paywalls) để bật in-app purchase. - Nếu bạn muốn điều chỉnh sản phẩm trong cửa hàng (ví dụ: đặt giá theo khu vực hoặc cấu hình thời gian ân hạn), hãy thực hiện trong App Store Connect hoặc Google Play Console. - Đọc thêm về cách [chỉnh sửa sản phẩm](edit-product) sau này. --- # File: edit-product --- --- title: "Chỉnh sửa sản phẩm" description: "Sửa đổi và quản lý các sản phẩm đăng ký của bạn trong Adapty để theo dõi doanh thu tốt hơn." --- Trong Adapty, bạn có thể chỉnh sửa tên sản phẩm, mức độ truy cập, giá theo khu vực, và các ID cửa hàng được liên kết, đồng thời xem nhật ký kiểm tra để theo dõi các thay đổi về giá. Thời hạn gói đăng ký không thể chỉnh sửa sau khi tạo sản phẩm, vì vậy bạn cần tạo sản phẩm mới nếu muốn thay đổi. :::warning Mặc dù bạn có thể chỉnh sửa bất kỳ sản phẩm nào, nhưng cần đảm bảo rằng việc thay đổi các sản phẩm đang được dùng trong paywall thực tế không gây ra sai lệch trong phân tích dữ liệu của bạn. **Không nên chỉnh sửa mức độ truy cập, App Store Product ID và Play Store Product ID** vì điều này có thể ảnh hưởng đến độ chính xác của analytics. Chỉ chỉnh sửa khi bạn mắc lỗi, chẳng hạn như nhập sai ID sản phẩm. Nếu bạn không còn sử dụng sản phẩm đó và muốn thay thế bằng sản phẩm khác, chúng tôi khuyên bạn nên tạo sản phẩm mới và cập nhật Paywalls cùng A/B tests cho phù hợp. ::: ## Chỉnh sửa sản phẩm \{#edit-product\} Để chỉnh sửa sản phẩm: 1. Vào **[Products](https://app.adapty.io/products)** từ menu chính của Adapty. 2. Nhấp vào hàng sản phẩm trong bảng hoặc nhấp vào biểu tượng ba chấm bên cạnh sản phẩm và chọn **Edit**. 3. Trong cửa sổ **Edit** vừa mở, thực hiện các thay đổi cần thiết. Để biết thêm chi tiết về các tùy chọn trong cửa sổ này, vui lòng đọc phần [Tạo sản phẩm](create-product). 4. Nhấp **Save**. :::warning Các thay đổi bạn thực hiện trong App Store Connect hoặc Google Play Console sẽ không được đồng bộ ngược lại với Adapty. Giá hiển thị trong Adapty được thiết lập khi bạn tạo sản phẩm và sẽ không cập nhật nếu bạn thay đổi giá trong cửa hàng. Điều này không ảnh hưởng đến analytics doanh thu của bạn — Adapty lấy dữ liệu doanh thu trực tiếp từ các cửa hàng. Trường giá trên dashboard chỉ để bạn tham khảo. ::: :::note Nếu bạn thay đổi mức độ truy cập, thay đổi chỉ áp dụng cho các gói đăng ký mới. Đối với người dùng đang đăng ký, mức độ truy cập hiện tại vẫn giữ nguyên và sẽ tự động cập nhật vào lần gia hạn gói đăng ký tiếp theo. ::: <img src={require('./img/edit-product.png').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Thiết lập giá theo từng quốc gia \{#set-country-specific-prices\} Bạn có thể thiết lập các mức giá khác nhau cho từng khu vực ngay trong Adapty Dashboard, và các mức giá theo quốc gia này sẽ tự động được áp dụng cho sản phẩm của bạn trong App Store Connect và/hoặc Google Play Console. Để thiết lập giá theo từng quốc gia: 1. [Mở sản phẩm để chỉnh sửa](#edit-product). 2. Nhấp **Download** để xuất giá hiện tại từ các cửa hàng theo đúng định dạng hoặc tạo một file CSV mới. <img src={require('./img/download-prices.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Cập nhật giá trong file CSV. Tuân theo [định dạng](#csv-file-format). Nếu bạn để nguyên giá của một quốc gia hoặc không đưa vào file, sẽ không có thay đổi nào xảy ra. Khi bạn tải lên CSV, Adapty sẽ so sánh giá và chỉ cập nhật những giá có sự khác biệt. 4. Trong cửa sổ **Edit**, nhấp **Upload** và chọn file CSV. <img src={require('./img/upload-prices.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nếu bạn muốn các thay đổi cũng áp dụng cho người dùng đang đăng ký hiện tại, hãy chọn **Apply to existing subscribers**. 6. Xem lại các thay đổi sẽ được áp dụng và nhấp **Save changes**. <img src={require('./img/country-level-price.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Định dạng file CSV \{#csv-file-format\} :::tip Bạn có thể tái sử dụng cùng một file CSV nếu bạn có các sản phẩm tương tự trong một ứng dụng hoặc muốn thiết lập cùng mức giá cho các ứng dụng khác nhau. ::: Cách dễ nhất để chỉnh sửa giá trong CSV là [tải xuống file với giá hiện tại và chỉnh sửa trực tiếp](#set-country-specific-prices). Tuy nhiên, nếu bạn tự tạo file, file phải chứa các cột sau: - `region_name` - `region_code` - `app_store_currency` - `app_store_requested_price` - `play_store_currency` - `play_store_requested_price` Ví dụ: ``` region_name,region_code,app_store_currency,app_store_requested_price,play_store_currency,play_store_requested_price United States,US,,8.99,,8.99 United Arab Emirates,AE,USD,8.99,AED,39.99 Germany,DE,USD,8.99,USD,8.99 ``` ## Xem nhật ký kiểm tra \{#view-audit-log\} Adapty ghi lại tất cả các thay đổi về giá cho từng sản phẩm, giúp bạn theo dõi ai đã thực hiện thay đổi và khi nào. Để xem nhật ký kiểm tra: 1. Vào **[Products](https://app.adapty.io/products)** từ menu chính của Adapty. 2. Nhấp vào biểu tượng ba chấm bên cạnh sản phẩm và chọn **Audit log**. Bảng nhật ký kiểm tra hiển thị từng thay đổi về giá kèm theo ngày, tên và vai trò của thành viên trong nhóm, cùng số lượng thay đổi. Để tải xuống bản chi tiết CSV của một sự kiện, nhấp vào biểu tượng tải xuống ở hàng tương ứng. <img src={require('./img/audit-log.webp').default} style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: delete-product --- --- title: "Xóa sản phẩm" description: "Tìm hiểu cách xóa sản phẩm gói đăng ký trong Adapty mà không làm gián đoạn doanh thu của ứng dụng." --- Bạn chỉ có thể xóa các sản phẩm không được sử dụng trong bất kỳ paywall nào. Để xóa sản phẩm: 1. Vào **[Products](https://app.adapty.io/products)** từ menu chính của Adapty. 2. Nhấn nút **3-dot** bên cạnh sản phẩm và chọn **Delete**. <img src="/assets/shared/img/delete-product.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhập tên sản phẩm bạn muốn xóa. <img src="/assets/shared/img/b945add-delete_product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấn **Delete forever**. --- # File: add-product-to-paywall --- --- title: "Thêm sản phẩm vào paywall" description: "Tìm hiểu cách thêm và quản lý sản phẩm trên các paywall trong Adapty." --- Để sản phẩm hiển thị và có thể chọn trong [paywall](paywalls) cho người dùng ứng dụng, hãy thực hiện các bước sau: 1. Trong khi [cấu hình paywall](create-paywall), nhấp vào **Add product** bên dưới tiêu đề **Products**. 2. Từ danh sách thả xuống, chọn các sản phẩm sẽ hiển thị cho khách hàng. Danh sách chỉ chứa các sản phẩm đã được tạo trước đó. Thứ tự sản phẩm được giữ nguyên ở phía SDK, vì vậy cần cân nhắc thứ tự mong muốn khi cấu hình paywall. Ngoài ra, bạn có thể chỉ định ưu đãi cho sản phẩm nếu muốn. <img src="/assets/shared/img/0479b51-ad_product_to_paywall.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp vào **Create as draft** hoặc **Save and publish** tùy theo trạng thái của paywall. Lưu ý rằng sau khi tạo, không nên chỉnh sửa, thêm hoặc xóa sản phẩm khỏi paywall vì điều này có thể ảnh hưởng đến các chỉ số của paywall. --- # File: virtual-currencies --- --- title: "Tiền tệ ảo" description: "Xác định các loại tiền tệ trong ứng dụng trong Adapty, liên kết chúng với sản phẩm để tự động cấp tín dụng và theo dõi số dư cho từng người dùng." --- <CustomDocCardList ids={['virtual-currency-quickstart', 'create-virtual-currency', 'virtual-currency-balance']} /> Cấp tiền tệ ảo cho người dùng — AI token, credit, hoặc coin — khi họ mua sản phẩm hoặc gia hạn gói đăng ký. Định nghĩa một loại tiền tệ một lần, liên kết với các sản phẩm cần trao thưởng, và Adapty sẽ tự động cộng số dư cho từng người dùng. Ứng dụng của bạn đọc và tiêu số dư đó thông qua [server-side API](getting-started-with-server-side-api) — ví dụ, để trừ token cho mỗi lần tạo nội dung. ## Cách hoạt động \{#how-it-works\} 1. [Tạo tiền tệ ảo](create-virtual-currency): Mở **Products** và nhấp vào tab **Virtual currency**. Nhấp **New virtual currency**. Định nghĩa một loại tiền tệ với mã, tên, và mô tả tùy chọn. 2. **Liên kết tiền tệ với sản phẩm**: Ánh xạ tiền tệ đến các sản phẩm mua một lần hoặc gói đăng ký và đặt số credits mà mỗi lần mua sẽ cấp. 3. **Credits được cấp tự động**: Khi người dùng mua một sản phẩm mua một lần đã liên kết hoặc gia hạn gói đăng ký đã liên kết, Adapty sẽ tự động thêm số credits đã cấu hình vào số dư của họ. 4. **Đọc và tiêu số dư**: Ứng dụng của bạn đọc số dư của từng người dùng và cấp hoặc tiêu credits thông qua API phía máy chủ. 5. **Theo dõi mọi thay đổi**: Mỗi thay đổi số dư đều được liệt kê trên [hồ sơ người dùng](virtual-currency-balance). Để xem hướng dẫn đầy đủ — từ thiết lập tiền tệ đến sử dụng tín dụng qua API — hãy xem [Hướng dẫn nhanh về tiền tệ ảo](virtual-currency-quickstart). ## Các trường hợp sử dụng \{#use-cases\} | Loại ứng dụng | Cách sử dụng tiền tệ ảo | |----------|-------------------------------| | **Ứng dụng AI** (tạo ảnh, video hoặc văn bản) | Tính credit cho mỗi lần tạo. Bao gồm hạn mức credit hàng tháng trong gói đăng ký và bán gói credit dưới dạng sản phẩm mua một lần. | | **Ứng dụng phim ngắn và video** | Tính coin để mở khóa tập phim. Cấp cho người dùng đăng ký một lượng coin định kỳ và bán gói coin dưới dạng sản phẩm mua một lần. | | **Học ngôn ngữ và giáo dục** | Cấp cho người dùng miễn phí một hạn mức tim hoặc mạng sống giới hạn thông qua API phía máy chủ. Cấp hạn mức lớn hơn hoặc bỏ qua hoàn toàn việc kiểm tra số dư với gói trả phí. | | **Game mobile** | Vận hành song song hai loại tiền tệ — mềm và cứng: cấp vàng cho gameplay thông qua API phía máy chủ, bán đá quý lấy tiền thật và cho phép chuyển đổi qua lại trong một giao dịch nguyên tử duy nhất. | | **Ứng dụng năng suất** | Đo lường các tác vụ tốn kém như OCR hoặc xuất file bằng credit. Cấp cho người dùng miễn phí một hạn mức nhỏ, tăng thêm với gói đăng ký và bán gói credit cho công việc theo lô. | :::tip Đối với các allowance theo gói đăng ký, hãy bật [hết hạn tín dụng](create-virtual-currency#link-products). Khi tín dụng chưa dùng được đặt lại sau mỗi lần gia hạn, allowance vẫn là lý do để giữ gói đăng ký, và người dùng nhiều sẽ mua thêm gói tín dụng thay vì tiêu hết lượng tích lũy. ::: ## Hạn chế \{#limitations\} - **Truy cập đa thiết bị yêu cầu định danh**: Số dư thuộc về một hồ sơ người dùng. Người dùng ẩn danh chỉ giữ số dư trên thiết bị nơi họ kiếm được, vì vậy hãy định danh người dùng để số dư của họ khả dụng trên mọi thiết bị. Xem [Số dư, hồ sơ người dùng và thiết bị](virtual-currency-balance#balances-profiles-and-devices). - **Chỉ phía server**: Hiện chưa có phương thức SDK nào để đọc hoặc tiêu số dư, nên ứng dụng của bạn cần có backend. Backend sẽ đọc số dư và cấp hoặc tiêu credits thông qua server-side API. - **Tối đa 20 loại tiền tệ mỗi ứng dụng**: Bạn có thể tạo tối đa 20 loại tiền tệ ảo trong một ứng dụng. --- # File: virtual-currency-quickstart --- --- title: "Hướng dẫn nhanh về tiền tệ ảo" description: "Thiết lập tiền tệ ảo từ đầu đến cuối: tạo tiền tệ token, cấp token kèm theo gói đăng ký, và tiêu token qua API phía máy chủ." --- :::link Bài viết chính: [Tiền tệ ảo](virtual-currencies) ::: Hướng dẫn nhanh này thiết lập một tiền tệ token từ đầu đến cuối: một gói đăng ký cấp cho người dùng 1000 token mỗi tháng, và các hành động có trả phí trong ứng dụng sẽ tốn token. Adapty quản lý toàn bộ số dư, vì vậy backend của bạn chỉ cần đọc và tiêu token. 1. Làm theo hướng dẫn [Tạo tiền tệ ảo](create-virtual-currency) để tạo loại tiền tệ `TOKENS`. Liên kết nó với gói đăng ký Pro và đặt **Credit per cycle** thành 1000. Để bán thêm token, hãy liên kết thêm các sản phẩm gói token mua một lần. 2. Gọi [Lấy danh sách số dư tiền tệ ảo](api-adapty/operations/listVirtualCurrencyBalances) để đọc số dư của người dùng, ví dụ trước khi bắt đầu một lần tạo nội dung: ```bash title="Read balances" curl https://api.adapty.io/api/v2/server-side-api/vc/balances/ \ -H "Authorization: Api-Key {your secret key}" \ -H "adapty-customer-user-id: user-42" ``` Phản hồi liệt kê tất cả các loại tiền tệ mà người dùng có, ví dụ 1000 `TOKENS`: ```json title="Response" { "data": [ { "code": "TOKENS", "name": "Tokens", "balance": 1000, "held": 0, "available": 1000 } ] } ``` 3. Khi người dùng chạy một lần tạo ảnh, hãy trừ token bằng cách gọi [Create virtual currency transaction](api-adapty/operations/createVirtualCurrencyTransaction) với `amount` âm. Trong ví dụ này, một lần tạo ảnh tốn 100 token: ```bash title="Spend tokens" curl -X POST https://api.adapty.io/api/v2/server-side-api/vc/transactions/ \ -H "Authorization: Api-Key {your secret key}" \ -H "adapty-customer-user-id: user-42" \ -H "Content-Type: application/json" \ -d '{"items": [{"currency_code": "TOKENS", "amount": -100}]}' ``` Giao dịch mang tính nguyên tử và trả về số dư đã cập nhật. Nếu người dùng không đủ số dư để thanh toán, yêu cầu sẽ trả về `insufficient_balance` và không có gì thay đổi. Hãy thêm header `Idempotency-Key` để retry yêu cầu một cách an toàn. 4. Để chạy promo ưu đãi thu hút khách hàng cũ, cấp token thông qua cùng một endpoint với `amount` dương. Các khoản tín dụng bạn cấp theo cách này không bao giờ hết hạn. 5. Xem lại mọi thay đổi trong [hồ sơ người dùng](virtual-currency-balance) hoặc trong [lịch sử giao dịch](api-adapty/operations/listVirtualCurrencyTransactions), để bạn có thể kiểm toán kinh tế ảo bất kỳ lúc nào. --- # File: create-virtual-currency --- --- title: "Tạo tiền tệ ảo" description: "Tạo tiền tệ ảo trong Adapty, liên kết với sản phẩm để mỗi lần mua hàng sẽ cấp credits, và thiết lập thời hạn sử dụng của credits." --- :::link Bài viết chính: [Tiền tệ ảo](virtual-currencies) ::: Để bán credits tiền tệ ảo, bạn chỉ cần định nghĩa một loại tiền tệ ảo, sau đó liên kết với các sản phẩm của mình. Quá trình này gồm hai bước — cả hai đều được mô tả trong bài viết này. Khi đã liên kết sản phẩm, mỗi lần mua hàng hoặc gia hạn sẽ tự động cấp credits. ## Tạo tiền tệ ảo \{#create-a-virtual-currency\} Trong Adapty Dashboard, mở **Products** > [**Virtual currency**](https://app.adapty.io/virtual-currency). Để tạo tiền tệ ảo: 1. Nhấn **New virtual currency**. Thanh trượt sẽ mở ra ở bước **General**. 2. Điền thông tin chi tiết về đồng tiền: - **Code**: Mã định danh **cố định**, duy nhất cho đồng tiền trong ứng dụng của bạn, ví dụ như `COINS`. Dùng để xác định đồng tiền trong API phía máy chủ và theo dõi số dư của từng người dùng. Dùng chữ cái Latin, chữ số và dấu gạch dưới, tối đa 32 ký tự. Chữ thường sẽ tự động được chuyển thành chữ hoa. - **Name**: Tên hiển thị, ví dụ `Gold`. Bạn có thể thay đổi sau. - **Description**: Ghi chú tùy chọn về loại tiền tệ. 3. Nhấn **Continue** để chuyển sang bước **Link Products**. :::important Bạn không thể thay đổi **Code** của loại tiền tệ sau khi đã tạo. Tuy nhiên, bạn vẫn có thể chỉnh sửa **Name** sau đó. ::: Khi bạn thêm một loại tiền tệ mới, số dư ban đầu của mọi hồ sơ người dùng sẽ là 0. ## Liên kết sản phẩm \{#link-products\} Liên kết một sản phẩm với một loại tiền tệ ảo để khi mua hàng hoặc gia hạn, người dùng sẽ nhận được tín dụng trong loại tiền tệ đó. Bạn có thể liên kết một sản phẩm với nhiều loại tiền tệ, và một loại tiền tệ với nhiều sản phẩm. Bạn có thể liên kết sản phẩm ngay bây giờ, trong bước **Link Products**, hoặc thêm sau khi chỉnh sửa loại tiền tệ. Trong bước **Link Products**, phần **Associated products** liệt kê các sản phẩm cấp loại tiền tệ này. Để thêm một sản phẩm, nhấn **Add associated products**, rồi chọn sản phẩm. Mỗi lần cấp phép sẽ cộng thêm vào số dư của người dùng thay vì ghi đè lên, do đó tín dụng từ các sản phẩm khác nhau sẽ tích lũy lại. Cài đặt tín dụng phụ thuộc vào loại sản phẩm: - **Sản phẩm gói đăng ký** có ba cài đặt credit: - **Credit per cycle** (bắt buộc): Số credit được cấp tại mỗi kỳ gia hạn, bao gồm cả lần gia hạn chuyển từ dùng thử sang trả phí. - **Credit on trial start** (tùy chọn): Một khoản riêng biệt, chỉ cấp một lần khi bắt đầu dùng thử miễn phí. - **Credits expire at the end of each billing cycle** (bật/tắt): Khi bật, Adapty đặt lại credit chưa dùng về 0 tại mỗi kỳ gia hạn, trước khi cấp credit cho chu kỳ mới. Khi tắt, credit tích lũy qua các chu kỳ và không bao giờ hết hạn. - **Sản phẩm mua một lần**: Đặt **Credit amount** — số credit được cấp cho mỗi lần mua. Credit của sản phẩm mua một lần không bao giờ hết hạn. Nhấn **Save** để tạo loại tiền tệ cùng với các liên kết sản phẩm. Các liên kết sản phẩm chỉ có hiệu lực từ thời điểm tạo trở đi. Một sản phẩm mới được liên kết sẽ cấp tín dụng từ lần mua hoặc gia hạn tiếp theo. Adapty không tự động cấp tiền tệ cho những người đang đăng ký — họ cần gia hạn gói đăng ký trước. Khi bạn xóa một liên kết, các khoản cấp trong tương lai sẽ dừng lại, nhưng tín dụng đã được cấp vẫn giữ nguyên. Khi bật chế độ hết hạn, tín dụng có thời hạn sẽ tuân theo vòng đời của gói đăng ký: - Trong thời gian thử lại thanh toán hoặc thời gian ân hạn, Adapty không cấp thêm credit mới. - Sau khi hủy, credit vẫn được giữ cho đến hết kỳ đã thanh toán. - Khi gói đăng ký hết hạn, Adapty đặt lại số credit còn lại về 0. ## Các bước tiếp theo \{#next-steps\} Sau khi tạo tiền tệ ảo và liên kết sản phẩm, các giao dịch mua sẽ tự động cộng credits. Từ đây, bạn có thể: - Theo dõi số dư của từng người dùng trên trang [Virtual currency balance](virtual-currency-balance). - Cấp, tiêu và đọc số dư trong thời gian thực qua server-side API: [Create virtual currency transaction](api-adapty/operations/createVirtualCurrencyTransaction) và [List virtual currency balances](api-adapty/operations/listVirtualCurrencyBalances). Để xem ví dụ thực tế, hãy xem [Virtual currency quickstart](virtual-currency-quickstart). --- # File: virtual-currency-balance --- --- title: "Số dư tiền tệ ảo" description: "Xem số dư tiền tệ ảo của người dùng trên hồ sơ của họ và xem lại mọi thay đổi số dư trong lịch sử sự kiện." --- :::link Bài viết chính: [Tiền tệ ảo](virtual-currencies) ::: Mỗi hồ sơ người dùng trong [Profiles/CRM](profiles-crm) đều hiển thị số dư tiền tệ ảo của người dùng đó, cùng với lịch sử mọi thay đổi. ## Xem số dư trong hồ sơ người dùng \{#view-balances-in-a-user-profile\} Mở [hồ sơ người dùng](https://app.adapty.io/profiles/users). Thẻ **Virtual currency** liệt kê tất cả các loại tiền tệ mà người dùng đang sở hữu. Mỗi hàng hiển thị: - **Mã** tiền tệ, ví dụ như `COINS`. - **Số dư** hiện tại, dưới dạng số nguyên. Số dư được cập nhật khi người dùng kiếm, tiêu hoặc nhận credits. Số dư không thể xuống dưới 0: một giao dịch cố tiêu nhiều hơn số dư hiện có sẽ thất bại với lỗi `insufficient_balance` và không thay đổi gì. Hồ sơ người dùng không có số dư nào sẽ hiển thị thẻ trống. Để đọc số dư từ code của bạn, hãy gọi [List virtual currency balances](api-adapty/operations/listVirtualCurrencyBalances) trong server-side API. ## Xem lại lịch sử thay đổi số dư \{#review-balance-changes-in-the-event-history\} Mọi thay đổi số dư đều xuất hiện trong lịch sử sự kiện của hồ sơ người dùng, theo thứ tự mới nhất trước. Mỗi mục ghi rõ tên thay đổi và các loại tiền tệ bị ảnh hưởng. Lịch sử hiển thị ba loại sự kiện tiền tệ ảo: - **Virtual currency credited**: Một giao dịch mua, gia hạn hoặc bắt đầu dùng thử đã cộng thêm số dư. - **Virtual currency transaction**: Một lệnh gọi API phía server đã cộng hoặc trừ số dư. - **Virtual currency expired**: Số dư có hạn bị đặt lại vào cuối chu kỳ thanh toán hoặc khi gói đăng ký kết thúc. Mỗi mục liệt kê, cho từng loại tiền tệ bị thay đổi: - **Virtual currency**: Tên và mã tiền tệ, ví dụ như `Gold Coins (COINS)`. - **Amount**: Thay đổi có dấu — dương cho khoản cộng, âm cho khoản trừ. - **Balance after**: Số dư của tiền tệ sau khi thay đổi được áp dụng. Một sự kiện có thể thay đổi nhiều loại tiền tệ cùng lúc — ví dụ, một [giao dịch](api-adapty/operations/createVirtualCurrencyTransaction) duy nhất trừ một loại tiền tệ và cộng một loại khác để chuyển đổi giữa chúng. ## Số dư, hồ sơ người dùng và thiết bị \{#balances-profiles-and-devices\} Một số dư luôn thuộc về đúng một [hồ sơ người dùng](profiles-crm) và không bao giờ chuyển sang hồ sơ khác. Adapty không chia sẻ hay chuyển số dư giữa các hồ sơ người dùng theo cách có thể [chia sẻ mức độ truy cập giữa các tài khoản người dùng](profiles-crm#sharing-paid-access-between-user-accounts). Trên thực tế, điều này có nghĩa là: - **Người dùng đã xác định danh tính**: ID người dùng khách hàng luôn trỏ đến cùng một hồ sơ người dùng, vì vậy người dùng thấy cùng một số dư trên mọi thiết bị họ đăng nhập. - **Người dùng ẩn danh**: Một hồ sơ người dùng ẩn danh có thể kiếm và tiêu credits, nhưng nó chỉ tồn tại trên một thiết bị. Khi cùng một người dùng mở ứng dụng của bạn trên thiết bị khác mà không đăng nhập, Adapty tạo một hồ sơ người dùng ẩn danh mới với số dư bằng không. Vì vậy, để vận hành nền kinh tế tiền tệ ảo trên nhiều thiết bị, bạn cần xác định người dùng. Bạn có thể xác định họ trong code ứng dụng thông qua SDK (xem [Xác định người dùng](ios-quickstart-identify) trong hướng dẫn bắt đầu nhanh) hoặc từ backend của bạn thông qua [API phía máy chủ](getting-started-with-server-side-api). Việc xác định người dùng muộn cũng có một lưu ý riêng: nếu người dùng tích lũy tín dụng dưới dạng ẩn danh rồi đăng nhập bằng customer user ID đã thuộc về một hồ sơ người dùng khác, thiết bị sẽ chuyển sang hồ sơ người dùng đó cùng với số dư của nó. Các tín dụng đã tích lũy ẩn danh vẫn nằm trên hồ sơ người dùng cũ và không thể truy cập được. Nếu customer user ID là mới, nó sẽ gắn với hồ sơ người dùng hiện tại và người dùng giữ được số dư. Để an toàn, hãy xác định người dùng trước khi họ có thể tích lũy hoặc mua tín dụng. Trong các lệnh gọi [API phía máy chủ](getting-started-with-server-side-api), hãy xác định hồ sơ người dùng bằng header `adapty-profile-id` hoặc `adapty-customer-user-id` — cả hai đều trỏ đến cùng một hồ sơ người dùng. Đối với hồ sơ người dùng ẩn danh, hãy dùng `adapty-profile-id`. --- # File: app-store-offers --- --- title: "Ưu đãi trên App Store" description: "Thiết lập và quản lý các ưu đãi trên App Store để tăng khả năng giữ chân người dùng." --- :::info Hãy cấu hình [sản phẩm trong cửa hàng](quickstart-products) trước khi làm theo hướng dẫn này. ::: Ưu đãi trên App Store là các deals đặc biệt, dùng thử, hoặc giảm giá dành cho gói đăng ký tự gia hạn. Bao gồm giảm giá và ưu đãi gói giúp thu hút người dùng mới và tăng tỷ lệ chuyển đổi. App Store có bốn loại ưu đãi, và Adapty hỗ trợ tất cả: - **[Ưu đãi giới thiệu](#introductory-offers) dành cho người dùng mới**: - Thời gian đăng ký miễn phí hoặc giảm giá - Chỉ người dùng mới mới đủ điều kiện (những người chưa từng kích hoạt ưu đãi giới thiệu hoặc có gói đăng ký) - Bạn không cần liên kết chúng với sản phẩm trong Adapty. Adapty tự động áp dụng ưu đãi cho người dùng đủ điều kiện khi họ mua sản phẩm. - **Ưu đãi [quảng cáo](#promotional-offers) và [thu hút khách hàng cũ](#win-back-offers)**: - Adapty tự động áp dụng các ưu đãi này khi mua hàng, nhưng bạn cần cấu hình ưu đãi trong sản phẩm và paywall trước. - Ưu đãi quảng cáo bao gồm thời gian đăng ký miễn phí, giảm giá theo phần trăm và giảm giá cố định. Bất kỳ người dùng nào cũng có thể đủ điều kiện. - Ưu đãi thu hút khách hàng cũ bao gồm thời gian đăng ký miễn phí hoặc giảm giá theo phần trăm. Chỉ người dùng đã hủy đăng ký mới đủ điều kiện. - **Mã ưu đãi**: Để biết thêm thông tin, xem [Đổi mã ưu đãi trên iOS](making-purchases#redeem-offer-codes-in-ios). :::important Để sử dụng ưu đãi trên App Store, hãy tải [khóa đăng ký](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers) lên Adapty Dashboard. ::: ## Ưu đãi giới thiệu \{#introductory-offers\} Adapty tự động áp dụng ưu đãi giới thiệu trên iOS nếu người dùng đủ điều kiện. Để bật ưu đãi giới thiệu cho các sản phẩm bạn bán, bạn chỉ cần tạo chúng trong App Store Connect: 1. Mở ứng dụng của bạn trong App Store Connect và chuyển đến **Monetization > Subscriptions**. 2. Chọn một nhóm đăng ký và điều hướng đến gói đăng ký bạn cần. Gói đăng ký phải có thời hạn đã được cấu hình. 3. Nhấp vào **View all Subscription Pricing** và chuyển sang tab **Introductory offers**. Nhấp vào **Set up introductory offer**. 4. Chọn các quốc gia và khu vực nơi ưu đãi giới thiệu sẽ có hiệu lực. 5. Chọn ngày bắt đầu và ngày kết thúc cho ưu đãi giới thiệu. Nếu ưu đãi giới thiệu không có ngày kết thúc cụ thể, chọn **No end date**. Nhấp vào **Next**. 6. Chọn loại ưu đãi giới thiệu. Tùy theo lựa chọn, bạn cũng cần xác định thời hạn và giá của ưu đãi. Đọc thêm trong [tài liệu của Apple](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-introductory-offers-for-auto-renewable-subscriptions). 7. Xem lại lựa chọn của bạn và nhấp vào **Confirm**. Sau khi hoàn tất thiết lập, bạn không cần làm gì thêm trong Adapty. Ưu đãi sẽ kích hoạt cho người dùng đủ điều kiện khi họ mua sản phẩm. Hãy đảm bảo bạn chỉ hiển thị paywall có sản phẩm này cho những người dùng đủ điều kiện nhận ưu đãi. ## Ưu đãi quảng cáo \{#promotional-offers\} Adapty tự động áp dụng ưu đãi quảng cáo nếu người dùng đủ điều kiện. Thiết lập ưu đãi trong App Store Connect trước, sau đó thêm chúng vào sản phẩm và paywall trong Adapty: 1. Mở ứng dụng của bạn trong App Store Connect và chuyển đến **Monetization > Subscriptions** từ menu bên trái. 2. Chọn một nhóm đăng ký và điều hướng đến gói đăng ký bạn cần. Gói đăng ký phải có thời hạn đã được cấu hình. 3. Nhấp vào **View all Subscription Pricing** và chuyển sang tab **Promotional offers**. Nhấp vào **Set up promotional offer**. 4. Điền thông tin chi tiết cho ưu đãi quảng cáo. Các giá trị này không thể thay đổi sau khi tạo và sẽ được tái sử dụng, vì vậy hãy chọn cẩn thận. - **Promotional offer reference name**: Tên ưu đãi quảng cáo. Người dùng sẽ không thấy tên này. - **Promotional offer identifier**: Mã định danh ưu đãi quảng cáo. Bạn sẽ dùng mã này để thêm ưu đãi vào Adapty. 5. Chọn loại ưu đãi quảng cáo. Loại này xác định người dùng sẽ trả giá thấp hơn hay được dùng miễn phí. Để giảm giá, chọn **Pay as you go** hoặc **Pay up front**. Để có thời gian đăng ký miễn phí, chọn **Free**. Sau đó đặt thời hạn và giá ưu đãi. Đọc thêm trong [tài liệu của Apple](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-promotional-offers-for-auto-renewable-subscriptions). 6. Nếu cần, đặt giá khác nhau cho các quốc gia và khu vực khác nhau rồi nhấp vào **Next**. 7. Xem lại lựa chọn của bạn và nhấp vào **Confirm**. 8. [Thêm ưu đãi quảng cáo](create-offer) vào Adapty. ## Ưu đãi thu hút khách hàng cũ \{#win-back-offers\} :::important Trước khi tạo ưu đãi thu hút khách hàng cũ, gói đăng ký của bạn phải được App Review phê duyệt. ::: Adapty tự động áp dụng ưu đãi thu hút khách hàng cũ nếu người dùng đủ điều kiện. Thiết lập ưu đãi trong App Store Connect trước, sau đó thêm chúng vào sản phẩm và paywall trong Adapty: 1. Mở ứng dụng của bạn trong App Store Connect và chuyển đến **Monetization > Subscriptions** từ menu bên trái. 2. Chọn một nhóm đăng ký và điều hướng đến gói đăng ký bạn cần. Gói đăng ký phải có thời hạn đã được cấu hình. 3. Nhấp vào **View all Subscription Pricing** và chuyển sang tab **Win-back offers**. Nhấp vào **Create offer**. 4. Điền thông tin chi tiết cho ưu đãi thu hút khách hàng cũ. Các giá trị này không thể thay đổi sau khi tạo. - **Reference name**: Tên ưu đãi. Người dùng sẽ không thấy tên này. - **Offer identifier**: Mã định danh ưu đãi. Bạn sẽ dùng mã này để thêm ưu đãi vào Adapty. 5. Cấu hình loại, thời hạn và giá ưu đãi. Đọc thêm trong [tài liệu của Apple](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-win-back-offers). 6. Xem lại lựa chọn của bạn và nhấp vào **Confirm**. 7. [Thêm ưu đãi](create-offer) vào Adapty. ## Các bước tiếp theo \{#next-steps\} Sau khi đã thêm ưu đãi, tiếp tục thiết lập: - Nếu bạn cũng có **ứng dụng trên Google Play**, hãy thiết lập [ưu đãi trên Google Play](google-play-offers). - Nếu bạn có **ưu đãi quảng cáo hoặc ưu đãi thu hút khách hàng cũ**, hãy [thêm chúng vào Adapty](create-offer). - Nếu bạn chỉ có **ưu đãi giới thiệu** và không có ưu đãi quảng cáo hoặc thu hút khách hàng cũ, bạn đã hoàn tất. Phần [Cách Adapty hoạt động với ưu đãi](create-offer#how-adapty-works-with-offers) vẫn có thể hữu ích. --- # File: google-play-offers --- --- title: "Ưu đãi trên Google Play" description: "Cấu hình các ưu đãi trên Google Play để cải thiện khả năng kiếm tiền và giữ chân người dùng." --- Trên Google Play, các ưu đãi ở bất kỳ dạng nào (dùng thử miễn phí hoặc thanh toán giảm giá) đều được thêm vào dưới dạng **offers**. Để tạo một ưu đãi, trước tiên bạn cần tạo một gói đăng ký và thêm base plan tự động gia hạn. Ưu đãi luôn được tạo cho các base plan trong gói đăng ký. Trong ảnh chụp màn hình bên dưới, bạn có thể thấy gói đăng ký `premium_access`(1) với hai base plan: `1-month` (2) và `1-year` (3). <img src="/assets/shared/img/c0b1dfa-001930-November-03-XYnbieeu.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Để tạo ưu đãi trong Google Play Console: 1. Nhấp vào **Add offer** và chọn base plan từ danh sách. <img src="/assets/shared/img/75a5d69-eb0bc9a-001931-November-03-eQdthUMx.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhập ID ưu đãi. ID này sẽ được sử dụng sau trong phần phân tích và Adapty Dashboard, vì vậy hãy đặt tên có ý nghĩa. <img src="/assets/shared/img/ff282c2-c0b1dfa-001930-November-03-XYnbieeu.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Chọn tiêu chí đủ điều kiện: 1. **New customer acquisition**: ưu đãi chỉ dành cho người đăng ký mới nếu họ chưa từng sử dụng ưu đãi này trước đây. Đây là lựa chọn phổ biến nhất và nên được dùng theo mặc định. 2. **Upgrade**: ưu đãi này dành cho những khách hàng đang nâng cấp từ gói đăng ký khác. Dùng khi bạn muốn khuyến khích người dùng hiện tại chuyển lên các gói đắt hơn, ví dụ như khách hàng nâng cấp từ gói bronze lên gói gold. 3. **Developer determined**: bạn có thể kiểm soát ai được sử dụng ưu đãi này từ trong code của ứng dụng. Hãy thận trọng khi dùng trong môi trường production để tránh gian lận: người dùng có thể kích hoạt gói đăng ký miễn phí hoặc giảm giá nhiều lần. Trường hợp sử dụng phù hợp cho loại ưu đãi này là thu hút lại những người đăng ký đã rời bỏ. <img src="/assets/shared/img/ee302dc-a506e5a-001934-November-03-TVBLOz2L.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Thêm tối đa hai giai đoạn giá cho ưu đãi của bạn. Có ba loại giai đoạn: 1. **Free trial**: gói đăng ký có thể dùng miễn phí trong một khoảng thời gian cấu hình sẵn (tối thiểu 3 ngày). Đây là ưu đãi phổ biến nhất. 2. **Single payment**: gói đăng ký rẻ hơn nếu khách hàng thanh toán trước. Ví dụ, thông thường gói hàng tháng có giá $9.99, nhưng với loại ưu đãi này, ba tháng đầu tiên chỉ có giá $19.99, giảm 30%. 3. **Discounted recurring payment**: gói đăng ký rẻ hơn trong `n` kỳ đầu tiên. Ví dụ, thông thường gói hàng tháng có giá $9.99, nhưng với loại ưu đãi này, mỗi tháng trong ba tháng đầu chỉ có giá $4.99, giảm 50%. Một ưu đãi có thể có hai giai đoạn. Trong trường hợp đó, giai đoạn đầu phải là Free trial, và giai đoạn thứ hai là Single payment hoặc Discounted recurring payment. Chúng sẽ được áp dụng theo thứ tự này. <img src="/assets/shared/img/d6267f3-a48f79e-001936-November-03-A13wutRh.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important Lưu ý rằng các paywall được tạo bằng Adapty Paywall Builder sẽ chỉ hiển thị giai đoạn đầu tiên của ưu đãi gói đăng ký Google nhiều giai đoạn. Tuy nhiên, hãy yên tâm rằng khi người dùng mua sản phẩm, tất cả các giai đoạn ưu đãi sẽ được áp dụng đúng như cấu hình trong Google Play. ::: 5. Kích hoạt ưu đãi để sử dụng trong ứng dụng. <img src="/assets/shared/img/d3fc09b-f149ba6-001937-November-03-MO9Gz3ap.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Tiếp tục với việc [thêm ưu đãi vào Adapty](create-offer). :::note Offer ID có thể giống nhau cho các base plan khác nhau. ::: ## Các bước tiếp theo \{#next-steps\} Sau khi đã thêm ưu đãi, hãy tiến hành thiết lập: - Nếu bạn có **ứng dụng trên App Store**, hãy xem [hướng dẫn App Store](app-store-offers). - Nếu bạn **chỉ có ứng dụng trên Google Play**, hãy làm theo [hướng dẫn này](create-offer) để thêm ưu đãi vào Adapty. --- # File: create-offer --- --- title: "Thêm ưu đãi vào Adapty" description: "Tạo và quản lý các ưu đãi gói đăng ký đặc biệt bằng công cụ của Adapty." --- Adapty cho phép bạn cung cấp ưu đãi dùng thử hoặc giảm giá cho người dùng mới, hiện tại, hoặc đã huỷ đăng ký. Sau khi thiết lập xong trong App Store Connect hoặc Google Play Console, bạn cần thêm chúng vào Adapty theo hai bước: 1. [Thêm ưu đãi vào sản phẩm trong Adapty bằng ID ưu đãi từ các cửa hàng.](#1-create-offer) 2. [Hiển thị ưu đãi trong một flow hoặc paywall.](#2-display-offer) :::warning Ưu đãi giới thiệu (App Store) được áp dụng tự động nếu người dùng đủ điều kiện. Không cần thêm chúng vào sản phẩm trong Adapty. Hướng dẫn này giải thích cách cấu hình ưu đãi (App Store), ưu đãi thu hút khách hàng cũ (App Store), và tất cả các ưu đãi của Google Play. ::: ## 0. Trước khi bắt đầu \{#before-you-start\} Trước khi thiết lập ưu đãi trong Adapty, hãy đảm bảo những điều sau: 1. Bạn đã tạo tất cả các ưu đãi cần thiết trong cửa hàng: - [App Store](app-store-offers) - [Google Play](google-play-offers) 2. Bạn đã tạo các [sản phẩm](create-product) trong Adapty và đã thêm ID của chúng. 3. Đối với App Store: Bạn đã tải lên [khoá in-app purchase dành cho ưu đãi](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers). ## 1. Thêm ưu đãi vào sản phẩm trong Adapty \{#1-create-offer\} Sau khi ưu đãi (cho cả Play Store và App Store) hoặc ưu đãi thu hút khách hàng cũ (cho App Store) đã được thiết lập trong cửa hàng ứng dụng, việc thêm vào Adapty rất đơn giản: 1. Mở [**Products**](https://app.adapty.io/products) từ menu chính trong Adapty. Tìm sản phẩm bạn muốn thêm ưu đãi. 2. Tìm sản phẩm bạn muốn thêm ưu đãi. Trong cột **Actions**, nhấp vào nút **3 chấm** bên cạnh sản phẩm và chọn **Edit**. 3. Trong cửa sổ **Edit product**, nhấp **+** và chọn **Add offers**. 4. Nhấp **Add offer**. 5. Sau đó nhập thông tin chi tiết về ưu đãi cho sản phẩm. Dưới đây là các trường thông tin cho ưu đãi: - **Offer name**: Đặt tên cho ưu đãi để dễ nhận biết trong Adapty. Dùng bất kỳ tên nào thuận tiện cho bạn. - **App Store Offer type**: Chọn loại ưu đãi App Store bạn đang thêm: Promotional hoặc Win-back. (Ưu đãi giới thiệu không cần thêm — chúng được áp dụng tự động nếu có.) - **App Store Offer ID**: Đây là ID duy nhất cho ưu đãi [mà bạn đã đặt trong App Store](app-store-products). - **Play Store Offer ID**: Tương tự, đây là ID duy nhất cho ưu đãi [mà bạn đã đặt trong Play Store](android-products). :::tip Nếu trường **App Store Offer ID** hoặc **Play Store Offer ID** không hoạt động, hãy chuyển sang tab **Products** và chọn ID sản phẩm. ::: 6. (tuỳ chọn) Thêm nhiều ưu đãi hơn nếu cần bằng cách nhấp **Add offer**. 7. Nhấp **Save** để thêm ưu đãi vào sản phẩm. ## 2. Hiển thị ưu đãi \{#2-display-offer\} Sau khi ưu đãi được gắn vào sản phẩm, hãy hiển thị nó ở nơi người dùng thấy sản phẩm đó — trong một flow hoặc trong một paywall. ### Thêm ưu đãi vào flow \{#add-offer-to-flow\} Trong [Flow Builder](adapty-flow-builder), ưu đãi được gắn vào sản phẩm trên phần tử Products. Trước tiên hãy thêm phần tử sản phẩm và gán sản phẩm vào đó — xem [Thiết lập mua hàng](paywall-product-block). Để gắn ưu đãi: 1. Trên canvas, chọn thẻ sản phẩm cần hiển thị ưu đãi. 2. Ở bảng bên phải, dưới mục **Product**, chọn sản phẩm, sau đó chọn ưu đãi từ danh sách **Select offer (optional)**. ### Thêm ưu đãi vào paywall \{#add-offer-to-paywall\} :::info Bạn không thể thêm ưu đãi vào paywall đang ở trạng thái **live**. Nếu muốn thêm ưu đãi vào paywall hiện có, hãy [nhân bản](duplicate-paywalls) paywall đó và cấu hình sản phẩm trong paywall mới. ::: Để ưu đãi hiển thị và có thể chọn được trong một [paywall](paywalls) cho người dùng ứng dụng của bạn, thực hiện các bước sau: 1. Khi tạo hoặc chỉnh sửa paywall, trong tab **General**, thêm sản phẩm mà bạn vừa thêm ưu đãi vào. 2. Chọn ưu đãi bạn đã tạo trước đó cho sản phẩm này từ danh sách **Offer**. Danh sách này chỉ hiển thị với các sản phẩm có ưu đãi. 3. Nếu cần, thêm nhiều sản phẩm và ưu đãi hơn, nhưng bạn chỉ có thể thêm một ưu đãi cho mỗi sản phẩm. ## Cách Adapty hoạt động với ưu đãi \{#how-adapty-works-with-offers\} Lưu ý những điểm sau về cách ưu đãi hoạt động trong Adapty: - Khi người dùng đủ điều kiện nhận ưu đãi, Adapty tự động áp dụng ưu đãi bạn đã cấu hình khi người dùng thực hiện mua hàng. - Nếu một sản phẩm có cả ưu đãi giới thiệu lẫn ưu đãi đã được cấu hình trong App Store, người dùng đủ điều kiện sẽ nhận ưu đãi giới thiệu trước. Sau khi kết thúc thời gian đó, nếu người dùng vẫn đủ điều kiện nhận ưu đãi và bạn đã cấu hình ưu đãi này trong Adapty, nó sẽ được áp dụng khi họ cố gắng mua lại sản phẩm. - Nếu bạn muốn kiểm soát nhiều hơn cách áp dụng ưu đãi hoặc cần bán sản phẩm không kèm ưu đãi trong một số trường hợp, bạn có một số lựa chọn: - Cấu hình tiêu chí đủ điều kiện trong App Store hoặc Google Play Console - Tạo sản phẩm riêng không có ưu đãi trong App Store hoặc Google Play Console - Tạo sản phẩm riêng không có ưu đãi trong Adapty, thêm paywall chứa cả hai biến thể sản phẩm vào một [placement](placements), và sử dụng [phân khúc](segments) đối tượng để kiểm soát paywall nào được hiển thị cho từng nhóm người dùng. Ví dụ, bạn có thể tạo phân khúc dựa trên **Subscription product** hoặc **Paid access level**, hoặc sử dụng [thuộc tính tùy chỉnh](profiles-crm) để triển khai logic của riêng bạn. --- # File: create-access-level --- --- title: "Tạo mức độ truy cập" description: "Tạo và gán mức độ truy cập trong Adapty để phân khúc người dùng tốt hơn." --- Mức độ truy cập giúp bạn kiểm soát những gì người dùng có thể làm trong ứng dụng mà không cần hardcode các ID sản phẩm cụ thể. Mỗi sản phẩm xác định thời gian người dùng được hưởng một mức độ truy cập nhất định. Vì vậy, mỗi khi người dùng thực hiện mua hàng, Adapty sẽ cấp quyền truy cập vào ứng dụng trong một khoảng thời gian cụ thể (đối với gói đăng ký) hoặc vĩnh viễn (đối với sản phẩm mua trọn đời). Khi bạn tạo ứng dụng trong Adapty Dashboard, mức độ truy cập `premium` sẽ được tự động tạo. Đây là mức độ truy cập mặc định và không thể xóa. :::tip Bạn cũng có thể tạo mức độ truy cập theo cách lập trình bằng [Developer CLI](developer-cli-reference#adapty-access-levels-create). ::: Để tạo mức độ truy cập mới: 1. Vào **[Products](https://app.adapty.io/access-levels)** từ menu chính của Adapty, sau đó chọn tab **Access levels**. <img src="/assets/shared/img/access-level-list.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào **Create access level**. <img src="/assets/shared/img/b8646ca-image.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Create access level**, đặt cho nó một ID. ID này sẽ là định danh trong ứng dụng của bạn, cho phép mở khóa các tính năng bổ sung khi người dùng mua hàng. Ngoài ra, định danh này giúp phân biệt mức độ truy cập này với các mức độ truy cập khác trong ứng dụng. Hãy đảm bảo ID rõ ràng và dễ hiểu để thuận tiện cho bạn. 4. Nhấp vào **Create access level** để xác nhận việc tạo mức độ truy cập. --- # File: assigning-access-level-to-a-product --- --- title: "Gán mức độ truy cập cho sản phẩm" description: "Gán mức độ truy cập cho sản phẩm để tối ưu hóa việc quản lý gói đăng ký." --- Mỗi [Sản phẩm](product) đều cần được liên kết với một mức độ truy cập để đảm bảo người dùng nhận được nội dung tương ứng sau khi mua. Adapty tự động xác định thời hạn gói đăng ký, và thời hạn đó sẽ là ngày hết hạn của mức độ truy cập. Đối với sản phẩm trọn đời, nếu khách hàng mua, mức độ truy cập sẽ luôn hoạt động mà không có ngày hết hạn. Để liên kết mức độ truy cập với một sản phẩm: 1. Trong khi [cấu hình sản phẩm](create-product), chọn mức độ truy cập từ danh sách **Access Level ID**. <img src="/assets/shared/img/access-level-product.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấn **Save**. --- # File: give-access-level-to-specific-customer --- --- title: "Cấp mức độ truy cập cho khách hàng cụ thể" description: "Gán mức độ truy cập cụ thể cho khách hàng bằng các công cụ nâng cao của Adapty." --- Bạn có thể điều chỉnh thủ công mức độ truy cập cho một khách hàng cụ thể ngay trong Adapty Dashboard. Tính năng này đặc biệt hữu ích trong các tình huống hỗ trợ. Ví dụ: nếu bạn muốn gia hạn quyền sử dụng premium cho người dùng thêm một tuần để cảm ơn họ đã để lại đánh giá tuyệt vời. ## Cấp mức độ truy cập cho khách hàng cụ thể trong Adapty Dashboard \{#give-access-level-to-a-specific-customer-in-the-adapty-dashboard\} 1. Vào **[Profiles and Segments](https://app.adapty.io/placements)** từ menu chính của Adapty. <img src="/assets/shared/img/profiles-list.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào khách hàng bạn muốn cấp quyền truy cập. 3. Nhấp vào **Add access level**. <img src="/assets/shared/img/add-access-level.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Chọn mức độ truy cập cần cấp và thời điểm hết hạn cho khách hàng này. 5. Nhấp vào **Apply**. ## Cấp mức độ truy cập cho khách hàng cụ thể qua API \{#give-access-level-to-a-specific-customer-via-api\} Bạn cũng có thể cấp mức độ truy cập cho khách hàng từ máy chủ của mình bằng Adapty API. Tính năng này rất tiện lợi nếu bạn có các phần thưởng cho chương trình giới thiệu hoặc các sự kiện khác liên quan đến sản phẩm của bạn. Xem thêm chi tiết tại trang [Cấp mức độ truy cập bằng server-side API](api-adapty/operations/grantAccessLevel). --- # File: local-access-levels --- --- title: "Mức độ truy cập cục bộ" description: "Quản lý mức độ truy cập trong trường hợp có sự cố tạm thời." --- :::important Lưu ý những điểm sau: - Mức độ truy cập cục bộ được hỗ trợ trong Adapty SDK bắt đầu từ phiên bản 3.12. - Theo mặc định, mức độ truy cập cục bộ bị tắt trên Android để tăng cường bảo mật. Nếu bạn cần dùng tính năng này, hãy bật nó khi khởi động SDK: [Android](sdk-installation-android#enable-local-access-levels), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter#enable-local-access-levels-android). ::: Mỗi sản phẩm bạn cấu hình đều có một [**mức độ truy cập**](access-level) liên kết với nó. Khi người dùng thực hiện mua hàng, Adapty SDK gán mức độ truy cập cho [hồ sơ người dùng](profiles-crm), vì vậy bạn cần sử dụng mức độ truy cập này để xác định liệu người dùng có thể truy cập nội dung trả phí trong ứng dụng hay không. Adapty SDK rất đáng tin cậy, và rất hiếm khi máy chủ của nó không khả dụng. Tuy nhiên, ngay cả trong trường hợp hiếm gặp đó, người dùng của bạn sẽ không nhận ra điều gì bất thường. Nếu người dùng thực hiện mua hàng nhưng Adapty không thể nhận được phản hồi, SDK sẽ chuyển sang xác minh giao dịch mua trực tiếp trong cửa hàng. Do đó, mức độ truy cập được cấp cục bộ trong ứng dụng mà không cần thiết lập thêm gì để kích hoạt. SDK xử lý điều này tự động ở phía sau, và người dùng sẽ truy cập những gì họ đã trả tiền như bình thường. Lưu ý những điểm sau về cách hoạt động của mức độ truy cập cục bộ: - Khi người dùng kết nối lại mạng, thông tin giao dịch sẽ tự động được đẩy lên máy chủ Adapty, sau đó áp dụng các giao dịch vào hồ sơ người dùng và trả về hồ sơ đã cập nhật cho SDK. - Dữ liệu đã cập nhật sẽ không xuất hiện trong Adapty analytics cho đến khi dữ liệu được đẩy lên. - Mức độ truy cập cục bộ chỉ hoạt động khi máy chủ Adapty bị ngừng hoạt động. Trong các trường hợp khác, SDK sẽ sử dụng dữ liệu đã được lưu cache. - Mức độ truy cập cục bộ không hoạt động với các sản phẩm consumable, ngoại trừ khi sản phẩm consumable được gán loại gói đăng ký (theo tháng, theo năm, theo tuần, v.v.) trong Adapty dashboard. --- # File: choose-meaningful-placements --- --- title: "Chọn placement phù hợp" description: "Tối ưu hóa placement flow và paywall với Adapty để tăng mức độ tương tác của người dùng và doanh thu." --- Khi [tạo placement](create-placement), bạn cần cân nhắc đến luồng logic của ứng dụng và trải nghiệm người dùng mà bạn muốn tạo ra. Hầu hết các ứng dụng không cần nhiều hơn 5 [placement](placements) mà vẫn đảm bảo khả năng chạy thử nghiệm. Dưới đây là ví dụ về cách bạn có thể cấu trúc các placement: <img src="/assets/shared/img/placement-flows.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 1. **Onboarding flow:** Đây là giai đoạn người dùng tương tác lần đầu tiên với ứng dụng của bạn. Đây là cơ hội tuyệt vời để giới thiệu giá trị của ứng dụng bằng cách kết hợp các placement flow, onboarding và paywall tại đây. Hơn 80% gói đăng ký được kích hoạt trong quá trình onboarding, vì vậy hãy tập trung vào việc bán các gói đăng ký mang lại lợi nhuận cao nhất ở đây. Với Adapty, bạn có thể dễ dàng thiết lập các [flow](adapty-flow-builder), [onboarding](onboardings) và [paywall](paywalls) khác nhau cho từng đối tượng, đồng thời chạy A/B test để tìm ra lựa chọn tốt nhất cho ứng dụng. Ví dụ: bạn có thể chạy A/B test cho người dùng tại Mỹ, hiển thị các gói đăng ký giá cao hơn với xác suất 50%. 2. **Cài đặt ứng dụng:** Nếu người dùng chưa đăng ký trong quá trình onboarding, bạn có thể tạo một placement flow hoặc paywall bên trong ứng dụng. Vị trí này có thể là trong phần cài đặt ứng dụng hoặc sau khi người dùng hoàn thành một hành động mục tiêu cụ thể. Vì người dùng bên trong ứng dụng thường cân nhắc kỹ hơn trước khi đăng ký, các sản phẩm ở đây có thể có mức giá thấp hơn một chút so với giai đoạn onboarding. 3. **Promo:** Nếu người dùng vẫn chưa đăng ký sau khi xem flow hoặc paywall nhiều lần, điều đó có thể cho thấy giá quá cao hoặc họ còn do dự về việc đăng ký. Trong trường hợp này, bạn có thể hiển thị một ưu đãi đặc biệt với gói đăng ký giá phải chăng nhất hoặc thậm chí một sản phẩm quyền truy cập trọn đời. Điều này có thể giúp thu hút những người dùng nhạy cảm về giá hoặc còn hoài nghi về việc đăng ký để thực hiện mua hàng. Hầu hết các ứng dụng sẽ có logic và placement tương tự, theo dõi hành trình người dùng và các điểm quan trọng nơi flow, paywall, onboarding hoặc A/B test có thể được hiển thị để thúc đẩy chuyển đổi và doanh thu. Bạn có thể cấu hình chúng trong từng placement để thử nghiệm và tối ưu hóa chiến lược kiếm tiền của mình. --- # File: create-placement --- --- title: "Tạo placement" description: "Tạo và quản lý các placement trong Adapty để cải thiện hiệu suất flow và paywall." --- [Placement](placements) là một vị trí cụ thể trong ứng dụng di động của bạn, nơi bạn có thể hiển thị flow, paywall, onboarding hoặc A/B test. Ví dụ, màn hình chọn gói đăng ký có thể xuất hiện trong flow khởi động, trong khi một consumable (như đồng xu vàng) có thể hiện ra khi người dùng hết xu trong game. Bạn có thể hiển thị cùng một hoặc các flow, paywall, onboarding, hay A/B test khác nhau ở nhiều placement hoặc cho các phân khúc người dùng khác nhau — gọi là "đối tượng" trong Adapty. Xem phần [Chọn placement có ý nghĩa](choose-meaningful-placements) để biết thêm các gợi ý về cách chọn placement phù hợp. :::tip Bạn cũng có thể tạo placement theo cách lập trình bằng [Developer CLI](developer-cli-reference#adapty-placements-create). ::: :::info Mặc dù quy trình tạo placement tương tự nhau cho flow, paywall và onboarding, bạn không thể tạo một placement phục vụ nhiều hơn một loại — mỗi loại placement xử lý các chỉ số khác nhau. ::: ## Tạo và cấu hình placement \{#create-and-configure-a-placement\} 1. Vào **[Placements](https://app.adapty.io/placements)** từ menu chính của Adapty. Chuyển sang tab **Flows**, **Paywalls**, hoặc **Onboardings** tùy theo loại placement bạn muốn tạo. 2. Nhấp vào **Create placement**. <img src="/assets/shared/img/create-placement-2.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhập **Placement name**. Đây là định danh nội bộ trong Adapty Dashboard. Bạn có thể chỉnh sửa nó sau nếu cần. 4. Nhập **Placement ID**. Bạn sẽ dùng ID này trong Adapty SDK để gọi các [flow](adapty-flow-builder), [paywall](paywalls), [onboarding](onboardings) và [A/B test](ab-tests) của placement. Bạn không thể chỉnh sửa nó sau vì đây là giá trị duy nhất cho mỗi placement. Tiếp theo, gán một flow, paywall, onboarding hoặc A/B test cho placement. Adapty hỗ trợ [đối tượng](audience) — các phân khúc người dùng dựa trên [phân khúc](segments) — để bạn có thể hiển thị nội dung khác nhau cho các nhóm người dùng khác nhau. Nếu bạn không cần nhắm mục tiêu, đối tượng mặc định *All users* sẽ bao gồm tất cả mọi người. :::note Để tiếp tục, hãy đảm bảo rằng bạn đã tạo một flow, paywall, onboarding, hoặc A/B test mà bạn muốn chạy và một đối tượng mà bạn muốn chỉ định. ::: 1. Trong cửa sổ **Placements/ Your placement**, thêm một flow, paywall, onboarding, hoặc A/B test để hiển thị cho đối tượng mặc định *All users*. Để làm điều này, nhấp vào nút **Run flow**, **Run paywall**, hoặc **Run A/B test** (nhãn phụ thuộc vào loại placement), sau đó chọn flow, paywall, onboarding, hoặc A/B test mong muốn từ danh sách thả xuống. 2. Nếu bạn muốn sử dụng nhiều hơn một đối tượng trong placement để tạo nội dung cá nhân hóa phù hợp với các nhóm người dùng khác nhau, nhấp vào nút **Add audience** và chọn phân khúc người dùng mong muốn từ danh sách. <Zoom> <img src="/docs/img/placement-add-audience.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 3. Bây giờ hãy thêm flow, paywall, onboarding, hoặc A/B test để hiển thị cho đối tượng này. 4. Thêm bao nhiêu đối tượng tùy theo nhu cầu. 5. Nếu bạn có nhiều hơn một đối tượng, hãy kiểm tra xem các đối tượng có thứ tự ưu tiên đúng hay không. 6. Nhấp vào nút **Save and publish button**. Khi placement đã được lưu và xuất bản, bạn đã có mọi thứ cần thiết — hãy dùng **Placement ID** trong code ứng dụng để tải và hiển thị nó. ## Các bước tiếp theo \{#next-steps\} Hiển thị paywall trong ứng dụng của bạn: [iOS](ios-present-paywalls) | [Android](android-present-paywalls) | [React Native](react-native-present-paywalls) | [Flutter](flutter-present-paywalls) | [Unity](unity-present-paywalls) | [Kotlin Multiplatform](kmp-present-paywalls) | [Capacitor](capacitor-present-paywalls) Hiển thị onboarding trong ứng dụng của bạn: [iOS](ios-present-onboardings) | [Android](android-present-onboardings) | [React Native](react-native-present-onboardings) | [Flutter](flutter-present-onboardings) | [Unity](unity-present-onboardings) | [Kotlin Multiplatform](kmp-present-onboardings) | [Capacitor](capacitor-present-onboardings) --- # File: edit-placement --- --- title: "Chỉnh sửa placement" description: "Tìm hiểu cách chỉnh sửa placement trong Adapty để tối ưu hóa khả năng hiển thị của flow và paywall cũng như mức độ tương tác của người dùng." --- [Placement](placements) là một vị trí cụ thể trong ứng dụng di động của bạn, nơi có thể hiển thị một flow, paywall, onboarding, hoặc A/B test. Ví dụ: màn hình chọn gói đăng ký có thể xuất hiện trong flow khởi động ứng dụng, trong khi một sản phẩm consumable (chẳng hạn như đồng xu vàng) có thể được hiển thị khi người dùng hết xu trong game. Bạn có thể linh hoạt hiển thị cùng một hoặc các flow, paywall, onboarding, hoặc A/B test khác nhau trên nhiều placement hoặc phân khúc người dùng khác nhau, được gọi là đối tượng trong Adapty. Để chỉnh sửa một placement hiện có: 1. Vào **[Placements](https://app.adapty.io/placements)** từ menu chính của Adapty. Chuyển sang tab **Flows**, **Paywalls**, hoặc **Onboardings** tùy thuộc vào loại placement bạn muốn chỉnh sửa. 2. Nhấp vào placement bạn muốn chỉnh sửa. 3. Nhấp vào **Edit placement** ở góc trên bên phải. 4. Thực hiện các thay đổi bạn cần. Để biết thêm chi tiết về các tùy chọn trong cửa sổ này, vui lòng đọc phần [Tạo placement](create-placement). 5. Nhấp vào nút **Save and publish** để xác nhận các thay đổi. --- # File: export-placements --- --- title: "Xuất placement" description: "Tìm hiểu cách xuất các placement trong Adapty để tối ưu hóa khả năng hiển thị của flow và paywall cũng như mức độ tương tác của người dùng." --- Khi làm việc với nhiều flow, paywall và onboarding, bạn cần theo dõi xem cái nào đang được hiển thị cho người dùng nào. Bạn có thể xuất toàn bộ cài đặt [placement](placements) ra file CSV để xem flow/paywall/onboarding nào xuất hiện cho từng đối tượng, đồng thời kiểm tra lại thiết lập sau khi thực hiện thay đổi hoặc chạy thử nghiệm. :::tip Nếu tiện hơn, bạn có thể [xuất placement bằng server-side API](api-export-analytics/operations/retrievePlacementInfo). ::: Để xuất placement của flow, paywall hoặc onboarding: 1. Vào **[Placements](https://app.adapty.io/placements)** trong menu chính. Chuyển sang tab **Flows**, **Paywalls** hoặc **Onboardings** — placement của từng loại được xuất riêng. 2. Nhấn **Export to CSV**. <img src="/assets/shared/img/export-placement.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> File CSV được xuất chứa các thông tin sau về placement của bạn: - Placement ID - Tên placement - Tên đối tượng - Tên phân khúc - Tên A/B test xuyên placement - Tên A/B test - Tên flow, tên paywall hoặc tên onboarding (tùy thuộc vào tab bạn đã xuất) :::note A/B test xuyên placement không được hỗ trợ cho placement của flow, vì vậy cột đó sẽ trống trong các lần xuất flow. ::: --- # File: delete-placement --- --- title: "Xóa placement" description: "Tìm hiểu cách xóa một placement trong Adapty mà không ảnh hưởng đến hiệu suất flow hoặc paywall của bạn." --- [Placement](placements) chỉ định một vị trí cụ thể trong ứng dụng di động của bạn, nơi flow, paywall, onboarding hoặc A/B test có thể được hiển thị. :::danger Mặc dù bạn có thể xóa bất kỳ placement nào, nhưng điều quan trọng là phải đảm bảo rằng bạn không xóa một placement đang được sử dụng trong ứng dụng di động của mình. Việc xóa một placement của flow hoặc paywall đang hoạt động sẽ dẫn đến paywall dự phòng cục bộ được hiển thị vĩnh viễn nếu bạn đã [thiết lập nó](fallback-paywalls), và bạn sẽ không bao giờ có thể thay thế nó bằng flow hoặc paywall động trong các phiên bản ứng dụng đã phát hành. ::: Để xóa một placement hiện có: 1. Vào **[Placements](https://app.adapty.io/placements)** từ menu chính của Adapty. Chuyển sang tab **Flows**, **Paywalls** hoặc **Onboardings** tùy thuộc vào loại placement bạn muốn xóa. 2. Nhấp vào nút **3 chấm** bên cạnh placement và chọn tùy chọn **Delete**. <img src="/assets/shared/img/delete-placement.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Delete placement** vừa mở, nhập tên sản phẩm bạn sắp xóa. <img src="/assets/shared/img/8177c51-delete_placement.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấp vào nút **Delete forever** để xác nhận việc xóa. --- # File: add-audience-paywall-ab-test --- --- title: "Thêm đối tượng và flow, paywall, hoặc A/B test vào placement" description: "Chạy A/B test trên các flow và paywall cho các phân khúc đối tượng khác nhau trong Adapty." --- **Đối tượng** trong Adapty là các nhóm người dùng được xác định theo [phân khúc](segments). Chúng cho phép bạn hiển thị flow, paywall, onboarding và A/B test đến đúng những người dùng cần xem. Hãy xây dựng phân khúc bằng các bộ lọc để đảm bảo mỗi nhóm nhận được nội dung phù hợp. Khi bạn thêm một đối tượng vào [placement](placements), bạn đang nhắm mục tiêu flow, paywall, onboarding hoặc A/B test vào một nhóm người dùng cụ thể. Việc liên kết đối tượng với placement đảm bảo đúng người dùng thấy đúng nội dung vào đúng thời điểm trong hành trình sử dụng ứng dụng của họ. Mở placement mà bạn muốn thêm flow, paywall, onboarding hoặc A/B test, hoặc tạo mới trong menu [Placements](https://app.adapty.io/placements). :::note Để tiếp tục, hãy đảm bảo rằng bạn đã tạo một flow, paywall, onboarding, hoặc A/B test mà bạn muốn chạy và một đối tượng mà bạn muốn chỉ định. ::: 1. Trong cửa sổ **Placements/ Your placement**, thêm một flow, paywall, onboarding, hoặc A/B test để hiển thị cho đối tượng mặc định *All users*. Để làm điều này, nhấp vào nút **Run flow**, **Run paywall**, hoặc **Run A/B test** (nhãn phụ thuộc vào loại placement), sau đó chọn flow, paywall, onboarding, hoặc A/B test mong muốn từ danh sách thả xuống. 2. Nếu bạn muốn sử dụng nhiều hơn một đối tượng trong placement để tạo nội dung cá nhân hóa phù hợp với các nhóm người dùng khác nhau, nhấp vào nút **Add audience** và chọn phân khúc người dùng mong muốn từ danh sách. <Zoom> <img src="/docs/img/placement-add-audience.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 3. Bây giờ hãy thêm flow, paywall, onboarding, hoặc A/B test để hiển thị cho đối tượng này. 4. Thêm bao nhiêu đối tượng tùy theo nhu cầu. 5. Nếu bạn có nhiều hơn một đối tượng, hãy kiểm tra xem các đối tượng có thứ tự ưu tiên đúng hay không. 6. Nhấp vào nút **Save and publish button**. --- # File: change-audience-priority --- --- title: "Thay đổi ưu tiên đối tượng trong placement" description: "Điều chỉnh thứ tự ưu tiên đối tượng trong Adapty để nhắm mục tiêu người dùng với các ưu đãi được cá nhân hóa." --- Khi bạn có nhiều đối tượng người dùng khác nhau trong một [placement](placements), một người dùng có thể thuộc về nhiều hơn một đối tượng. Ví dụ, nếu bạn đã định nghĩa các đối tượng như "Beginners", "Runners", và một đối tượng chung như "All users", điều quan trọng là phải xác định đối tượng nào cần được xem xét trước khi một người dùng rơi vào nhiều danh mục. <img src="/assets/shared/img/afee54f-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trong trường hợp này, chúng ta dựa vào thứ tự ưu tiên đối tượng. Thứ tự ưu tiên đối tượng là một thứ tự số, trong đó #1 là cao nhất. Nó hướng dẫn trình tự kiểm tra các đối tượng. Nói đơn giản hơn, thứ tự ưu tiên đối tượng giúp Adapty quyết định đối tượng nào được áp dụng trước khi chọn paywall, onboarding hay A/B test để hiển thị. Nếu mức độ ưu tiên của một đối tượng thấp, những người dùng có thể đủ điều kiện sẽ bị bỏ qua. Thay vào đó, họ có thể được chuyển đến một đối tượng khác có mức độ ưu tiên cao hơn. Các đối tượng crossplacement, tức là những đối tượng được tạo cho [A/B test crossplacement](ab-tests#ab-test-types), luôn có mức độ ưu tiên cao hơn các đối tượng thông thường. Đối tượng "All users" luôn có mức độ ưu tiên thấp nhất vì đây là đối tượng dự phòng và bao gồm tất cả những người không khớp với bất kỳ đối tượng nào khác. Để điều chỉnh thứ tự ưu tiên đối tượng cho một placement: 1. Khi tạo mới hoặc chỉnh sửa một placement hiện có, nhấp vào **Edit priority**. Nút này chỉ hiển thị khi có ít nhất ba đối tượng được thêm vào một placement ("All users" và hai đối tượng khác). Nếu ít hơn, thứ tự đã rõ ràng — đối tượng "All users" luôn đứng cuối. <img src="/assets/shared/img/edit-priority.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong cửa sổ **Edit audience priorities** vừa mở, kéo và thả các đối tượng để sắp xếp lại thứ tự cho đúng. <img src="/assets/shared/img/reorder_audiences.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp vào nút **Save**. --- # File: placement-metrics --- --- title: "Chỉ số Placement" description: "Phân tích các chỉ số placement trong Adapty để cải thiện hiệu suất paywall." --- Với Adapty, bạn có thể tạo và quản lý nhiều placement trong ứng dụng, mỗi placement được liên kết với các paywall hoặc A/B test riêng biệt. Sự linh hoạt này giúp bạn nhắm mục tiêu đến các phân khúc người dùng cụ thể, thử nghiệm các ưu đãi hoặc mô hình định giá khác nhau, và tối ưu hóa chiến lược kiếm tiền của ứng dụng. Để thu thập thông tin chi tiết về hiệu suất của các placement và mức độ tương tác của người dùng với ưu đãi, Adapty theo dõi nhiều tương tác của người dùng và các giao dịch liên quan đến paywall được hiển thị. Hệ thống phân tích mạnh mẽ ghi lại các chỉ số bao gồm lượt xem, lượt xem duy nhất, lượt mua, trial, hoàn tiền, tỷ lệ chuyển đổi và doanh thu. Các chỉ số được thu thập liên tục cập nhật theo thời gian thực và có thể được truy cập, phân tích tiện lợi qua dashboard thân thiện với người dùng của Adapty. Bạn có thể tùy chỉnh khoảng thời gian phân tích, áp dụng bộ lọc theo các thông số khác nhau, và so sánh chỉ số giữa các placement, phân khúc người dùng hoặc sản phẩm. Chỉ số placement có sẵn trong danh sách placement, nơi bạn có thể xem tổng quan hiệu suất của tất cả các placement. Chế độ xem tổng quan này cung cấp chỉ số tổng hợp cho từng placement, cho phép bạn so sánh hiệu suất và xác định xu hướng. Để phân tích chi tiết hơn từng placement, bạn có thể điều hướng đến trang chỉ số chi tiết của placement. Trên trang này, bạn sẽ tìm thấy các chỉ số toàn diện dành riêng cho placement được chọn. Các chỉ số này cung cấp thông tin sâu hơn về cách một placement cụ thể đang hoạt động, giúp bạn đánh giá hiệu quả và đưa ra quyết định dựa trên dữ liệu. <img src="/assets/shared/img/3e711fc-CleanShot_2023-07-26_at_14.55.042x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Lọc chỉ số theo ngày cài đặt \{#filter-metrics-by-install-date\} Các chỉ số về paywall, trial và mua hàng có thể được nhóm theo hai loại ngày khác nhau: - **Ngày sự kiện** — khi paywall được xem, trial bắt đầu, hoặc giao dịch mua xảy ra. - **Ngày cài đặt** — khi người dùng lần đầu mở ứng dụng. Hai chế độ xem này có thể hiển thị các con số rất khác nhau cho cùng một khoảng thời gian. Hộp kiểm **Filter metrics by install date** kiểm soát chế độ nào được dashboard sử dụng: - **Bỏ chọn (mặc định)**: Các chỉ số được nhóm theo ngày sự kiện. - **Đã chọn**: Các chỉ số được nhóm theo ngày cài đặt. **Ví dụ.** Bạn đặt khoảng thời gian từ ngày 1–30 tháng 4 và xem các trial. - **Bỏ chọn**: Hiển thị các trial *bắt đầu* trong tháng 4, bất kể người dùng đó cài đặt ứng dụng khi nào. - **Đã chọn**: Hiển thị các trial từ những người dùng *đã cài đặt* trong tháng 4, bất kể trial của họ bắt đầu khi nào. Dùng chế độ xem theo ngày cài đặt để đo hiệu quả thu hút người dùng cho một cohort cụ thể. Dùng chế độ xem theo ngày sự kiện để đo hoạt động paywall hoặc onboarding trong một khoảng thời gian cụ thể. ### Điều khiển chỉ số \{#metrics-controls\} Hệ thống hiển thị chỉ số dựa trên khoảng thời gian đã chọn và sắp xếp chúng theo thông số cột bên trái với bốn cấp độ thụt lề. #### Tùy chọn xem dữ liệu chỉ số \{#view-options-for-metrics-data\} Trang chỉ số placement cung cấp hai tùy chọn xem dữ liệu: theo paywall và theo đối tượng. <img src="/assets/shared/img/9d26b32-Export-1690376094858.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trong chế độ xem theo paywall, chỉ số được nhóm theo các placement liên kết với paywall. Điều này cho phép người dùng phân tích chỉ số theo các placement khác nhau. Trong chế độ xem theo đối tượng, chỉ số được nhóm theo đối tượng mục tiêu của paywall. Người dùng có thể đánh giá chỉ số dành riêng cho từng phân khúc đối tượng. #### Khoảng thời gian \{#time-ranges\} Bạn có thể chọn từ nhiều khoảng thời gian để phân tích dữ liệu chỉ số, cho phép tập trung vào các khoảng thời gian cụ thể như ngày, tuần, tháng hoặc phạm vi ngày tùy chỉnh. <img src="/assets/shared/img/15d2c3e-CleanShot_2023-07-26_at_16.49.272x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> #### Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: Adapty cung cấp các công cụ mạnh mẽ để lọc và tùy chỉnh phân tích chỉ số theo nhu cầu của bạn. Với trang chỉ số của Adapty, bạn có thể truy cập nhiều khoảng thời gian, tùy chọn nhóm và khả năng lọc. - ✅ Lọc theo: Đối tượng, paywall, nhóm paywall, placement, quốc gia, cửa hàng. - ✅ Nhóm theo: Phân khúc, cửa hàng và sản phẩm #### Biểu đồ chỉ số đơn \{#single-metrics-chart\} Một trong những thành phần chính của trang chỉ số placement là phần biểu đồ, trực quan hóa các chỉ số được chọn và hỗ trợ phân tích dễ dàng. Phần biểu đồ trên trang chỉ số placement bao gồm biểu đồ thanh ngang trực quan hóa các giá trị chỉ số được chọn. Mỗi thanh trong biểu đồ tương ứng với một giá trị chỉ số và có kích thước tỷ lệ, giúp dễ dàng nắm bắt dữ liệu ngay lập tức. Đường ngang biểu thị khoảng thời gian đang được phân tích, và cột dọc hiển thị các giá trị số của chỉ số. Tổng giá trị của tất cả các chỉ số được hiển thị bên cạnh biểu đồ. <img src="/assets/shared/img/4623c5b-Export-1690375597411.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Ngoài ra, nhấp vào biểu tượng mũi tên ở góc trên bên phải của phần biểu đồ sẽ mở rộng chế độ xem, hiển thị các chỉ số được chọn trên toàn bộ đường của biểu đồ. #### Tóm tắt tổng chỉ số \{#total-metrics-summary\} Bên cạnh biểu đồ chỉ số đơn, phần tóm tắt tổng chỉ số hiển thị các giá trị tích lũy cho các chỉ số được chọn tại một thời điểm cụ thể, với khả năng thay đổi chỉ số hiển thị bằng menu thả xuống. <img src="/assets/shared/img/0f647cf-CleanShot_2023-07-26_at_14.55.492x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Định nghĩa các chỉ số \{#metrics-definitions\} Khám phá sức mạnh của các chỉ số placement với những định nghĩa toàn diện dưới đây. Từ doanh thu đến tỷ lệ chuyển đổi, bạn sẽ có được những insight giá trị giúp tối ưu hóa chiến lược kiếm tiền và thúc đẩy thành công cho ứng dụng của mình. <img src="/assets/shared/img/771a0f0-Export-1690375049771.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: #### Doanh thu \{#revenue\} Chỉ số này đại diện cho tổng số tiền tạo ra tính bằng USD từ các lần mua và gia hạn trong các placement cụ thể. Lưu ý rằng phép tính doanh thu không bao gồm hoa hồng Apple App Store hoặc Google Play Store và được tính trước khi khấu trừ bất kỳ phí nào. #### Proceeds \{#proceeds\} Chỉ số này đại diện cho số tiền thực tế mà chủ ứng dụng nhận được tính bằng USD từ các lần mua và gia hạn trong các placement cụ thể sau khi khấu trừ hoa hồng áp dụng của Apple App Store hoặc Google Play Store. Nó phản ánh doanh thu ròng đóng góp trực tiếp vào thu nhập của ứng dụng. Để biết thêm thông tin về cách tính proceeds, bạn có thể tham khảo [tài liệu](analytics-cohorts#revenue-vs-proceeds) Adapty. #### ARPPU \{#arppu\} ARPPU là viết tắt của Average revenue per paying user (Doanh thu trung bình trên mỗi người dùng trả phí), đo lường doanh thu trung bình được tạo ra trên mỗi người dùng trả phí trong các placement cụ thể. Nó được tính bằng tổng doanh thu chia cho số người dùng trả phí duy nhất. Ví dụ: nếu tổng doanh thu là $15.000 và có 1.000 người dùng trả phí, ARPPU sẽ là $15. #### ARPAS \{#arpas\} ARPAS, hay Average revenue per active subscriber (Doanh thu trung bình trên mỗi người đăng ký đang hoạt động), cho phép bạn đo lường doanh thu trung bình được tạo ra trên mỗi người đăng ký đang hoạt động trong các placement cụ thể. Nó được tính bằng cách chia tổng doanh thu cho số người đăng ký đã kích hoạt trial hoặc gói đăng ký. Ví dụ: nếu tổng doanh thu là $5.000 và có 1.000 người đăng ký, ARPAS sẽ là $5. Chỉ số này giúp đánh giá tiềm năng kiếm tiền trung bình trên mỗi người đăng ký. #### ARPU \{#arpu\} Chỉ dành cho placement onboarding. ARPU là doanh thu trung bình trên mỗi người dùng đã xem onboarding. Được tính bằng tổng doanh thu chia cho số lượng người xem duy nhất. #### Unique CR to purchases \{#unique-cr-to-purchases\} Tỷ lệ chuyển đổi duy nhất sang lần mua được tính bằng cách chia số lần mua trong các placement cụ thể cho số lượt xem duy nhất. Nó tập trung vào tỷ lệ mua so với số lượt xem duy nhất, cung cấp thông tin về hiệu quả chuyển đổi khách truy cập duy nhất trong các placement cụ thể thành khách hàng trả phí. #### CR to purchases \{#cr-to-purchases\} Tỷ lệ chuyển đổi sang lần mua được tính bằng cách chia số lần mua trong các placement cụ thể cho tổng số lượt xem paywall. Nó chỉ ra tỷ lệ phần trăm lượt xem trong các placement cụ thể dẫn đến lần mua, cung cấp thông tin về hiệu quả của paywall trong việc chuyển đổi người dùng thành khách hàng trả phí. #### Unique CR to trials \{#unique-cr-to-trials\} Tỷ lệ chuyển đổi duy nhất sang trial được tính bằng cách chia số lượt bắt đầu trial trong các placement cụ thể cho số lượt xem duy nhất. Nó đo lường tỷ lệ phần trăm lượt xem duy nhất trong các placement cụ thể dẫn đến kích hoạt trial, cung cấp thông tin về hiệu quả của paywall trong việc chuyển đổi khách truy cập duy nhất thành người dùng trial. #### Purchases \{#purchases\} Purchases đại diện cho tổng tích lũy của các giao dịch được thực hiện trên paywall trong các placement cụ thể. Các giao dịch sau đây được bao gồm trong chỉ số này (không bao gồm gia hạn): - Các lần mua mới được thực hiện trực tiếp trong các placement cụ thể. - Chuyển đổi trial của các trial được kích hoạt ban đầu trong các placement cụ thể. - Hạ cấp, nâng cấp và chuyển đổi chéo gói đăng ký được thực hiện trong các placement cụ thể. - Khôi phục gói đăng ký trong các placement cụ thể, chẳng hạn khi gói đăng ký được tái kích hoạt sau khi hết hạn mà không có tự động gia hạn. Bằng cách xem xét các loại giao dịch khác nhau này, chỉ số purchases cung cấp cái nhìn toàn diện về hoạt động thu thập và kiếm tiền tổng thể trong các placement cụ thể. #### Trials \{#trials\} Chỉ số trials đại diện cho tổng số trial đã được kích hoạt trong các placement cụ thể. Nó phản ánh số lượng người dùng đã bắt đầu thời gian dùng thử qua paywall trong các placement đó. Chỉ số này giúp theo dõi hiệu quả của ưu đãi dùng thử và cung cấp thông tin về mức độ tương tác của người dùng và tỷ lệ chuyển đổi từ trial sang gói đăng ký trả phí. #### Trials canceled \{#trials-canceled\} Chỉ số trials canceled đại diện cho số lượng trial trong các placement cụ thể mà tính năng tự động gia hạn đã bị tắt. Điều này xảy ra khi người dùng hủy đăng ký thủ công khỏi trial, cho thấy quyết định không tiếp tục gói đăng ký sau khi thời gian dùng thử kết thúc. Theo dõi trials canceled cung cấp thông tin có giá trị về hành vi người dùng và cho phép bạn hiểu tỷ lệ người dùng từ chối trial trong các placement cụ thể. #### Refunds \{#refunds\} Chỉ số refunds đại diện cho số lần mua và gói đăng ký được hoàn tiền trong các placement cụ thể. Điều này bao gồm các giao dịch đã bị đảo ngược hoặc hoàn tiền do nhiều lý do khác nhau, chẳng hạn như yêu cầu của khách hàng, vấn đề thanh toán, hoặc bất kỳ chính sách hoàn tiền áp dụng nào khác. #### Refund rate \{#refund-rate\} Tỷ lệ hoàn tiền được tính bằng cách chia số lần hoàn tiền trong các placement cụ thể cho số lần mua lần đầu (không bao gồm gia hạn). Ví dụ: nếu có 5 lần hoàn tiền và 1.000 lần mua lần đầu, tỷ lệ hoàn tiền sẽ là 0,5%. #### Views \{#views\} Chỉ số views đại diện cho tổng số lần paywall trong các placement cụ thể đã được người dùng xem. Mỗi lần người dùng truy cập paywall trong các placement đó, nó được tính là một lượt xem riêng biệt. Theo dõi views giúp bạn hiểu mức độ tương tác và tương tác của người dùng với paywall, cung cấp thông tin về hành vi người dùng và hiệu quả của vị trí và thiết kế paywall trong các khu vực cụ thể của ứng dụng. #### Unique views \{#unique-views\} Chỉ số unique views đại diện cho số lần duy nhất mà paywall trong các placement cụ thể đã được người dùng xem. Không giống như tổng views tính mỗi lần truy cập là một lượt xem riêng, unique views chỉ tính một lần truy cập của mỗi người dùng vào paywall trong các placement đó, bất kể họ truy cập bao nhiêu lần. Theo dõi unique views giúp cung cấp thước đo chính xác hơn về mức độ tương tác của người dùng và phạm vi tiếp cận của paywall trong các placement cụ thể, vì nó tập trung vào người dùng cá nhân thay vì tổng số lần truy cập. #### Completions & unique completions \{#completions--unique-completions\} Chỉ dành cho placement onboarding. Completions đếm số lần người dùng hoàn thành placement onboarding, tức là họ đi từ màn hình đầu tiên đến màn hình cuối cùng. Nếu ai đó hoàn thành hai lần, đó là hai **completions** nhưng chỉ một **unique completion**. #### Unique completions rate \{#unique-completions-rate\} Chỉ dành cho placement onboarding. Số unique completion chia cho số unique view. Chỉ số này giúp bạn hiểu cách mọi người tương tác với placement onboarding và thực hiện thay đổi nếu bạn nhận thấy mọi người bỏ qua nó. --- # File: create-paywall --- --- title: "Tạo paywall" description: "Tìm hiểu cách tạo paywall có tỷ lệ chuyển đổi cao bằng Paywall Builder của Adapty." --- [Paywall](paywalls) là một cấu hình trong Adapty xác định những 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 trong ứng dụng của bạn. Bạn cần có paywall bất kể bạn hiển thị nó như thế nào: - [**Paywall Builder**](adapty-paywall-builder): Thiết kế màn hình trong trình chỉnh sửa không cần code. Adapty sẽ hiển thị và xử lý các giao dịch mua. - **Paywall tùy chỉnh**: Tự triển khai giao diện của bạn và sử dụng cấu hình paywall để lấy sản phẩm. Sau khi tạo xong, hãy gán paywall vào một [placement](placements) — placement kiểm soát paywall nào người dùng sẽ thấy. Các sản phẩm trên paywall đang hoạt động sẽ được cố định, vì vậy các chỉ số của nó luôn phản ánh cùng một tổ hợp sản phẩm, giúp bạn so sánh hiệu suất giữa các sản phẩm và mức giá khác nhau. :::tip Bạn cũng có thể tạo paywall theo cách lập trình bằng [Developer CLI](developer-cli-reference#adapty-paywalls-create). ::: <details> <summary>Trước khi bắt đầu tạo paywall (Nhấp để mở rộng)</summary> 1. [Tạo ít nhất một sản phẩm](create-product). 2. (tùy chọn) [Tạo ưu đãi](create-offer). </details> ## Tạo paywall \{#create-paywall\} Để tạo paywall mới trong Adapty dashboard: 1. Vào [**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty. Trang này hiển thị tổng quan tất cả các paywall và chỉ số của chúng. 2. Nhấp vào **Create paywall**. 3. Trên trang **Paywalls / New paywall**, nhập **Paywall name** để xác định paywall này trong toàn bộ Adapty Dashboard. 4. Nhấp vào **Add product**. 5. Chọn các sản phẩm sẽ hiển thị cho khách hàng của bạn. :::note - Thứ tự sản phẩm trong danh sách này sẽ được duy trì trong SDK, vì vậy hãy sắp xếp sản phẩm theo thứ tự bạn mong muốn. - Sau khi paywall đã được hiển thị trong môi trường production, bạn sẽ không thể thay đổi sản phẩm trên đó, vì điều này có thể ảnh hưởng đến các chỉ số của paywall. ::: 6. Nếu bạn đang cung cấp dùng thử miễn phí hoặc các ưu đãi khác cho sản phẩm, hãy thêm chúng vào đây, nếu không chúng sẽ không khả dụng. Chọn ưu đãi bạn đã [tạo trước đó](create-offer) cho sản phẩm này từ danh sách **Offer**. Danh sách này chỉ khả dụng cho các sản phẩm có ưu đãi. 7. Nhấp vào **Create as a draft** để xác nhận tạo paywall. Paywall của bạn đã được tạo! <img src="/assets/shared/img/create-paywall.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '900px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Các bước tiếp theo \{#next-steps\} Sau khi đã tạo paywall đầu tiên: 1. Thêm nó vào một [placement](placements). ID của placement sẽ là những thứ duy nhất được hardcode. Bạn sẽ dùng chúng để lấy sản phẩm cần bán. 2. Cách bạn làm việc với paywall tiếp theo phụ thuộc vào cách triển khai của bạn: - Nếu bạn muốn dùng [Adapty Paywall Builder](adapty-paywall-builder), hãy thiết kế paywall trong trình chỉnh sửa không cần code. Adapty sẽ hiển thị paywall và xử lý logic mua hàng, trong khi bạn chỉ cần hiển thị paywall trong code ứng dụng. - Nếu bạn có paywall tùy chỉnh muốn sử dụng, hãy xem hướng dẫn triển khai in-app purchase với Adapty cho nền tảng của bạn: - [iOS](ios-implement-paywalls-manually) - [Android](android-implement-paywalls-manually) - [React Native](react-native-implement-paywalls-manually) - [Flutter](flutter-implement-paywalls-manually) - [Unity](unity-implement-paywalls-manually) - [Kotlin Multiplatform](kmp-implement-paywalls-manually) --- # File: customize-paywall-with-remote-config --- --- title: "Thiết kế paywall với Remote Config" description: "Tùy chỉnh paywall của bạn với Remote Config trong Adapty để nhắm mục tiêu tốt hơn." --- :::important Hướng dẫn này đề cập đến Remote Config cho các paywall thông thường. Đối với Flow Builder, xem [Tùy chỉnh flow với remote config](customize-flow-with-remote-config). ::: Paywall Remote Config là một công cụ mạnh mẽ cung cấp các tùy chọn cấu hình linh hoạt. Nó cho phép sử dụng các JSON payload tùy chỉnh để điều chỉnh paywall của bạn một cách chính xác. Với nó, bạn có thể định nghĩa các tham số khác nhau như tiêu đề, hình ảnh, phông chữ, màu sắc và nhiều hơn nữa. <details> <summary>Trước khi bắt đầu tùy chỉnh paywall (Nhấn để mở rộng)</summary> 1. [Tạo sản phẩm](create-product). 2. [Tạo paywall và thêm sản phẩm vào đó](create-paywall). </details> Để bắt đầu tùy chỉnh paywall bằng Remote Config: 1. Mở mục [**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty. 2. Nhấn vào paywall để mở nó. <img src="/assets/shared/img/remote-config.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Chuyển sang tab **Remote config**. <img src="/assets/shared/img/remote-config-3.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Remote Config có 2 chế độ xem: - [Bảng](customize-paywall-with-remote-config#table-view-of-the-remote-config) - [JSON](customize-paywall-with-remote-config#json-view-of-the-remote-config) Cả chế độ xem **Bảng** và **JSON** đều bao gồm các thành phần cấu hình giống nhau. Sự khác biệt duy nhất là theo sở thích cá nhân, với điểm khác biệt duy nhất là chế độ xem bảng cung cấp menu ngữ cảnh, có thể hữu ích khi sửa lỗi bản địa hóa. Bạn có thể chuyển đổi giữa các chế độ xem bằng cách nhấn vào tab **Bảng** hoặc **JSON** bất cứ khi nào cần. Dù bạn chọn chế độ xem nào để tùy chỉnh paywall, bạn vẫn có thể truy cập dữ liệu này từ SDK sau đó thông qua thuộc tính `remoteConfig` hoặc `remoteConfigString` của `AdaptyPaywall`, và thực hiện một số điều chỉnh cho paywall của bạn. Bạn cũng có thể cập nhật các giá trị Remote Config theo lập trình thông qua [server-side API](api-adapty/operations/updatePaywall) để thay đổi cấu hình paywall một cách linh động mà không cần cập nhật thủ công trên dashboard. Dưới đây là một số ví dụ về cách bạn có thể sử dụng Remote Config. <Tabs groupId="current-os" queryString> <TabItem value="Titles" label="Tiêu đề" default> ```json showLineNumbers { "screen_title": "Today only: Subscribe, and get 7 days for free!" } # Test titles or others texts ``` </TabItem> <TabItem value="Images" label="Hình ảnh" default> ```json showLineNumbers { "background_image": "https://adapty.io/media/paywalls/bg1.webp" } # Test images on your paywall ``` </TabItem> <TabItem value="Fonts" label="Phông chữ" default> ```json showLineNumbers { "font_family": "San Francisco", "font_size": 16 } # Test fonts ``` </TabItem> <TabItem value="Color" label="Màu sắc" default> ```json showLineNumbers { "subscribe_button_color": "purple" } # Test colors of buttons, texts etc. ``` </TabItem> <TabItem value="HTML" label="HTML" default> ```json showLineNumbers { "photo_gallery": "https://adapty.io/media/paywalls/link-to-html-snippet.html" } # Any HTML code that can be displayed on the paywall ``` </TabItem> <TabItem value="Soft/Hard Paywall" label="Soft/Hard Paywall" default> ```json showLineNumbers { "hard_paywall": true } # By setting it to true, you disalow skipping paywall without subscribing # You have to handle this logic in your app ``` </TabItem> <TabItem value="Translations" label="Bản dịch" default> ```json showLineNumbers { "title": { "en": "Try for free!", "es": "¡Prueba gratis!", "ru": "Попробуй бесплатно!" } } ``` </TabItem> </Tabs> Bạn có thể kết hợp các tùy chọn khác nhau và tự tạo ra của riêng mình. Bằng cách này, bạn có thể thử nghiệm các tiêu đề, văn bản, hình ảnh, phông chữ, màu sắc khác nhau và nhiều hơn nữa. ### Chế độ xem JSON của Remote Config \{#json-view-of-the-remote-config\} Trong chế độ xem **JSON** của Remote Config, bạn có thể nhập bất kỳ dữ liệu nào theo định dạng JSON: <img src="/assets/shared/img/3356ff5-remote_config_JSON.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Chế độ xem Bảng của Remote Config \{#table-view-of-the-remote-config\} Nếu bạn không quen làm việc với code và cần sửa một số giá trị trong JSON, Adapty có chế độ xem **Bảng** dành cho bạn. <img src="/assets/shared/img/4c27b2f-remote_config_table.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đây là bản sao JSON của bạn ở định dạng bảng dễ đọc và hiểu. Mã màu giúp nhận biết các kiểu dữ liệu khác nhau. Để thêm một key, nhấn nút **Add row**. Chúng tôi tự động kiểm tra việc ánh xạ giá trị và kiểu dữ liệu, đồng thời hiển thị cảnh báo nếu các chỉnh sửa của bạn có thể dẫn đến JSON không hợp lệ. <img src="/assets/shared/img/ef682d8-add_raw.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Các tùy chọn hàng bổ sung chủ yếu hữu ích cho [bản địa hóa paywall](add-remote-config-locale): <img src="/assets/shared/img/17bcf80-remote_config_table_options.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bây giờ đã đến lúc [tạo một placement](create-placement) và thêm paywall vào đó. Sau đó, bạn có thể <InlineTooltip tooltip="hiển thị các paywall Remote Config của bạn">[iOS](present-remote-config-paywalls), [Android](present-remote-config-paywalls-android), [React Native](present-remote-config-paywalls-react-native), [Flutter](present-remote-config-paywalls-flutter), và [Unity](present-remote-config-paywalls-unity)</InlineTooltip> trong ứng dụng di động của bạn. --- # File: add-paywall-locale-in-adapty-paywall-builder --- --- title: "Thêm ngôn ngữ trong Flow Builder" description: "Thêm nội dung đã được bản địa hóa trong Flow Builder của Adapty để tiếp cận người dùng trên toàn thế giới bằng ngôn ngữ của họ." --- Bản địa hóa flow giúp flow của bạn hiển thị được bằng nhiều ngôn ngữ khác nhau. Trong Flow Builder, việc bản địa hóa được tổ chức theo từng màn hình, mỗi màn hình hiển thị tỷ lệ phần trăm hoàn thành để theo dõi tiến trình dịch thuật. :::tip Hãy hoàn thiện flow của bạn ở ngôn ngữ mặc định trước khi thêm các ngôn ngữ khác. ::: ## Thêm và thiết lập bản địa hóa \{#add-and-set-up-localization\} 1. Trong bảng điều khiển bên trái, nhấp vào Localizations. Sau đó nhấp vào **Add locale**. Chọn các ngôn ngữ muốn thêm. 2. Mỗi ngôn ngữ được thêm vào sẽ xuất hiện dưới dạng một cột trong bảng bản địa hóa, được điền sẵn các giá trị từ ngôn ngữ mặc định. 3. Để chỉ tập trung vào những phần còn thiếu, bật nút chuyển **Missing only** ở bảng điều khiển bên trái. Bảng sẽ lọc và chỉ hiển thị các hàng chưa được dịch. ## Xuất và nhập cho dịch thuật bên ngoài \{#export-and-import-for-external-translation\} Bạn có thể xuất file bản địa hóa để chia sẻ với người dịch và nhập lại kết quả sau khi dịch xong. Trong thanh công cụ trên cùng, nhấp vào **Import / Export**. ### Định dạng file xuất \{#export-file-format\} Khi xuất, bạn sẽ nhận được file `.tsv` (phân tách bằng tab) với mỗi hàng tương ứng một phần tử có thể dịch. Các cột bao gồm: | Cột | Mô tả | |--------|-------------| | `Screen` | Màn hình chứa phần tử đó (ví dụ: `Welcome`, `Quiz`) | | `Element` | Định danh phần tử được tạo tự động trong màn hình đó. Bạn có thể thay đổi trong **Interactions** > **Element ID**. | | `Property` | Loại thuộc tính (ví dụ: `content`) | | `[default_locale]` | Mã ngôn ngữ mặc định (ví dụ: `en`) | | `[locale]` | Một cột cho mỗi ngôn ngữ đã thêm (ví dụ: `fr`, `es`) | Ví dụ: ``` Screen Element Property en fr es Welcome title content Turn words into art Transformez les mots en art Welcome subtitle content Create stunning images in seconds with AI Créez des images en quelques secondes Quiz quiz-title content What will you create? ``` :::note Để trống các cột ngôn ngữ với những dòng chưa dịch — Adapty sẽ coi chúng là còn thiếu. ::: ### Yêu cầu đối với file nhập \{#import-file-requirements\} - **Định dạng**: `.tsv` (giá trị phân tách bằng tab) - **Header**: phải bao gồm `Screen`, `Element`, `Property` và ít nhất một cột ngôn ngữ - **Tên cột ngôn ngữ**: phải khớp với mã ngôn ngữ của các ngôn ngữ đã được thêm vào flow. Nhập file có mã ngôn ngữ chưa có trong flow sẽ dẫn đến lỗi. - **Nhập một phần**: bạn có thể chỉ đưa vào một tập hợp con các hàng; các hàng không có trong file sẽ giữ nguyên giá trị hiện tại ## Dịch thủ công \{#translate-manually\} Bạn cũng có thể nhập trực tiếp bản dịch vào bất kỳ ô nào trong bảng bản địa hóa. Để quản lý một hàng cụ thể, mở menu ngữ cảnh của hàng đó (**⋮**): - **Reset to default**: Khôi phục bản dịch của hàng về giá trị ngôn ngữ mặc định. ## Xem trước bản địa hóa \{#preview-the-localization\} Để kiểm tra các bản dịch, hãy chuyển ngôn ngữ đang hoạt động trong Flow Builder và xem lại từng màn hình. --- # File: add-remote-config-locale --- --- title: "Bản địa hóa paywall bằng Remote Config" description: "Thêm ngôn ngữ vào Remote Config để cá nhân hóa paywall trong Adapty." --- Việc điều chỉnh paywall cho phù hợp với từng ngôn ngữ là điều cần thiết trong một thế giới đa văn hóa. Bản địa hóa giúp bạn tạo ra trải nghiệm phù hợp với người dùng ở từng khu vực. Với mỗi paywall, bạn có thể thêm các phiên bản bằng nhiều ngôn ngữ khác nhau, đảm bảo sản phẩm của bạn tiếp cận được khán giả địa phương. Nếu bạn không sử dụng Paywall Builder của Adapty để thiết kế paywall, bạn vẫn có thể bản địa hóa paywall tùy chỉnh và quản lý các bản dịch mà không cần triển khai lại ứng dụng: 1. Bạn tạo một Remote Config với các biến trong Adapty Dashboard. Các biến có thể đại diện cho văn bản, nội dung đa phương tiện hoặc các loại nội dung khác. 2. Bạn thiết lập giá trị cho từng biến theo từng ngôn ngữ. 3. Bạn xử lý các biến trong code ứng dụng. 4. Khi lấy paywall cùng với sản phẩm và truyền vào một mã ngôn ngữ, bạn sẽ nhận được các giá trị biến tương ứng. Nhờ vậy, các bản địa hóa không bị hardcode trong code ứng dụng, và bạn có thể điều chỉnh chúng bất cứ lúc nào. Dù ở chế độ xem bảng hay định dạng JSON, bạn đều có thể dễ dàng điều chỉnh cài đặt cho từng ngôn ngữ. Ví dụ: dịch các khóa chuỗi, bật/tắt giá trị Boolean (ví dụ: `TRUE` cho tiếng Anh, `FALSE` cho tiếng Ý), hoặc thậm chí thay đổi ảnh nền. ## Thiết lập bản địa hóa cho paywall dùng Remote Config \{#set-up-localization-for-remote-configured-paywalls\} 1. Vào mục [**Paywalls**](https://app.adapty.io/paywalls) trong Adapty. 2. Nhấp vào paywall để mở. 3. Chuyển sang tab **Remote config**. <img src="/assets/shared/img/switch_to_remote_config.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấp vào **Locales** và chọn các ngôn ngữ bạn muốn hỗ trợ. Lưu thay đổi để thêm các ngôn ngữ này vào paywall. <img src="/assets/shared/img/add_locale.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bây giờ, bạn có thể dịch nội dung thủ công, dùng AI, hoặc xuất file bản địa hóa để gửi cho dịch giả bên ngoài. ## Dịch paywall bằng AI \{#translate-paywalls-with-ai\} Dịch thuật bằng AI là cách nhanh chóng và hiệu quả để bản địa hóa paywall. Bạn có thể dịch cả giá trị **String** và **List**. Theo mặc định, tất cả các dòng đều được chọn (được tô màu tím). Các dòng đã được dịch sẽ được đánh dấu màu xanh lá và mặc định sẽ không được đưa vào lần dịch mới. Các dòng chưa được chọn hoặc chưa dịch sẽ hiển thị màu xám. <img src="/assets/shared/img/localization-table.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <img src="/assets/shared/img/localization-json.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 1. Chọn các dòng cần dịch. Bạn nên bỏ chọn các dòng chứa ID, URL và biến để tránh AI dịch nhầm chúng. 2. Chọn các ngôn ngữ cần dịch. <img src="/assets/shared/img/localization-table-language.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp **AI Translate** để áp dụng bản dịch. Các dòng được chọn sẽ được dịch và thêm vào paywall, với các dòng đã dịch được đánh dấu màu xanh lá. ## Xuất file bản địa hóa để dịch thuật bên ngoài \{#exporting-localization-files-for-external-translation\} Mặc dù bản địa hóa bằng AI đang trở thành xu hướng phổ biến, bạn có thể muốn một phương pháp đáng tin cậy hơn, chẳng hạn như sử dụng dịch giả chuyên nghiệp hoặc công ty dịch thuật có uy tín. Trong trường hợp đó, bạn có thể xuất file bản địa hóa để chia sẻ với dịch giả, sau đó nhập lại kết quả đã dịch vào Adapty. Nhấp nút **Export** sẽ tạo các file `.json` riêng lẻ cho từng ngôn ngữ, được đóng gói thành một file nén duy nhất. Nếu bạn chỉ cần một file, bạn có thể xuất trực tiếp từ menu của ngôn ngữ đó. <img src="/assets/shared/img/localization-single-export.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi nhận được các file đã dịch, dùng nút **Import** để tải lên toàn bộ hoặc từng file một. Adapty sẽ tự động kiểm tra các file để đảm bảo chúng đúng định dạng. ### Định dạng file nhập \{#import-file-format\} Để đảm bảo nhập thành công, file nhập phải đáp ứng các yêu cầu sau: - **Tên file và phần mở rộng:** Tên file phải khớp với ngôn ngữ mà nó đại diện và có phần mở rộng `.json`. Bạn có thể kiểm tra và sao chép tên ngôn ngữ trong Adapty Dashboard. Nếu tên không được nhận dạng, quá trình nhập sẽ thất bại. <img src="/assets/shared/img/locale-name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> - **JSON hợp lệ:** File phải là JSON hợp lệ. Nếu không, quá trình nhập sẽ thất bại. ## Bản địa hóa thủ công \{#manual-localization\} Đôi khi, bạn có thể muốn chỉnh sửa bản dịch, thêm hình ảnh khác nhau cho từng ngôn ngữ, hoặc thậm chí điều chỉnh trực tiếp cấu hình Remote Config. 1. Chọn phần tử bạn muốn dịch và nhập giá trị mới. Bạn có thể cập nhật cả giá trị **String** và **List**, hoặc thay thế hình ảnh bằng những hình phù hợp hơn với ngôn ngữ đó. <img src="/assets/shared/img/032b429-remote_config_localization.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Tận dụng menu ngữ cảnh trong ngôn ngữ tiếng Anh để xử lý các vấn đề bản địa hóa một cách hiệu quả: - **Copy this value to all locales**: Ghi đè mọi thay đổi đã thực hiện trong các ngôn ngữ không phải tiếng Anh cho hàng đã chọn, thay thế bằng giá trị từ ngôn ngữ tiếng Anh. - **Revert all row changes to original values**: Hủy bỏ mọi thay đổi đã thực hiện trong phiên hiện tại và khôi phục các giá trị về trạng thái đã lưu lần cuối. <img src="/assets/shared/img/d7e70f1-remote_confi_loc_table_options.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi thêm ngôn ngữ vào paywall, hãy đảm bảo bạn triển khai mã ngôn ngữ đúng cách trong code ứng dụng. Xem <InlineTooltip tooltip="hướng dẫn sử dụng bản địa hóa và mã ngôn ngữ trong ứng dụng">[iOS](localizations-and-locale-codes), [Android](android-localizations-and-locale-codes)</InlineTooltip> --- # File: web-paywall-configuration --- --- title: "Cấu hình web paywall" --- Sau khi nhấn **Create web paywall** trên trang **Web paywall**, bạn sẽ được chuyển đến một trang riêng để thiết lập thiết kế web paywall và phương thức thanh toán. ## Thiết lập phương thức thanh toán \{#set-up-a-payment-method\} Trước tiên, bạn cần kết nối một nhà cung cấp thanh toán để xử lý các giao dịch mua. Các tùy chọn hiện có bao gồm: - Stripe - Paddle - Paypal - Solidgate :::important Để đảm bảo theo dõi analytics web paywall chính xác trong Adapty, bạn cần [thêm sản phẩm của mình](product) cùng với ID sản phẩm Stripe/Paddle/nhà cung cấp thanh toán tương ứng vào Adapty. ::: Để thiết lập nhà cung cấp thanh toán: 1. Trên trang danh sách web paywall, nhấn **Settings** và chuyển sang tab **Integrations**. 2. Chọn nhà cung cấp thanh toán và làm theo hướng dẫn tích hợp trên màn hình. <img src="/assets/shared/img/web-paywall-configuration-1.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. ⚠️ Nếu bạn chọn Stripe, hãy đảm bảo bạn đang dùng các khóa từ môi trường **Test Mode** dù giao diện hiển thị là **Sandbox**. Nếu không, web paywall của bạn sẽ không hoạt động. **Sandboxes** trong Stripe chưa được hỗ trợ. <img src="/assets/shared/img/web-paywall-configuration-stripe.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Thiết lập xác minh domain Apple Pay \{#set-up-apple-pay-domain-verification\} Trong **Settings > Domains**, chọn nhà cung cấp thanh toán chính để sử dụng cho việc xác minh domain. Sau đó, xác minh các domain paywall của bạn với nhà cung cấp tương ứng: **Stripe**: 1. Truy cập [Payment method domain settings](https://dashboard.stripe.com/settings/payment_method_domains) và nhấn **Add a new domain**. 2. Thêm `app.funnelfox.com` và subdomain paywall cá nhân của bạn (có dạng `paywalls-....fnlfx.com`). Để tìm subdomain của bạn, vào **Settings > Domains** và sao chép giá trị **Hosted subdomain**. **Paddle**: 1. Trong Paddle console, vào **Checkout > Website approval** và nhấn **Add a new domain**. 2. Thêm `app.funnelfox.com` và subdomain paywall cá nhân của bạn (có dạng `paywalls-....fnlfx.com`). Để tìm subdomain của bạn, vào **Settings > Domains** và sao chép giá trị **Hosted subdomain**. Quá trình phê duyệt trong Paddle là thủ công, vì vậy bạn cần đợi cho đến khi domain chuyển từ trạng thái `Pending` sang `Approved`. **FunnelFox Billing**: Làm theo [hướng dẫn tích hợp FunnelFox Billing](https://funnelfox.com/docs/billing/integration-billing-funnelfox). **SolidGate**: 1. Trong Solidgate Dashboard của bạn, vào **Developers > Apple Pay Domains**. 2. Nhấn **+ Add new domain** và dán domain dự án của bạn (từ **Settings > Domains** trong FunnelFox). Thêm custom domain của bạn nếu có. 3. Để sử dụng Apple Pay ở chế độ preview, cũng cần thêm `http://app.funnelfox.com/`. ## Tạo và cấu hình web paywall \{#create-and-configure-a-web-paywall\} 1. Trên trang danh sách web paywall, nhấn **Create a paywall**. 2. Nhập tên paywall và nhấn **Create**. <img src="/assets/shared/img/web-paywall-configuration-2.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Bạn sẽ được chuyển đến một template cơ bản với hai tùy chọn gói đăng ký và nút mua hàng Apple Pay. Màn hình đầu tiên liệt kê các gói đăng ký. Màn hình thứ hai và thứ ba là màn hình thanh toán. Mỗi màn hình tương ứng với một gói bạn cung cấp. Nếu bạn chỉ có một gói, hãy xóa màn hình thừa. Nếu có nhiều hơn, bạn cần nhân đôi các màn hình thanh toán. Màn hình cuối cùng mà người dùng thấy sau khi mua thành công là nơi bạn cần chỉ rõ rằng họ có thể quay lại ứng dụng của bạn. <img src="/assets/shared/img/web-paywall-configuration-10.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Thiết lập danh sách gói: thêm hoặc xóa các gói và mức giá. Tất cả các mức giá và gói hiển thị trên màn hình không được thêm tự động, vì vậy bạn cần cấu hình thủ công. <img src="/assets/shared/img/web-paywall-configuration-8.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Thêm hoặc cấu hình màn hình thanh toán cho từng gói bạn có. Chúng tôi khuyến nghị thêm tổng số tiền vào mỗi màn hình thanh toán để người dùng biết họ cần thanh toán bao nhiêu trước khi nhấn nút mua. 6. Trên các màn hình thanh toán, bạn đã có sẵn nút Apple Pay. Để nút này hoạt động, trên mỗi màn hình, hãy cấu hình: 1. **Product type**: Chọn xem bạn muốn thêm thời gian dùng thử hay giảm giá. 2. **Trial period**: Nhập thời gian dùng thử. 3. **Product**: Chọn sản phẩm của bạn từ nhà cung cấp thanh toán. :::important Hãy đảm bảo sản phẩm đã được thêm vào Adapty. Nếu không, kết quả mua hàng sẽ được đặt về mặc định. ::: 4. **Subscription discount**: Tùy chọn, chọn một coupon từ nhà cung cấp thanh toán của bạn. <img src="/assets/shared/img/web-paywall-configuration-6.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Bây giờ, bạn cần liên kết các gói với màn hình thanh toán. Trên màn hình chọn gói, nhấn nút **Continue** và chọn màn hình đích cho từng gói. <img src="/assets/shared/img/web-paywall-configuration-9.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Khi bạn đã hoàn thiện paywall, bạn cần lấy link của nó để kích hoạt paywall này trong Adapty. Cách lấy link phụ thuộc vào việc bạn đang kiểm thử hay triển khai lên môi trường production: 1. **Để kiểm thử sandbox**: Nhấn **Preview** ở góc trên bên phải và sao chép link. 2. **Để triển khai production**: Nhấn **Publish** ở góc trên bên phải. Nhấn **Home** và sao chép link từ cột **URL**. <img src="/assets/shared/img/web-paywall-configuration-11.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Vậy là xong! Dùng link này để [tiếp tục thiết lập](web-paywall#step-2-trigger-the-paywall). --- # File: fallback-paywalls --- --- title: "Fallback paywalls" description: "Sử dụng paywall dự phòng để đảm bảo trải nghiệm người dùng liền mạch trong Adapty." --- Để duy trì trải nghiệm người dùng mượt mà, bạn cần thiết lập **phiên bản dự phòng** cho các [paywall](paywalls) và [onboarding](onboardings) của mình. Khi ứng dụng tải một paywall, Adapty SDK sẽ yêu cầu dữ liệu cấu hình paywall từ máy chủ của chúng tôi. Nhưng nếu thiết bị không thể kết nối tới Adapty do sự cố mạng hoặc máy chủ bị gián đoạn thì sao? * Nếu người dùng đã truy cập paywall trước đó và thiết bị đã lưu dữ liệu vào bộ nhớ đệm, ứng dụng sẽ tải dữ liệu paywall **từ bộ nhớ đệm**. * Nếu thiết bị chưa lưu paywall vào bộ nhớ đệm, ứng dụng sẽ tìm kiếm tệp cấu hình được lưu cục bộ. Điều này cho phép ứng dụng hiển thị paywall mà không gặp lỗi. Adapty tự động tạo các tệp cấu hình dự phòng để bạn tải về và sử dụng. Mỗi tệp chứa cấu hình dành riêng cho từng nền tảng, bao gồm *tất cả* các placement của bạn. ## Bắt đầu \{#get-started\} 1. [Tải tệp cấu hình dự phòng](/local-fallback-paywalls) từ Adapty. 2. Sử dụng Adapty SDK để cấu hình paywall dự phòng của bạn: * [iOS](ios-use-fallback-paywalls) * [Android](android-use-fallback-paywalls) * [React Native](react-native-use-fallback-paywalls) * [Flutter](flutter-use-fallback-paywalls) * [Unity](unity-use-fallback-paywalls) * [Kotlin Multiplatform](kmp-use-fallback-paywalls) * [Capacitor](capacitor-use-fallback-paywalls) ## Giới hạn \{#limitations\} Paywall dự phòng được mã hóa cứng và lưu trữ cục bộ, nên chúng không có các tính năng động như paywall thông thường của Adapty. * Paywall dự phòng không hỗ trợ [đa ngôn ngữ](paywall-localization). Khi Adapty tạo tệp cấu hình, nó sử dụng ngôn ngữ mặc định `en`. * Mỗi placement chỉ có thể có một paywall dự phòng. Nếu thiết lập của bạn bao gồm các cấu hình paywall khác nhau cho các [đối tượng](audience) khác nhau, Adapty sẽ sử dụng cấu hình dành cho "All users". * Paywall dự phòng không hỗ trợ [A/B test](ab-tests). Nếu một paywall đang tham gia A/B test, tệp cấu hình dự phòng của nó sẽ bao gồm biến thể có trọng số cao nhất. * Paywall dự phòng không thể được [quản lý từ xa](customize-paywall-with-remote-config). Nếu bạn muốn cập nhật tệp cấu hình, bạn cần phát hành phiên bản mới của ứng dụng trên App Store / Google Play. --- # File: local-fallback-paywalls --- --- title: "Tải xuống paywall dự phòng" description: "Sử dụng paywall dự phòng cục bộ trong Adapty để đảm bảo luồng đăng ký không bị gián đoạn." --- Adapty tự động tạo các file cấu hình JSON cho [paywall dự phòng](/fallback-paywalls) của bạn, mỗi nền tảng một file. Các file này cũng chứa dữ liệu dự phòng cho các onboarding của bạn. Nếu một placement có nhiều hơn một paywall hoặc onboarding, phiên bản dự phòng sẽ bao gồm biến thể có trọng số cao nhất, hoặc đối tượng rộng nhất. Adapty cập nhật các file này bất cứ khi nào bạn chỉnh sửa paywall hoặc onboarding. Làm theo các bước dưới đây để tải xuống cấu hình dự phòng của bạn: 1. Mở trang **[Placements](https://app.adapty.io/placements)**. 2. Nhấn nút **Fallbacks**. 3. Chọn nền tảng mục tiêu (*iOS* hoặc *Android*) từ menu thả xuống. 4. Chọn phiên bản SDK để bắt đầu tải xuống. <img src="/assets/shared/img/9c63367-placements.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sau khi tải xuống \{#after-the-download\} Làm theo hướng dẫn cài đặt cho nền tảng của bạn: * [iOS](ios-use-fallback-paywalls) * [Android](android-use-fallback-paywalls) * [React Native](react-native-use-fallback-paywalls) * [Flutter](flutter-use-fallback-paywalls) * [Unity](unity-use-fallback-paywalls) * [Kotlin Multiplatform](kmp-use-fallback-paywalls) * [Capacitor](capacitor-use-fallback-paywalls) --- # File: paywall-metrics --- --- title: "Chỉ số paywall" description: "Theo dõi và phân tích các chỉ số hiệu suất paywall để cải thiện doanh thu gói đăng ký." --- Adapty thu thập một loạt các chỉ số để giúp bạn đo lường hiệu suất của các paywall tốt hơn. Tất cả các chỉ số được cập nhật theo thời gian thực, ngoại trừ lượt xem được cập nhật vài phút một lần. Tất cả các chỉ số, ngoại trừ lượt xem, được gán cho sản phẩm trong paywall. Tài liệu này mô tả các chỉ số hiện có, định nghĩa và cách tính của chúng. Các chỉ số paywall được hiển thị trong danh sách paywall, cung cấp cho bạn cái nhìn tổng quan về hiệu suất của tất cả các paywall. Giao diện tổng hợp này trình bày các chỉ số được tổng hợp cho từng paywall, giúp bạn đánh giá hiệu quả và xác định những điểm cần cải thiện. Để phân tích chi tiết hơn từng paywall, bạn có thể truy cập vào trang chỉ số chi tiết của paywall. Trong phần này, bạn sẽ thấy các chỉ số toàn diện dành riêng cho paywall đã chọn, giúp bạn hiểu sâu hơn về hiệu suất của nó. ### Lọc chỉ số theo ngày cài đặt \{#filter-metrics-by-install-date\} Các chỉ số về paywall, trial và mua hàng có thể được nhóm theo hai loại ngày khác nhau: - **Ngày sự kiện** — khi paywall được xem, trial bắt đầu, hoặc giao dịch mua xảy ra. - **Ngày cài đặt** — khi người dùng lần đầu mở ứng dụng. Hai chế độ xem này có thể hiển thị các con số rất khác nhau cho cùng một khoảng thời gian. Hộp kiểm **Filter metrics by install date** kiểm soát chế độ nào được dashboard sử dụng: - **Bỏ chọn (mặc định)**: Các chỉ số được nhóm theo ngày sự kiện. - **Đã chọn**: Các chỉ số được nhóm theo ngày cài đặt. **Ví dụ.** Bạn đặt khoảng thời gian từ ngày 1–30 tháng 4 và xem các trial. - **Bỏ chọn**: Hiển thị các trial *bắt đầu* trong tháng 4, bất kể người dùng đó cài đặt ứng dụng khi nào. - **Đã chọn**: Hiển thị các trial từ những người dùng *đã cài đặt* trong tháng 4, bất kể trial của họ bắt đầu khi nào. Dùng chế độ xem theo ngày cài đặt để đo hiệu quả thu hút người dùng cho một cohort cụ thể. Dùng chế độ xem theo ngày sự kiện để đo hoạt động paywall hoặc onboarding trong một khoảng thời gian cụ thể. ### Điều khiển chỉ số \{#metrics-controls\} Hệ thống hiển thị các chỉ số dựa trên khoảng thời gian được chọn và sắp xếp chúng theo tham số cột bên trái với ba cấp độ thụt lề. Đối với paywall đang hoạt động (Live), các chỉ số bao gồm khoảng thời gian từ ngày bắt đầu của paywall đến ngày hiện tại. Đối với các paywall không hoạt động, các chỉ số bao gồm toàn bộ khoảng thời gian từ ngày bắt đầu đến cuối khoảng thời gian được chọn. Các paywall ở trạng thái Draft và Archived được đưa vào bảng chỉ số, nhưng nếu không có dữ liệu cho những paywall đó, chúng sẽ được liệt kê mà không hiển thị chỉ số nào. #### Tùy chọn hiển thị dữ liệu chỉ số \{#view-options-for-metrics-data\} Trang paywall cung cấp hai tùy chọn hiển thị dữ liệu chỉ số: theo placement và theo đối tượng. Trong chế độ xem theo placement, các chỉ số được nhóm theo các placement liên kết với paywall. Điều này cho phép người dùng phân tích chỉ số theo từng placement khác nhau. Trong chế độ xem theo đối tượng, các chỉ số được nhóm theo đối tượng mục tiêu của paywall. Người dùng có thể đánh giá các chỉ số dành riêng cho từng phân khúc đối tượng khác nhau. Bạn có thể chọn chế độ xem ưa thích bằng tùy chọn dropdown ở đầu trang chi tiết paywall. #### Khoảng thời gian \{#time-ranges\} Bạn có thể chọn từ nhiều khoảng thời gian khác nhau để phân tích dữ liệu chỉ số, cho phép bạn tập trung vào các khoảng thời gian cụ thể như ngày, tuần, tháng hoặc phạm vi ngày tùy chỉnh. #### Bộ lọc và nhóm hiện có \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: Adapty cung cấp các công cụ mạnh mẽ để lọc và tùy chỉnh phân tích chỉ số theo nhu cầu của bạn. Với trang chỉ số của Adapty, bạn có quyền truy cập vào nhiều khoảng thời gian, tùy chọn nhóm và khả năng lọc đa dạng. - Lọc theo: Đối tượng, quốc gia, paywall, trạng thái paywall, nhóm paywall, placement, quốc gia, cửa hàng, sản phẩm và cửa hàng sản phẩm. - Nhóm theo: Sản phẩm và cửa hàng. #### Biểu đồ chỉ số đơn \{#single-metrics-chart\} Một trong những thành phần chính của trang chỉ số paywall là phần biểu đồ, trực quan hóa các chỉ số được chọn và giúp phân tích dễ dàng hơn. Phần biểu đồ trên trang chỉ số paywall bao gồm biểu đồ thanh ngang trực quan hóa các giá trị chỉ số được chọn. Mỗi thanh trong biểu đồ tương ứng với một giá trị chỉ số và có kích thước tỷ lệ thuận, giúp bạn dễ dàng hiểu dữ liệu ngay từ cái nhìn đầu tiên. Đường ngang biểu thị khung thời gian đang được phân tích, và cột dọc hiển thị các giá trị số của chỉ số. Tổng giá trị của tất cả các chỉ số được hiển thị bên cạnh biểu đồ. Ngoài ra, nhấp vào biểu tượng mũi tên ở góc trên bên phải của phần biểu đồ sẽ mở rộng chế độ xem, hiển thị các chỉ số đã chọn trên toàn bộ dòng biểu đồ. #### Tóm tắt tổng chỉ số \{#total-metrics-summary\} Bên cạnh biểu đồ chỉ số đơn, phần tóm tắt tổng chỉ số được hiển thị, cho thấy các giá trị tích lũy cho các chỉ số đã chọn tại một thời điểm cụ thể, với khả năng thay đổi chỉ số hiển thị bằng menu dropdown. ### Định nghĩa các chỉ số \{#metrics-definitions\} :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: #### Doanh thu \{#revenue\} Chỉ số này thể hiện tổng số tiền thu được bằng USD từ các lượt mua và gia hạn. Lưu ý rằng cách tính doanh thu không bao gồm phí hoa hồng của App Store / Play Store và được tính trước khi khấu trừ bất kỳ khoản phí nào. #### Số tiền thực nhận \{#proceeds\} Chỉ số này thể hiện số tiền thực tế mà chủ ứng dụng nhận được bằng USD từ các lượt mua và gia hạn sau khi khấu trừ phí hoa hồng áp dụng của App Store / Play Store. :::important Hãy thông báo cho Adapty nếu ứng dụng của bạn tham gia chương trình hoa hồng giảm. Để đảm bảo tính toán chính xác, hãy chỉ định trạng thái [Small Business Program](app-store-small-business-program) và [Reduced Service Fee program](google-reduced-service-fee) của bạn trong [cài đặt ứng dụng](general). ::: Chỉ số này phản ánh doanh thu thuần trực tiếp góp phần vào thu nhập của ứng dụng. Để biết thêm thông tin về cách tính proceeds, bạn có thể tham khảo [tài liệu](analytics-cohorts#revenue-vs-proceeds) của Adapty. #### ARPPU \{#arppu\} ARPPU là doanh thu trung bình trên mỗi người dùng trả tiền. Được tính bằng tổng doanh thu chia cho số người dùng trả tiền duy nhất. Ví dụ: $15.000 doanh thu / 1.000 người dùng trả tiền = $15 ARPPU. #### ARPAS \{#arpas\} Doanh thu trung bình trên mỗi người đăng ký đang hoạt động cho phép bạn đo lường doanh thu trung bình tạo ra trên mỗi người đăng ký đang hoạt động. Được tính bằng cách chia tổng doanh thu cho số người đăng ký đã kích hoạt bản dùng thử hoặc gói đăng ký. Ví dụ: nếu tổng doanh thu là $5.000 và có 1.000 người đăng ký, ARPAS sẽ là $5. Chỉ số này giúp đánh giá tiềm năng kiếm tiền trung bình trên mỗi người đăng ký. #### Tỷ lệ chuyển đổi (CR) duy nhất sang giao dịch mua \{#unique-conversion-rate-cr-to-purchases\} Tỷ lệ chuyển đổi duy nhất sang giao dịch mua được tính bằng cách chia số lượng giao dịch mua cho số lượt xem duy nhất. Ví dụ: nếu có 10 giao dịch mua và 100 lượt xem duy nhất, tỷ lệ chuyển đổi duy nhất sang giao dịch mua sẽ là 10%. Chỉ số này tập trung vào tỷ lệ giao dịch mua trên số lượt xem duy nhất, cung cấp thông tin về hiệu quả chuyển đổi khách truy cập duy nhất thành khách hàng trả tiền. #### CR to purchases \{#cr-to-purchases\} Tỷ lệ chuyển đổi sang giao dịch mua được tính bằng cách chia số lượng giao dịch mua cho tổng số lượt xem. Ví dụ: nếu có 10 giao dịch mua và 100 lượt xem, tỷ lệ chuyển đổi sang giao dịch mua sẽ là 10%. Chỉ số này cho biết tỷ lệ phần trăm lượt xem dẫn đến giao dịch mua, cung cấp thông tin về hiệu quả của paywall trong việc chuyển đổi người dùng thành khách hàng trả tiền. #### Unique CR to trials \{#unique-cr-to-trials\} Tỷ lệ chuyển đổi duy nhất sang bản dùng thử được tính bằng cách chia số lượng bản dùng thử được bắt đầu cho số lượt xem duy nhất. Ví dụ: nếu có 30 bản dùng thử được bắt đầu và 100 lượt xem duy nhất, tỷ lệ chuyển đổi duy nhất sang bản dùng thử sẽ là 30%. Chỉ số này đo lường tỷ lệ phần trăm lượt xem duy nhất dẫn đến kích hoạt bản dùng thử, cung cấp thông tin về hiệu quả của paywall trong việc chuyển đổi khách truy cập duy nhất thành người dùng dùng thử. #### Purchases \{#purchases\} Purchases (Giao dịch mua) biểu thị tổng cộng dồn của các loại giao dịch được thực hiện trên paywall. Các giao dịch sau đây được đưa vào chỉ số này (gia hạn không được tính): - Giao dịch mua mới được thực hiện trực tiếp trên paywall. - Chuyển đổi bản dùng thử từ các bản dùng thử được kích hoạt ban đầu trên paywall. - Hạ cấp, nâng cấp và chuyển đổi ngang cấp gói đăng ký được thực hiện trên paywall. - Khôi phục gói đăng ký trên paywall, chẳng hạn khi gói đăng ký được tái kích hoạt sau khi hết hạn mà không có tự động gia hạn. Bằng cách xem xét các loại giao dịch khác nhau này, chỉ số purchases cung cấp cái nhìn toàn diện về hoạt động mua lại và kiếm tiền tổng thể trên paywall của bạn. #### Trials \{#trials\} Chỉ số trials biểu thị tổng số bản dùng thử đã được kích hoạt. Nó phản ánh số người dùng đã bắt đầu thời gian dùng thử thông qua paywall của bạn. Chỉ số này giúp theo dõi hiệu quả của ưu đãi dùng thử và cung cấp thông tin về mức độ tương tác của người dùng cũng như tỷ lệ chuyển đổi từ dùng thử sang gói đăng ký trả phí. #### Trials canceled \{#trials-canceled\} Chỉ số trials canceled biểu thị số lượng bản dùng thử mà tính năng tự động gia hạn đã bị tắt. Điều này xảy ra khi người dùng tự hủy đăng ký khỏi bản dùng thử, cho thấy quyết định không tiếp tục gói đăng ký sau khi kết thúc thời gian dùng thử. Theo dõi trials canceled cung cấp thông tin có giá trị về hành vi người dùng và giúp bạn hiểu tỷ lệ người dùng từ chối bản dùng thử. #### Refunds \{#refunds\} Chỉ số refunds biểu thị số lượng giao dịch mua và gói đăng ký được hoàn tiền. Điều này bao gồm các giao dịch đã bị đảo ngược hoặc hoàn tiền vì nhiều lý do khác nhau, chẳng hạn như yêu cầu của khách hàng, vấn đề thanh toán hoặc bất kỳ chính sách hoàn tiền nào áp dụng. #### Refund rate \{#refund-rate\} Tỷ lệ hoàn tiền được tính bằng cách chia số lần hoàn tiền cho số lượng giao dịch mua lần đầu (gia hạn không được tính). Ví dụ: nếu có 5 lần hoàn tiền và 1.000 giao dịch mua lần đầu, tỷ lệ hoàn tiền sẽ là 0,5%. #### Views \{#views\} Chỉ số views biểu thị tổng số lần paywall được người dùng xem. Mỗi lần người dùng truy cập paywall được tính là một lượt xem riêng biệt. Ví dụ: nếu một người dùng truy cập paywall hai lần, sẽ được ghi nhận là hai lượt xem. Theo dõi views giúp bạn hiểu mức độ tương tác và tương tác của người dùng với paywall, cung cấp thông tin về hành vi người dùng và hiệu quả của vị trí và thiết kế paywall của bạn. #### Unique views \{#unique-views\} Chỉ số unique views biểu thị số lần duy nhất mà paywall được người dùng xem. Không giống như tổng lượt xem tính mỗi lần truy cập là một lượt xem riêng biệt, unique views chỉ tính mỗi lần người dùng truy cập paywall một lần, bất kể họ truy cập bao nhiêu lần. Ví dụ: nếu một người dùng truy cập paywall hai lần, sẽ được ghi nhận là một lượt xem duy nhất. Theo dõi unique views giúp cung cấp thước đo chính xác hơn về mức độ tương tác của người dùng và phạm vi tiếp cận của paywall, vì nó tập trung vào từng người dùng cá nhân thay vì tổng số lượt truy cập. :::warning Hãy đảm bảo gửi lượt xem paywall đến Adapty bằng phương thức `.logShowFlow()` (iOS SDK v4+) / `.logShowPaywall()`. Nếu không, lượt xem paywall sẽ không được tính vào chỉ số và tỷ lệ chuyển đổi sẽ không có giá trị tham chiếu. ::: --- # File: migrate-paywalls --- --- title: "Migrate paywall giữa các ứng dụng" description: "Tìm hiểu cách migrate paywall từ các ứng dụng khác trong Adapty." --- Với Adapty, bạn không cần xây dựng paywall mới từ đầu cho mỗi ứng dụng. Nếu bạn quản lý nhiều ứng dụng, bạn có thể migrate cấu hình paywall builder của bất kỳ paywall nào được tạo bằng builder từ ứng dụng này sang ứng dụng khác. Migration cho phép bạn sao chép toàn bộ cấu hình giao diện: - Cài đặt bố cục cho paywall và tất cả các thành phần của paywall - Media - Bản địa hóa Migration chỉ áp dụng cho cấu hình builder và không sao chép sản phẩm hay Remote Config. :::note Nếu bạn migrate cấu hình paywall builder có sử dụng font chữ tùy chỉnh, hãy kiểm tra trên thiết bị thực vì chúng có thể hiển thị không đúng. ::: ## Migrate paywall \{#migrate-paywall\} :::important Bạn chỉ có thể migrate các paywall được tạo trong Adapty paywall builder **mới**. Để migrate các paywall của **legacy** paywall builder, bạn phải migrate chúng sang paywall builder mới trước. ::: Để migrate cấu hình paywall builder: 1. **Với paywall mới**: Bắt đầu [tạo paywall](create-paywall) và thêm sản phẩm. Sau đó, nhấp vào **Build no-code paywall** để mở thư viện template. **Với paywall hiện có**: Truy cập phần **Layout settings** trong tab **Builder & Generator** và nhấp vào **Change template**. 2. Nhấp vào **Choose paywall** trong hộp **Copy a design from your apps** khi đang chỉnh sửa template paywall. <img src="/assets/shared/img/migrate-paywall-builder.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Chọn ứng dụng và paywall mà bạn muốn sao chép cấu hình từ đó. <img src="/assets/shared/img/migrate-app.png" style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấp vào **Copy Selected Paywall**. Sau khi migration, bạn có thể chỉnh sửa tùy ý mà không ảnh hưởng đến paywall gốc. --- # File: duplicate-paywalls --- --- title: "Nhân đôi paywall" description: "Tìm hiểu cách quản lý các paywall trùng lặp và tối ưu hóa hiệu suất paywall trong Adapty." --- Nếu bạn cần thực hiện các thay đổi nhỏ đối với một paywall hiện có trong Adapty, đặc biệt khi nó đã được sử dụng trong ứng dụng di động của bạn và bạn không muốn làm ảnh hưởng đến analytics, bạn có thể nhân đôi nó. Sau đó, bạn có thể dùng các bản sao này để thay thế các paywall gốc trong một số hoặc tất cả các placement tùy theo nhu cầu. Thao tác này tạo ra một bản sao của paywall với tất cả thông tin chi tiết của nó, như tên, sản phẩm và các ưu đãi. Paywall mới sẽ có thêm chữ "Copy" vào tên để bạn có thể dễ dàng phân biệt với bản gốc. Để nhân đôi một paywall trong Adapty dashboard: 1. Mở mục [**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty. Trang danh sách paywall trong Adapty dashboard cung cấp tổng quan về tất cả các paywall hiện có trong tài khoản của bạn. 2. Nhấp vào nút **3-dot** bên cạnh paywall và chọn tùy chọn **Duplicate**. <img src="/assets/shared/img/duplicate.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Điều chỉnh paywall mới và nhấp vào nút **Save**. 4. Adapty sẽ nhắc bạn thay thế các paywall gốc bằng bản sao trong các placement nếu paywall gốc hiện đang được sử dụng trong bất kỳ placement nào. Nếu bạn chọn **Create and replace original**, các paywall mới sẽ ngay lập tức chuyển sang trạng thái **Live**. Ngoài ra, bạn có thể tạo chúng như các paywall mới ở trạng thái **Draft** và thêm vào các placement sau. --- # File: archive-paywalls --- --- title: "Lưu trữ paywall" description: "Tìm hiểu cách lưu trữ các paywall lỗi thời trong Adapty mà không mất dữ liệu." --- Khi bạn làm quen với Adapty và tinh chỉnh các cài đặt paywall, bạn có thể tích lũy những paywall không còn phù hợp với chiến lược hoặc chiến dịch hiện tại nữa. Những paywall không dùng đến này, ở trạng thái `Inactive`, có thể làm rối workspace của bạn, khiến việc tìm kiếm những paywall quan trọng trở nên khó khăn hơn. Để giải quyết vấn đề này, Adapty giới thiệu tùy chọn lưu trữ những paywall không cần thiết. Lưu trữ đảm bảo chúng được lưu giữ an toàn mà không bị xóa vĩnh viễn, sẵn sàng để truy cập khi cần trong tương lai. Ngoài ra, các paywall đã lưu trữ có thể được lọc ra khỏi chế độ xem mặc định, giúp workspace gọn gàng hơn và đơn giản hóa giao diện người dùng. Trong hướng dẫn này, chúng tôi sẽ hướng dẫn bạn cách lưu trữ paywall một cách hiệu quả trong Adapty, giúp bạn kiểm soát tốt hơn quá trình quản lý paywall. Lưu ý nhỏ: Các paywall đang hoạt động (Live) trong ít nhất một placement không thể được lưu trữ. Nếu bạn muốn lưu trữ một paywall như vậy, hãy xóa nó khỏi tất cả các placement trước. :::note Bạn không thể lưu trữ một paywall nếu nó đang được sử dụng trong một A/B test chưa được lưu trữ. Điều này cho phép người dùng xem các chỉ số chi tiết của một A/B test đã hoàn thành, và paywall được liên kết là một phần của dữ liệu đó. ::: **Để lưu trữ một paywall:** 1. Mở mục [**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty. 2. Nhấp vào nút **3 chấm** bên cạnh paywall và chọn tùy chọn **Archive**. <img src="/assets/shared/img/archive-paywall.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Khi cửa sổ **Archive paywall** hiện ra, chỉ cần nhập tên của paywall bạn muốn lưu trữ, sau đó nhấp vào nút **Archive**. --- # File: restore-paywall --- --- title: "Khôi phục paywall từ lưu trữ" description: "Khôi phục các paywall trong Adapty để đảm bảo dịch vụ đăng ký không bị gián đoạn cho người dùng." --- Khả năng lưu trữ paywall là một tính năng rất hữu ích giúp đơn giản hóa quá trình quản lý paywall của bạn. Nó cho phép bạn ẩn các paywall không còn cần thiết, giảm sự lộn xộn trong không gian làm việc. Hơn nữa, tùy chọn khôi phục các paywall đã lưu trữ mang lại sự linh hoạt, cho phép bạn đưa chúng trở lại chiến lược nếu chúng hữu ích trở lại. Các paywall đã lưu trữ có thể bị lọc ra khỏi chế độ xem mặc định. Để xem chúng, hãy chọn **Archived** trong bộ lọc **State**. **Để đưa paywall trở lại từ lưu trữ** 1. Mở mục [**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty. 2. Đảm bảo rằng các paywall đã lưu trữ được hiển thị trong danh sách. Nếu không, hãy cập nhật bộ lọc ở bên phải. <img src="/assets/shared/img/paywall-filter.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp vào nút **3-dot** bên cạnh paywall đã lưu trữ và chọn **Back to active**. <img src="/assets/shared/img/restore-paywall.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: profiles-crm --- --- title: "Profiles/CRM" description: "Quản lý hồ sơ người dùng và dữ liệu CRM trong Adapty để nâng cao khả năng phân khúc đối tượng." --- Profiles là CRM dành cho người dùng của bạn. Với Profiles, bạn có thể: 1. Tìm người dùng cụ thể theo profile ID, customer user ID, email, hoặc transaction ID. 2. Xem lịch sử sự kiện của người dùng, bao gồm các vấn đề thanh toán, thời gian ân hạn, và các [sự kiện](events) khác. 3. Phân tích các thuộc tính của người dùng như trạng thái gói đăng ký, tổng doanh thu/lợi nhuận, và nhiều thông tin khác. 4. Cấp gói đăng ký cho người dùng. :::note Các sự kiện từ luồng sự kiện sẽ đến dashboard với một khoảng trễ. Hồ sơ người dùng mới và các thay đổi thuộc tính có thể không hiển thị ngay lập tức. ::: <img src="/assets/shared/img/profiles.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::link Để hiểu cách Adapty tạo và liên kết hồ sơ người dùng, xem [Cách profiles hoạt động](how-profiles-work). ::: ## Tìm người dùng \{#finding-users\} Trong danh sách Profiles, bạn có thể tìm kiếm người dùng cụ thể theo: - **Profile ID**: Định danh nội bộ của Adapty cho người dùng (còn gọi là Adapty ID). - **Customer user ID**: Định danh của ứng dụng bạn cho người dùng, nếu bạn đã thiết lập. - **Email**: Email của người dùng, nếu được gửi dưới dạng custom attribute. - **Transaction ID**: Transaction ID của cửa hàng từ một giao dịch mua. Nhấp vào bất kỳ hàng nào để mở hồ sơ đầy đủ của người dùng. ## Trạng thái gói đăng ký \{#subscription-state\} Trong danh sách Profiles, bạn có thể lọc và sắp xếp người dùng theo trạng thái gói đăng ký. Các giá trị trạng thái bao gồm: | **Trạng thái** người dùng | Mô tả | | :------------------------ | :----------------------------------------------------------- | | Subscribed | Người dùng có gói đăng ký đang hoạt động với tính năng tự động gia hạn được bật. | | Auto-renew off | Người dùng đã tắt tự động gia hạn nhưng vẫn có quyền truy cập các tính năng cao cấp cho đến khi kết thúc kỳ đăng ký. | | Subscription cancelled | Người dùng đã hủy gói đăng ký và gói đó đã hoàn toàn kết thúc. | | Billing issue | Người dùng không thể bị tính phí do sự cố thanh toán, xảy ra sau khi gói đăng ký hoặc dùng thử của họ hết hạn. | | Grace period | Người dùng hiện đang trong thời gian ân hạn do sự cố thanh toán xảy ra khi cố gắng tính phí sau khi gói đăng ký hoặc dùng thử của họ hết hạn. | | Active trial | Người dùng có gói đăng ký đang hoạt động và hiện đang trong giai đoạn dùng thử. | | Trial cancelled | Người dùng đã hủy dùng thử và không có gói đăng ký đang hoạt động. | | Never subscribed | Người dùng chưa bao giờ đăng ký hoặc bắt đầu dùng thử và vẫn là người dùng freemium. | ## Thuộc tính người dùng \{#user-attributes\} <img src="/assets/shared/img/ce8df4d-CleanShot_2023-06-26_at_20.32.232x.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> Bạn có thể gửi thêm các thuộc tính người dùng lên Adapty thông qua SDK. Mặc định, Adapty thiết lập: | Thuộc tính | Mô tả | | ---------------- | ------------------------------------------------------------ | | Customer user ID | Định danh người dùng cuối của bạn trong hệ thống của bạn. | | Adapty ID | Định danh nội bộ Adapty của người dùng cuối, gọi là Profile ID. | | IDFA | Identifier for Advertisers, do Apple gán cho thiết bị của người dùng. Yêu cầu quyền App Tracking Transparency (ATT) trên iOS 14+. Không khả dụng trên Android. | | Country | Quốc gia của người dùng cuối. | | OS | Hệ điều hành mà người dùng cuối đang sử dụng. | | Device | Tên model thiết bị hiển thị cho người dùng cuối. | | Install date | Ngày người dùng được ghi nhận lần đầu trong Adapty: <ul><li>Ngày người dùng được tạo. </li><li>Nếu người dùng cài đặt ứng dụng của bạn trước khi bạn tích hợp Adapty, ngày cài đặt phản ánh ngày giao dịch đầu tiên của họ.</li><li>Nếu có, ngày được cung cấp trong quá trình import dữ liệu lịch sử.</li></ul> | | Created at | Ngày người dùng được tạo. | Hãy gửi ít nhất ID nội bộ của người dùng hoặc email người dùng. Điều này cho phép bạn tìm người dùng theo các định danh này trong danh sách Profiles. Sau khi bạn cài đặt SDK, Adapty tự động thu thập các sự kiện người dùng từ hàng đợi thanh toán và hiển thị chúng trong hồ sơ người dùng. Các thuộc tính trong bảng trên được thu thập tự động — bạn không cần gửi chúng. ### Custom attributes \{#custom-attributes\} Trong phần **Attributes** của một profile, bạn có thể xem các custom attribute được thiết lập thông qua SDK hoặc API. Bạn cũng có thể gán thuộc tính thủ công bằng nút **Add attribute**. <img src="/assets/shared/img/378c1fb-add_attribute.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> ## Cấp gói đăng ký \{#granting-a-subscription\} Trong một profile, bạn có thể gia hạn gói đăng ký đang hoạt động hoặc cấp cho người dùng quyền truy cập trọn đời vào một mức độ truy cập — mà không yêu cầu họ thực hiện mua hàng. <img src="/assets/shared/img/b1d74fd-edit_paid_access_level.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> Tính năng này hữu ích nhất cho: - Bồi thường cho người dùng sau sự cố thanh toán hoặc hỗ trợ. - Chạy các chương trình khuyến mãi thủ công hoặc chương trình beta. - Kiểm tra flow gói đăng ký mà không cần mua hàng thực. Để cấp quyền truy cập, mở hồ sơ người dùng, vào phần **Access levels**, và nhấp **Edit**. Đặt ngày hết hạn và lưu lại. Ngày hết hạn phải là ngày trong tương lai và không thể giảm xuống sau khi đã thiết lập. Việc điều chỉnh ngày hết hạn cho các gói đăng ký đang hoạt động không ảnh hưởng đến các lần thanh toán đang diễn ra. :::note Việc cấp quyền truy cập không tạo ra các sự kiện mua hàng trên App Store hoặc Google Play. Lịch sử sự kiện và phân tích của người dùng sẽ khác so với flow mua hàng thực tế. ::: Bạn cũng có thể cấp quyền truy cập theo lập trình bằng phương thức API [Grant access level](api-adapty/operations/grantAccessLevel). ## Chia sẻ quyền truy cập trả phí giữa các tài khoản người dùng \{#sharing-paid-access-between-user-accounts\} :::link Bài viết chính: [Chia sẻ quyền truy cập trả phí giữa các tài khoản người dùng](sharing-paid-access-between-user-accounts) ::: ### Lịch sử chia sẻ quyền truy cập \{#access-sharing-history\} Khi các mức độ truy cập được chia sẻ hoặc chuyển giao, hồ sơ người dùng hiển thị liên kết đến hồ sơ được kết nối — hồ sơ đã chia sẻ quyền truy cập, hoặc hồ sơ đã nhận quyền truy cập. Để xem hồ sơ được kết nối, trong **Profile** của người dùng, nhấp vào liên kết bên cạnh mức độ truy cập. <img src="/assets/shared/img/profile-access-level-origin.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Các bước tiếp theo \{#next-steps\} - Để hiểu cách Adapty tạo và liên kết hồ sơ, xem [Cách profiles hoạt động](how-profiles-work). - Để cấu hình chính sách chia sẻ quyền truy cập, xem [Chia sẻ quyền truy cập trả phí giữa các tài khoản người dùng](sharing-paid-access-between-user-accounts). - Để cấp quyền truy cập theo lập trình, xem phương thức API [Grant access level](api-adapty/operations/grantAccessLevel). --- # File: how-profiles-work --- --- title: "Cách hồ sơ người dùng hoạt động" description: "Hiểu cách Adapty tạo, theo dõi và liên kết hồ sơ người dùng — bao gồm hồ sơ ẩn danh, người dùng đã xác định danh tính, và mối quan hệ parent/inheritor." --- Mỗi người dùng trong ứng dụng của bạn đều có một hồ sơ người dùng Adapty để theo dõi các giao dịch mua, sự kiện và trạng thái gói đăng ký. Hiểu cách hồ sơ được tạo và liên kết giúp bạn ngăn ngừa lỗi tích hợp, tránh phân mảnh dữ liệu, và diễn giải dữ liệu trong phần [Profiles](profiles-crm). ## Tạo hồ sơ \{#profile-creation\} Adapty tự động tạo hồ sơ lần đầu tiên người dùng mở ứng dụng của bạn. **Không có Customer User ID**, hồ sơ là ẩn danh. Một hồ sơ ẩn danh mới được tạo mỗi khi: - Người dùng cài lại ứng dụng - Người dùng đăng xuất khỏi ứng dụng của bạn (khi ứng dụng gọi `Adapty.logout()`) Các giao dịch mua được gắn với lần cài đặt ứng dụng, chứ không phải với danh tính người dùng cố định. **Với Customer User ID**, hồ sơ tồn tại xuyên suốt các lần cài lại và trên nhiều thiết bị. Sử dụng customer user ID cho phép bạn: 1. Theo dõi người dùng qua các lần cài lại ứng dụng và nhiều thiết bị. 2. Tìm người dùng theo customer user ID của họ trong phần [**Profiles**](profiles-crm). 3. Sử dụng customer user ID trong [server-side API](getting-started-with-server-side-api). 4. Adapty gửi customer user ID đến tất cả các tích hợp. Hành vi hồ sơ với customer user ID phụ thuộc vào thời điểm bạn thiết lập nó: - **Khi kích hoạt SDK**: Adapty sử dụng hồ sơ hiện có với customer user ID đó (cho người dùng cũ) hoặc tạo hồ sơ mới (cho người dùng lần đầu). - **Sau khi kích hoạt SDK**: Adapty tạo hồ sơ ẩn danh khi kích hoạt. Khi bạn xác định danh tính người dùng sau đó, Adapty liên kết customer user ID với hồ sơ ẩn danh (cho người dùng lần đầu) hoặc chuyển sang hồ sơ hiện có với ID đó (cho người dùng cũ). **Nên dùng cách nào:** - **Customer user ID có sẵn khi khởi động ứng dụng** (ví dụ: lưu từ phiên trước) — truyền vào `activate()` khi khởi tạo SDK. - **Người dùng đăng nhập sau khi ứng dụng khởi động** — gọi `identify()` sau khi xác thực. Adapty liên kết ID với hồ sơ hiện tại (nếu ID mới) hoặc chuyển sang hồ sơ hiện có (nếu ID đã tồn tại). - **Người dùng có thể mua trước khi đăng nhập** — gọi `identify()` sau khi đăng nhập. Nếu customer user ID đã tồn tại trong Adapty, hãy lấy lại hồ sơ sau đó để đồng bộ mức độ truy cập hiện tại. Để biết chi tiết triển khai, xem hướng dẫn SDK về [xác định danh tính người dùng](identifying-users). :::note Nếu người dùng cũ trước đây đã dùng ứng dụng của bạn mà không có customer user ID, những hồ sơ ẩn danh đó sẽ không được tự động gộp khi bạn bắt đầu xác định danh tính tại thời điểm kích hoạt SDK. Để duy trì lịch sử đầy đủ cho những người dùng như vậy, hãy dùng `identify()` sau khi đăng nhập thay thế. ::: ## Hồ sơ parent và inheritor \{#parent-and-inheritor-profiles\} Khi cùng một gói đăng ký phía cửa hàng được liên kết với nhiều hơn một hồ sơ người dùng Adapty, Adapty coi các hồ sơ đó là một chuỗi: một hồ sơ **parent** và một hoặc nhiều hồ sơ **inheritor** chia sẻ quyền truy cập từ cùng một giao dịch mua. Điều này xảy ra khi: - [Chia sẻ quyền truy cập có trả phí giữa các tài khoản người dùng](sharing-paid-access-between-user-accounts) được bật và người dùng đăng nhập trên thiết bị nơi một hồ sơ khác đã thực hiện giao dịch mua trước đó. - Người dùng cài lại ứng dụng mà không có `customer_user_id`, và hồ sơ mới tiếp nhận giao dịch mua từ lần cài đặt trước. - Các người dùng đã xác định danh tính khác nhau khôi phục giao dịch mua trên cùng một thiết bị. - Ứng dụng được chuyển giữa các Apple Team ID và ứng dụng mới tiếp nhận các giao dịch mua thực hiện dưới Team ID cũ. **Cách chọn parent.** Parent là **hồ sơ đầu tiên ghi nhận giao dịch mua** — được xác định theo thứ tự receipt mua trong Adapty, không phải theo thứ tự tạo hồ sơ. Ví dụ: bạn cài ứng dụng và không mua gì, sau đó cài lại và mua gói đăng ký. Hồ sơ thứ hai trở thành parent vì nó thực hiện giao dịch mua. Hồ sơ đầu tiên trở thành inheritor và được cấp quyền truy cập thông qua tính năng chia sẻ. **Cách phân phối sự kiện:** - **Sự kiện giao dịch** (mua hàng, gia hạn, hủy, vấn đề thanh toán, thời gian ân hạn, hoàn tiền): Chỉ xuất hiện trên hồ sơ **parent** đã thực hiện giao dịch mua. Tất cả các lần gia hạn và cập nhật gói đăng ký tiếp tục xuất hiện trên hồ sơ này. - **Sự kiện `access_level_updated`**: Xuất hiện trên **cả hồ sơ parent lẫn inheritor** bất cứ khi nào trạng thái mức độ truy cập thay đổi. Điều này giúp tất cả các hồ sơ được kết nối luôn cập nhật về trạng thái truy cập hiện tại. Hồ sơ parent hiển thị toàn bộ lịch sử giao dịch. Các hồ sơ inheritor chỉ hiển thị các cập nhật mức độ truy cập của họ và một liên kết đến hồ sơ parent trong phần **Access level**. <img src="/assets/shared/img/98d0dad-non-original_profile.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> **Theo dõi cùng một gói đăng ký trên nhiều hồ sơ.** Mỗi hồ sơ inheritor có `profile_id` riêng, vì vậy `profile_id` không ổn định xuyên suốt một chuỗi. Để xác định cùng một gói đăng ký trên nhiều hồ sơ — ví dụ, khi đối chiếu các sự kiện webhook hoặc khớp hồ sơ dashboard với một người dùng cơ bản — hãy dùng mã định danh phía cửa hàng thay thế. | Trường | Dùng để | | --- | --- | | `store_original_transaction_id` | Xác định chuỗi gói đăng ký trên nhiều hồ sơ. Duy nhất cho mỗi gói đăng ký Apple. | | `profiles_sharing_access_level` (trường webhook) | Tất cả các hồ sơ hiện đang được cấp quyền bởi gói đăng ký, khi tính năng chia sẻ được bật. | | `profile_id` | **Không** phù hợp để theo dõi xuyên hồ sơ — mỗi inheritor có `profile_id` riêng. | ## Giao dịch không có hồ sơ \{#transactions-without-profiles\} Một số giao dịch trong Adapty không được gắn với hồ sơ nào — chúng xuất hiện trong phân tích và xuất dữ liệu nhưng không xuất hiện trong danh sách Profiles. Điều này xảy ra với **các thông báo cửa hàng server-to-server (S2S)** được gửi cho những người dùng mà tài khoản của họ chưa bao giờ kết nối với ứng dụng của bạn thông qua Adapty SDK. Các nguồn đã biết bao gồm: - Thông báo S2S của App Store (bao gồm sự kiện hoàn tiền) - Thông báo S2S của Google Play - Sự kiện webhook của Stripe và Paddle Các giao dịch này: - **Xuất hiện trong biểu đồ phân tích** (chúng được tính vào tổng chỉ số) - **Xuất hiện trong xuất dữ liệu** (S3, GCS, BigQuery) với `profile_id` được đặt là `null` - **Không xuất hiện trong danh sách Profiles** — không có hồ sơ nào để gắn vào Nếu bạn thấy nhiều sự kiện hơn trong phân tích hoặc xuất dữ liệu so với những gì bạn có thể tìm thấy trong giao diện Profiles, sự khác biệt đó có thể là do các giao dịch không có hồ sơ này. Để tìm chúng trong một bản xuất, hãy lọc các hàng có `profile_id IS NULL`. ## Chia sẻ quyền truy cập có trả phí giữa các tài khoản người dùng \{#sharing-paid-access-between-user-accounts\} :::link Bài viết chính: [Chia sẻ quyền truy cập có trả phí giữa các tài khoản người dùng](sharing-paid-access-between-user-accounts) ::: Để thiết lập chính sách chia sẻ mức độ truy cập, trên trang cài đặt [**General**](general), hãy chọn một tùy chọn chia sẻ. Bạn có thể đặt chính sách riêng cho [môi trường sandbox](test-purchases-in-sandbox). **Enabled (mặc định)** Người dùng đã xác định (những người có [Customer User ID](identifying-users#set-customer-user-id-on-configuration)) có thể chia sẻ cùng một [mức độ truy cập](access-level) do Adapty cung cấp nếu thiết bị của họ đăng nhập vào cùng một Apple/Google ID. Điều này hữu ích khi người dùng cài lại ứng dụng và đăng nhập bằng email khác — họ vẫn có thể truy cập vào giao dịch mua trước đó. Với tùy chọn này, nhiều người dùng đã xác định có thể dùng chung một mức độ truy cập. Dù mức độ truy cập được chia sẻ, tất cả các giao dịch trong quá khứ và tương lai vẫn được ghi lại dưới dạng sự kiện trong Customer User ID gốc để đảm bảo tính nhất quán của dữ liệu phân tích và lưu giữ toàn bộ lịch sử giao dịch — bao gồm thời gian dùng thử, mua gói đăng ký, gia hạn, v.v., đều được liên kết với cùng một hồ sơ người dùng. **Transfer access to new user** Người dùng đã xác định vẫn có thể tiếp tục truy cập [mức độ truy cập](access-level) do Adapty cung cấp, ngay cả khi họ đăng nhập bằng [Customer User ID](identifying-users#set-customer-user-id-on-configuration) khác hoặc cài lại ứng dụng, miễn là thiết bị đăng nhập vào cùng một Apple/Google ID. Khác với tùy chọn trước, Adapty sẽ chuyển giao dịch mua giữa các người dùng đã xác định. Điều này đảm bảo nội dung đã mua vẫn khả dụng, nhưng chỉ một người dùng có thể truy cập tại một thời điểm. Ví dụ: nếu UserA mua gói đăng ký và UserB đăng nhập trên cùng thiết bị đó và khôi phục giao dịch, UserB sẽ được cấp quyền truy cập gói đăng ký đó, còn UserA sẽ bị thu hồi. Nếu một trong hai người dùng (mới hoặc cũ) chưa được xác định, mức độ truy cập vẫn sẽ được chia sẻ giữa các hồ sơ người dùng đó trong Adapty. Dù mức độ truy cập được chuyển giao, tất cả các giao dịch trong quá khứ và tương lai vẫn được ghi lại dưới dạng sự kiện trong Customer User ID gốc để đảm bảo tính nhất quán của dữ liệu phân tích và lưu giữ toàn bộ lịch sử giao dịch — bao gồm thời gian dùng thử, mua gói đăng ký, gia hạn, v.v., đều được liên kết với cùng một hồ sơ người dùng. Sau khi chuyển sang **Transfer access to new user**, mức độ truy cập sẽ không được chuyển ngay lập tức giữa các hồ sơ người dùng. Quá trình chuyển giao cho từng mức độ truy cập cụ thể chỉ được kích hoạt khi Adapty nhận được sự kiện từ cửa hàng, chẳng hạn như gia hạn gói đăng ký, khôi phục, hoặc khi xác thực giao dịch. **Disabled** Hồ sơ người dùng đã xác định đầu tiên được cấp mức độ truy cập sẽ giữ nó mãi mãi. Đây là lựa chọn tốt nhất nếu logic nghiệp vụ của bạn yêu cầu giao dịch mua phải được gắn với một Customer User ID duy nhất. Lưu ý rằng mức độ truy cập vẫn được chia sẻ giữa các người dùng ẩn danh. Bạn có thể "gỡ liên kết" giao dịch mua bằng cách [xóa hồ sơ người dùng của chủ sở hữu](https://adapty.io/docs/vi/api-adapty/operations/deleteProfile). Sau khi xóa, mức độ truy cập sẽ khả dụng cho hồ sơ người dùng đầu tiên yêu cầu nó, dù là ẩn danh hay đã xác định. Việc tắt chia sẻ chỉ ảnh hưởng đến người dùng mới. Các gói đăng ký đã được chia sẻ giữa người dùng sẽ tiếp tục được chia sẻ ngay cả sau khi tắt tùy chọn này. :::warning Apple và Google yêu cầu in-app purchase phải được chia sẻ hoặc chuyển giao giữa các người dùng vì họ dựa vào Apple/Google ID để liên kết giao dịch mua. Nếu không có chia sẻ, việc khôi phục giao dịch mua có thể không hoạt động sau khi cài lại ứng dụng. Tắt chia sẻ có thể khiến người dùng không thể lấy lại quyền truy cập sau khi đăng nhập. Chúng tôi khuyến nghị chỉ tắt chia sẻ nếu người dùng của bạn **bắt buộc phải đăng nhập** trước khi thực hiện giao dịch mua. Nếu không, một người dùng đã xác định có thể mua gói đăng ký, đăng nhập vào tài khoản khác và mất quyền truy cập vĩnh viễn. ::: ### Tôi nên chọn cài đặt nào? \{#which-setting-should-i-choose\} | Ứng dụng của tôi... | Tùy chọn nên chọn | | ------------------------------------------------------------ | ------------------------------------------------------------ | | Không có hệ thống đăng nhập và chỉ sử dụng ID hồ sơ người dùng ẩn danh của Adapty. | Dùng tùy chọn mặc định, vì mức độ truy cập luôn được chia sẻ giữa các ID hồ sơ người dùng ẩn danh cho cả ba tùy chọn. | | Có hệ thống đăng nhập tùy chọn và cho phép khách hàng mua trước khi tạo tài khoản. | Chọn **Transfer access to new user** để đảm bảo những khách hàng mua khi chưa có tài khoản vẫn có thể khôi phục giao dịch sau này. | | Yêu cầu khách hàng tạo tài khoản trước khi mua, nhưng cho phép giao dịch mua được liên kết với nhiều Customer User ID. | Chọn **Transfer access to new user** để đảm bảo chỉ một Customer User ID có quyền truy cập tại một thời điểm, đồng thời vẫn cho phép người dùng đăng nhập bằng Customer User ID khác mà không mất quyền truy cập đã trả phí. | | Yêu cầu khách hàng tạo tài khoản trước khi mua, với quy tắc nghiêm ngặt ràng buộc giao dịch mua với một Customer User ID duy nhất. | Chọn **Disabled** để đảm bảo giao dịch không bao giờ được chuyển giao giữa các tài khoản. | ## Dấu thời gian sự kiện có ngày trong tương lai (Apple/iOS) \{#event-timestamps-with-future-dates-appleios\} Hành vi này chỉ xảy ra với Apple App Store. Hệ thống thông báo của Google Play không gửi sự kiện trước. Dấu thời gian sự kiện trong hồ sơ và tích hợp có thể hiển thị ngày trong tương lai vì Apple gửi sự kiện gia hạn trước. - **Lý do xảy ra**: Apple làm điều này để đảm bảo gói đăng ký gia hạn tự động trước khi hết hạn, ngăn ngừa gián đoạn dịch vụ cho người dùng. Để biết thêm chi tiết, xem Diễn đàn nhà phát triển Apple: [Server Notifications for Subscriptions](https://developer.apple.com/forums/tags/app-store-server-notifications). - **Các loại sự kiện bị ảnh hưởng**: Thông thường, điều này áp dụng cho các lần gia hạn gói đăng ký và chuyển đổi từ dùng thử sang trả phí. Các sự kiện này có thể có dấu thời gian trong tương lai vì Apple thông báo cho hệ thống về chúng trước. - **Các loại sự kiện khác**: Các in-app purchase bổ sung và thay đổi gói đăng ký được ghi lại với dấu thời gian thực tế vì những sự kiện này không thể được dự đoán trước. - **Ảnh hưởng đến Analytics và Event Feed**: Các sự kiện này sẽ chỉ xuất hiện trong **Analytics** và **Event Feed** sau khi dấu thời gian của chúng đã qua. Các sự kiện có dấu thời gian trong tương lai không hiển thị trong cả hai phần. - **Ảnh hưởng đến Integrations**: Adapty gửi sự kiện đến các tích hợp ngay khi nhận được. Nếu một sự kiện có dấu thời gian trong tương lai, Adapty gửi nó đến tích hợp của bạn với dấu thời gian trong tương lai không thay đổi. ## Bước tiếp theo \{#next-steps\} - Để dùng dashboard Profiles để tìm và quản lý người dùng, xem [Profiles](profiles-crm). - Để thiết lập xác định danh tính người dùng trong ứng dụng, xem hướng dẫn SDK về [xác định danh tính người dùng](identifying-users). - Để cấu hình chính sách chia sẻ quyền truy cập, xem [Chia sẻ quyền truy cập có trả phí giữa các tài khoản người dùng](sharing-paid-access-between-user-accounts). --- # File: sharing-paid-access-between-user-accounts --- --- title: "Chia sẻ quyền truy cập trả phí giữa các tài khoản người dùng" description: "Chia sẻ quyền truy cập trả phí giữa các tài khoản người dùng khác nhau để hỗ trợ người dùng có nhiều thiết bị hoặc nhiều hồ sơ trong ứng dụng" --- Khi người dùng thực hiện một giao dịch mua, Adapty sẽ gán một [mức độ truy cập](access-level) mới cho [hồ sơ người dùng](identifying-users) đang hoạt động của họ. Mức độ truy cập này cho phép người mua truy cập vào nội dung trả phí. Hồ sơ người dùng của người mua có thể thay đổi ngoài ý muốn nếu họ cài đặt lại ứng dụng, hoặc đăng nhập vào một tài khoản trong ứng dụng mới. Để đảm bảo quyền truy cập liên tục, Adapty tự động chia sẻ mức độ truy cập của người dùng giữa hồ sơ ban đầu và các hồ sơ tiếp theo. Cách tiếp cận này phù hợp với hầu hết các ứng dụng. Nhưng nếu logic nghiệp vụ của bạn yêu cầu, bạn có thể chọn chính sách chia sẻ quyền truy cập trả phí hạn chế hơn. Mở trang [General Settings](https://app.adapty.io/settings/general) để thiết lập chính sách chia sẻ mức độ truy cập. Để thuận tiện cho việc kiểm thử, bạn có thể thay đổi cài đặt này chỉ cho [môi trường sandbox](#sharing-paid-access-on-sandbox). <Details> :::important Nếu ứng dụng của bạn không xác thực người dùng, bạn có thể bỏ qua cài đặt này. Các hồ sơ ẩn danh được liên kết với cùng một tài khoản cửa hàng *luôn luôn* chia sẻ mức độ truy cập với nhau. ::: <summary>Tôi nên chọn chính sách chia sẻ quyền truy cập nào? (Nhấp để mở rộng)</summary> | Ứng dụng của tôi... | Lựa chọn tốt nhất | | ------------------------------------------------------------ | ------------------------------------------------------------ | | Không có khả năng xác thực người dùng và chỉ sử dụng ID hồ sơ ẩn danh của Adapty. | Sử dụng cài đặt **Enabled (default)**. | | Có thể xác thực người dùng, nhưng cho phép họ mua hàng mà không cần tài khoản. | Bật cài đặt **Transfer access to new user**. Người dùng sẽ có thể đăng ký và xác nhận quyền sở hữu các giao dịch mua ẩn danh. | | Yêu cầu khách hàng tạo tài khoản trước khi mua, nhưng có thể liên kết một sản phẩm với nhiều Customer User ID. | Bật cài đặt **Transfer access to new user**. Nhiều tài khoản sẽ có thể truy cập sản phẩm, nhưng chỉ theo thứ tự. | | Yêu cầu khách hàng tạo tài khoản trước khi mua, với các quy tắc nghiêm ngặt ràng buộc giao dịch mua với một Customer User ID duy nhất. | **Disable** chia sẻ mức độ truy cập. | </Details> <img src="/assets/shared/img/sharing-paid-access.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Enabled (default) \{#enabled-default\} Cài đặt này phù hợp nhất cho các ứng dụng **không có xác thực tích hợp**. Sau khi mua, tất cả các hồ sơ được liên kết với cùng một tài khoản cửa hàng sẽ tự động *kế thừa* mức độ truy cập. * Nếu người dùng đăng nhập vào ứng dụng của bạn với thông tin xác thực mới, họ vẫn giữ quyền truy cập vào nội dung trả phí. * Nếu người dùng cài đặt lại ứng dụng sau khi khôi phục cài đặt gốc, họ vẫn giữ quyền truy cập vào nội dung trả phí. * Nếu người dùng cài đặt ứng dụng trên các thiết bị khác với cùng một tài khoản cửa hàng, giao dịch mua sẽ được cung cấp trên tất cả các thiết bị. Ngay cả khi mỗi phiên bản ứng dụng có hồ sơ khách hàng riêng. ## Transfer access to new user \{#transfer-access-to-new-user\} Cài đặt này phù hợp nhất cho các ứng dụng cho phép mua hàng **có hoặc không có xác thực**, hoặc muốn thực thi chính sách **một thiết bị mỗi người dùng**. Adapty giới hạn quyền truy cập mua hàng cho 1 customer ID tại một thời điểm. Chủ thiết bị có thể cài đặt lại ứng dụng, đăng nhập và đăng xuất, nhưng không thể truy cập cùng một sản phẩm từ nhiều hơn một customer ID đồng thời. Khi bật cài đặt này, các hồ sơ ẩn danh (ví dụ: hồ sơ trở nên hoạt động sau khi người dùng đăng xuất) luôn kế thừa mức độ truy cập của customer ID đang hoạt động gần nhất. Điều này là cần thiết để ngăn mất quyền truy cập sau này. :::warning Khi bạn tắt cài đặt mặc định và bật **Transfer access to new user**, Adapty không cập nhật ngay lập tức mức độ truy cập của các hồ sơ khách hàng hiện có. Việc chuyển đổi xảy ra khi người dùng kích hoạt một sự kiện cửa hàng mới: ví dụ, gia hạn gói đăng ký hoặc khôi phục giao dịch mua của họ. ::: :::important Adapty thu hồi hồ sơ cũ chỉ khi hồ sơ mới có [Customer User ID](identifying-users#set-customer-user-id-on-configuration) tại thời điểm SDK truyền giao dịch. Nếu `restorePurchases` chạy trên một hồ sơ ẩn danh, cả Customer User ID cũ và hồ sơ ẩn danh mới đều sẽ có mức độ truy cập. Hồ sơ cũ sẽ bị thu hồi sau, khi bạn xác định hồ sơ ẩn danh. Để tránh điều này, hãy gọi các phương thức SDK theo thứ tự: `activate` → `identify` → `restorePurchases`. ::: ## Tắt chia sẻ quyền truy cập trả phí \{#disable-paid-access-sharing\} Cài đặt này **chỉ phù hợp** cho các ứng dụng có **xác thực bắt buộc** hoặc triển khai quản lý quyền truy cập độc lập. Trong các trường hợp khác, người dùng có thể không thể truy cập các giao dịch mua của họ, và ứng dụng của bạn có nguy cơ **không vượt qua được quy trình xét duyệt bắt buộc của cửa hàng**. Nếu bạn tắt chia sẻ quyền truy cập trả phí, Adapty sẽ gắn sản phẩm với [customer ID](identifying-users#set-customer-user-id-on-configuration) đang hoạt động tại thời điểm mua, và không chia sẻ mức độ truy cập với bất kỳ hồ sơ khách hàng nào khác. Chính sách này cho phép phân phối sản phẩm theo tỷ lệ 1-1 nghiêm ngặt. :::warning Khi bạn tắt chia sẻ quyền truy cập trả phí, bạn ngăn các customer ID kế thừa quyền truy cập trả phí. Nếu một customer ID đã kế thừa quyền truy cập trả phí trong quá khứ, nó không thể bị thu hồi tự động. ::: :::important Trong các tình huống khẩn cấp, bạn có thể cần [xóa hồ sơ người dùng](api-adapty/operations/deleteProfile) để hồ sơ tiếp theo có sẵn (dù đã được xác định hay ẩn danh) có thể xác nhận quyền sở hữu mức độ truy cập của nó. ::: ## Tham khảo thực tế \{#practical-reference\} Sau khi bạn chọn một chế độ, các mô tả dưới đây cho biết điều gì sẽ xảy ra: hồ sơ nào thấy quyền truy cập, khi nào hồ sơ cũ mất quyền truy cập, và các sự kiện webhook nào được kích hoạt. | Chế độ | Nhiều hồ sơ chia sẻ một giao dịch mua? | Hồ sơ cũ bị thu hồi khi chuyển? | Khi nào hồ sơ cũ bị thu hồi | Sự kiện webhook khi hồ sơ thứ hai xác nhận gói đăng ký | | --- | --- | --- | --- | --- | | **Enabled (default)** | Có — mọi hồ sơ khôi phục hoặc đăng nhập đều kế thừa quyền truy cập | Không bao giờ | N/A | `access_level_updated` (`is_active=true`) cho mỗi hồ sơ mới kế thừa | | **Transfer access to new user** | Không — độc quyền, nhưng có thể chuyển giữa các hồ sơ | Có | Ngay lập tức khi thiết bị được xác định mới truyền giao dịch (`restorePurchases`, identify, hoặc sự kiện phía cửa hàng tiếp theo) | Hồ sơ mới: `access_level_updated` (`is_active=true`). Hồ sơ cũ: `access_level_updated` (`is_active=false`) | | **Disabled** | Không — một Customer User ID cho mỗi giao dịch mua, vĩnh viễn | N/A — quyền truy cập không bao giờ được chuyển | N/A | Không có sự kiện nào trên hồ sơ thứ hai. SDK không hiển thị quyền truy cập cho hồ sơ đó | ## Chia sẻ quyền truy cập trả phí trên sandbox \{#sharing-paid-access-on-sandbox\} Bạn có thể thiết lập chính sách chia sẻ quyền truy cập trả phí riêng cho môi trường sandbox. Khi bạn kiểm thử giao dịch mua trong môi trường sandbox, hãy lưu ý các hành vi sau: * Apple lưu trữ thông tin về các giao dịch mua trong quá khứ của bạn trong lịch sử mua của tài khoản. Adapty SDK cũng có thể truy cập thông tin này. * Nếu bạn cài đặt lại ứng dụng và Adapty phát hiện rằng sản phẩm đã được mua trước đó, hồ sơ đang hoạt động sẽ kế thừa mức độ truy cập. * Nếu Apple phát hiện một giao dịch mua hiện có cho sản phẩm, Apple sẽ không cho phép bạn mua cùng một sản phẩm hai lần, ngay cả khi hồ sơ đang hoạt động không có mức độ truy cập cần thiết. Hành vi này xảy ra **độc lập với cài đặt chia sẻ quyền truy cập trả phí của bạn**. Ứng dụng của bạn không hiển thị paywall, bạn không thể mua sản phẩm. Giải pháp duy nhất là **xóa lịch sử mua của tài khoản**. Hãy xem [hướng dẫn kiểm thử sandbox](test-purchases-in-sandbox) để biết hướng dẫn chi tiết. :::warning Các gói đăng ký sandbox trên Apple tự động gia hạn mỗi vài phút. Những lần gia hạn nhanh chóng này có thể thay đổi hồ sơ mà Adapty coi là [cha](how-profiles-work#parent-and-inheritor-profiles) — một mô hình chuỗi mà môi trường sản xuất hiếm khi tái hiện. Hãy kiểm thử chế độ bạn sử dụng trong môi trường sản xuất và xác nhận hành vi với Apple ID thực trước khi rút ra kết luận từ sandbox. ::: ## Chia sẻ quyền truy cập trả phí trong analytics \{#paid-access-sharing-in-analytics\} * Adapty ghi lại các giao dịch khi chúng xảy ra. Một giao dịch có thể được liên kết với nhiều hơn một hồ sơ, nhưng không được tính nhiều hơn một lần. * Nếu hai hoặc nhiều hồ sơ chia sẻ cùng một mức độ truy cập, giao dịch mua được quy cho [hồ sơ cha](how-profiles-work#parent-and-inheritor-profiles). * Việc kế thừa mức độ truy cập không ảnh hưởng đến thống kê cài đặt. Để xác định cách Adapty đếm lượt cài đặt, bạn có thể chọn một trong hai [định nghĩa cài đặt](installs#counting-modes) có sẵn trên trang cài đặt. --- # File: segments --- --- title: "Phân khúc" description: "Tạo và quản lý phân khúc người dùng để nhắm mục tiêu tốt hơn trong Adapty." --- **Phân khúc** là tập hợp các bộ lọc giúp nhóm người dùng có đặc điểm chung. Dùng phân khúc để nhắm mục tiêu paywall và A/B test hiệu quả hơn. :::note Các sự kiện từ luồng sự kiện sẽ đến dashboard với một khoảng trễ. Hồ sơ người dùng mới và các thay đổi thuộc tính có thể không hiển thị ngay lập tức. ::: Sau khi tạo phân khúc, bạn có thể [sử dụng nó làm **đối tượng** trong Placements và A/B test](audience) để kiểm soát paywall mà người dùng thấy (một hoặc nhiều). Ví dụ: - Hiển thị paywall tiêu chuẩn cho người chưa đăng ký và cung cấp ưu đãi cho người đã hủy gói đăng ký hoặc dùng thử trước đó. - Hiển thị các paywall khác nhau cho người dùng từ các quốc gia khác nhau. - Nhắm mục tiêu người dùng dựa trên dữ liệu attribution từ Apple Search Ads. - Đảm bảo người dùng phiên bản cũ vẫn thấy paywall hiện tại, trong khi phiên bản mới hơn nhận được phiên bản cập nhật. - [Trong Analytics](controls-filters-grouping-compare-proceeds#filter-and-group-data), lọc theo phân khúc để xem hiệu suất của các nhóm người dùng cụ thể. Nhóm theo phân khúc để so sánh hiệu suất hoặc mức đóng góp trong **Tất cả người dùng**. <img src="/assets/shared/img/3244407-Segments.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Tạo phân khúc \{#creation\} Để tạo phân khúc, nhập tên và chọn các thuộc tính định nghĩa bộ lọc của nó. Khi bạn chọn nhiều thuộc tính, người dùng phải thỏa mãn tất cả các điều kiện. Adapty áp dụng logic AND giữa các thuộc tính. <img src="/assets/shared/img/1af9744-new_cohort.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Các thuộc tính có sẵn \{#available-attributes\} :::note Nhiều thuộc tính người dùng được đặt tự động (như **Country** hoặc **Calculated total revenue USD**), nhưng **Age**, **App user ID**, dữ liệu **Attribution**, **Gender** và **Custom attributes** thì không. Bạn phải [đặt thuộc tính người dùng](setting-user-attributes) hoặc [truyền dữ liệu attribution](attribution-integration) nếu muốn dùng chúng để phân khúc. ::: :::tip Đối với các thuộc tính theo ngày, bạn có thể lọc bằng: - **Ngày cố định**: Chọn ngày cụ thể từ lịch (ví dụ: hiển thị ưu đãi đặc biệt cho người dùng cài đặt trong khoảng Black Friday đến Cyber Monday) - **Khoảng thời gian tương đối**: Đặt khoảng thời gian động như "7 ngày qua" hoặc "3 tháng qua" (ví dụ: thu hút lại người dùng chưa mở app từ 30+ ngày trước, hoặc nhắm mục tiêu người cài đặt gần đây) Khoảng thời gian tương đối tự động cập nhật, rất lý tưởng cho các chiến dịch liên tục. Ngày cố định phù hợp nhất cho các chương trình khuyến mãi có thời hạn. ::: | Thuộc tính | Lọc theo | |------------|----------| | **Age** | Độ tuổi của người dùng. Lưu ý rằng độ tuổi được tính khi Adapty nhận được lần đầu tiên và không được cập nhật sau đó. | | **App User ID** | Định danh của người dùng trong ứng dụng của bạn ([customer_user_id](profiles-crm#user-attributes)). Bạn có thể lọc theo sự hiện diện hoặc vắng mặt của nó, ví dụ để chỉ hiển thị paywall cho những người chưa đăng nhập. | | **App version (current)** | Phiên bản hiện tại của ứng dụng được cài đặt trên thiết bị của người dùng mà Adapty nhận dữ liệu sự kiện gần nhất — **được cập nhật khi người dùng nâng cấp**, vì vậy nó luôn phản ánh phiên bản họ đang dùng. Sử dụng thuộc tính này khi triển khai cho tất cả người dùng đang chạy một phiên bản cụ thể, kể cả những người đã nâng cấp lên từ phiên bản cũ hơn. Khi tạo phân khúc, chọn biểu tượng bút chì bên cạnh **App version** và thêm phiên bản mới để sử dụng ngay.<br/> Dùng điều kiện **version > X.X** cho phép bạn đo tác động chuyển đổi của tất cả phiên bản ứng dụng cũ hơn hoặc mới hơn một phiên bản cụ thể mà không cần liệt kê từng phiên bản.<br/><br/> **Định dạng:** Chuỗi phiên bản phải tuân theo định dạng [SemVer](https://semver.org/). Số 0 đứng đầu trong bất kỳ phần nào đều không hợp lệ — `26.03.4` sẽ không khớp, trong khi `26.3.4` sẽ khớp. Các phiên bản không hợp lệ bị loại khỏi phân khúc mà không thông báo. | | **App version (on install)** | Phiên bản ứng dụng được cài đặt trên thiết bị của người dùng khi Adapty nhận dữ liệu sự kiện lần đầu tiên — **được gắn cố định với lần cài đặt đó và không bao giờ cập nhật**, ngay cả sau khi người dùng nâng cấp. Sử dụng thuộc tính này để nhắm mục tiêu người dùng theo phiên bản họ cài đặt ban đầu, không phải phiên bản hiện tại. `App version (on install) = 1.5.7` chỉ khớp với người dùng có lần cài đặt đầu tiên là 1.5.7 và loại trừ những người đã nâng cấp lên 1.5.7 từ phiên bản cũ hơn — để bắt cả người dùng đã nâng cấp, hãy dùng **App version (current)** thay thế.<br/><br/> **Định dạng:** Chuỗi phiên bản phải tuân theo định dạng [SemVer](https://semver.org/). Số 0 đứng đầu trong bất kỳ phần nào đều không hợp lệ — `26.3.04` sẽ không khớp, trong khi `26.3.4` sẽ khớp. Các phiên bản không hợp lệ bị loại khỏi phân khúc mà không thông báo. | | **Attribution: Ad Group** | Nhóm quảng cáo trong attribution. | | **Attribution: Ad Set** | Bộ quảng cáo trong attribution. | | **Attribution: Campaign** | Tên chiến dịch marketing. | | **Attribution: Creative** | Từ khóa creative trong attribution. | | **Attribution: Channel** | Tên kênh marketing. | | **Attribution: Source** | Nguồn gốc của attribution. | | **Attribution: Status** | Trạng thái attribution. Các giá trị có thể: <ul><li> **Organic** – Người dùng cài đặt ứng dụng mà không có tác động từ marketing trả phí (ví dụ: tìm kiếm trực tiếp trên App Store/Google Play, truyền miệng, hoặc mạng xã hội tự nhiên).</li><li> **Non-organic** – Người dùng được tiếp cận thông qua kênh marketing trả phí (ví dụ: quảng cáo, chiến dịch influencer, chương trình giới thiệu).</li><li> **Unknown** – Không có dữ liệu attribution cho người dùng này.</li></ul> | | **Calculated subscription state** | [Trạng thái gói đăng ký hiện tại](profiles-crm#subscription-state) của người dùng, cho biết gói đăng ký đang hoạt động, đã hủy hay có vấn đề thanh toán chưa được giải quyết. | | **Calculated total revenue USD** | Tổng doanh thu từ người dùng này. | | **Country** | Quốc gia của khách hàng, được xác định bởi địa chỉ IP gần nhất. Adapty cập nhật tín hiệu IP tối đa một lần mỗi tuần, vì vậy có thể bị lệch nếu người dùng thay đổi vị trí hoặc dùng VPN. Để nhắm mục tiêu theo quốc gia tài khoản App Store / Play Store của người dùng, hãy dùng **Country from store account**. | | **Country from store account** | Quốc gia liên kết với tài khoản cửa hàng iOS hoặc Android của người dùng. Lưu ý rằng Adapty chỉ thu thập quốc gia của cửa hàng cho thiết bị iOS chạy phiên bản 13 trở lên. | | **Creation date** | Ngày tạo hồ sơ người dùng (khi ứng dụng được cài đặt lần đầu trên thiết bị của người dùng). | | **Device** | Loại thiết bị dựa trên metadata. Ví dụ: 'Samsung Galaxy' hoặc 'iPhone 13'. | | **Gender** | Giới tính của người dùng. Lưu ý rằng bạn tự đặt giá trị này. | | **Installation date** | Ngày người dùng cài đặt ứng dụng. | | **Language** | Ngôn ngữ trên thiết bị của người dùng. <Callout type="warning">Adapty lưu trữ ngôn ngữ dưới dạng mã `ISO 639-1` 2 chữ cái. Không sử dụng locale mở rộng như `zh-Hant-TW` hay `pt-BR`. Chúng có thể xuất hiện trong dropdown nhưng không khớp với người dùng nào.</Callout> <Callout type="tip">Để thu hẹp thêm nhắm mục tiêu theo ngôn ngữ, kết hợp **Language** với **Country**. Ví dụ: **Tiếng Trung giản thể (`zh`)** + **Country = TW, HK, MO** nhắm đến người dùng chữ Trung phồn thể.</Callout> | | **Last seen** | Ngày gần nhất người dùng mở ứng dụng. | | **OS** | Phiên bản hệ điều hành trên thiết bị của người dùng. | | **Paid access level** | Mức độ truy cập được cấp cho người dùng. | | **Platform** | Nền tảng thiết bị của người dùng. Các giá trị có thể: `iOS`, `macOS`, `iPadOS`, `visionOS`, `Android`. <br/> Nếu người dùng truy cập ứng dụng của bạn từ nhiều nền tảng (ví dụ: iOS và Android), tư cách thành viên phân khúc được đánh giá riêng biệt cho từng nền tảng dựa trên dữ liệu mới nhất từ thiết bị đó. Điều này cho phép nhắm mục tiêu theo nền tảng ngay cả với cùng một hồ sơ người dùng. | | **Subscription expiration date** | Ngày hết hạn gói đăng ký hoặc sự hiện diện/vắng mặt của nó. Hiển thị `none` cho sản phẩm mua một lần trọn đời và để trống nếu người dùng có hồ sơ nhưng chưa từng có dùng thử, gói đăng ký, hay sản phẩm mua một lần trọn đời. | | **Subscription product** | ID sản phẩm gần nhất của gói đăng ký đang hoạt động của khách hàng. | | **[Custom attributes](profiles-crm#custom-attributes)** | Tự định nghĩa thuộc tính để tạo phân khúc nhắm mục tiêu cao dựa trên các đặc điểm riêng của ứng dụng hoặc doanh nghiệp của bạn. | ## Thuộc tính tùy chỉnh \{#custom-attributes\} Định nghĩa các thuộc tính tùy chỉnh để xây dựng phân khúc nhắm mục tiêu hơn dựa trên các đặc điểm riêng của ứng dụng hoặc doanh nghiệp của bạn. :::note - Bạn có thể thiết lập thuộc tính tùy chỉnh trong SDK hoặc Adapty Dashboard. Để thiết lập qua SDK, làm theo hướng dẫn [tại đây](setting-user-attributes#custom-user-attributes). - Thay đổi thuộc tính tùy chỉnh sau khi nó được dùng trong phân khúc có thể làm mất đồng bộ người dùng khỏi phân khúc đó trong [analytics](controls-filters-grouping-compare-proceeds#filter-and-group-data). Dữ liệu sẽ phản ánh giá trị trước đó. ::: ### Cách cấu hình thuộc tính tùy chỉnh \{#how-to-configure-a-custom-attribute\} Trong Adapty Dashboard, chọn **Create custom attributes** từ menu dropdown thuộc tính. <img src="/assets/shared/img/883d3b2-CleanShot_2023-03-16_at_17.20.452x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> | Trường | Mô tả | | ------ |-------| | **Name** | Nhãn cho thuộc tính tùy chỉnh, chỉ dùng trong Adapty Dashboard. | | **Key** | Định danh duy nhất cho thuộc tính. Phải khớp với key được dùng trong SDK. | | **Type** | Chọn giữa:<ul><li>String: Yêu cầu danh sách các giá trị có thể được định nghĩa trước.</li><li>Number: Chỉ chấp nhận giá trị số.</li></ul> | | **Values** | Nếu bạn chọn `String`, nhập danh sách các giá trị có thể. Nếu bạn chọn `Number`, thuộc tính chỉ chấp nhận đầu vào số. Thuộc tính số hỗ trợ giá trị thập phân và có thể dùng với các toán tử so sánh. | Sau khi điền các trường bắt buộc, bạn có thể dùng thuộc tính tùy chỉnh trong phân khúc, [A/B test](ab-tests) và nhiều hơn nữa. Mỗi hồ sơ người dùng có thể có tối đa 30 thuộc tính tùy chỉnh. ## Tổng số và mẫu ngẫu nhiên \{#total-number-and-random-sample\} Sau khi tạo phân khúc, Adapty hiển thị tổng số người dùng thỏa mãn tiêu chí phân khúc. Adapty cũng hiển thị một mẫu ngẫu nhiên gồm 40 người dùng phù hợp với tiêu chí. Dùng nó để kiểm tra phân khúc của bạn và đảm bảo nó được cấu hình đúng. <img src="/assets/shared/img/segment-random-set.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Nhân bản phân khúc \{#duplicate-segments\} Nếu bạn cần một phân khúc tương tự phân khúc đã có, hãy nhân bản nó thay vì xây dựng lại từ đầu. Điều này giúp tiết kiệm thời gian cho các nhóm chạy nhiều chiến dịch hoặc A/B test với các nhóm người dùng chồng lên nhau. Nhân bản phân khúc tạo ra một bản sao với tất cả các bộ lọc và mô tả của nó. Phân khúc mới sẽ có thêm "(copy)" vào tên để phân biệt với bản gốc. Phân khúc mới độc lập với bản gốc. Thay đổi ở một phân khúc không ảnh hưởng đến phân khúc kia. Để nhân bản phân khúc trong Adapty Dashboard: 1. Mở phần **Profiles & Segments** trong menu chính của Adapty và chuyển sang tab [**Segments**](https://app.adapty.io/segments). 2. Nhấp vào nút **3 chấm** bên cạnh phân khúc và chọn **Duplicate**. <img src="/assets/shared/img/duplicate-segment.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Mở phân khúc mới và điều chỉnh bộ lọc theo nhu cầu. ## Xóa phân khúc \{#delete-segments\} Khi bạn không còn cần một phân khúc, bạn có thể xóa nó vĩnh viễn. Adapty chặn việc xóa nếu phân khúc đang được dùng làm đối tượng bởi một trong những trường hợp sau: - **Một placement**: Ít nhất một placement chưa bị xóa đang dùng phân khúc này làm đối tượng. - **Một A/B test (đang chạy hoặc đã hoàn thành)**: Ít nhất một A/B test chưa bị xóa đang dùng phân khúc này làm đối tượng. Đối với việc xóa phân khúc, Adapty coi cả A/B test **đang chạy** lẫn **đã hoàn thành** là đang hoạt động. Một A/B test đã hoàn thành vẫn dùng đối tượng để hiển thị paywall hoặc onboarding sau test cho người dùng phù hợp, và các chỉ số lịch sử của test được giới hạn trong phân khúc đó. Phân khúc chỉ được giải phóng khi bản thân A/B test bị xóa. :::warning Việc xóa phân khúc là vĩnh viễn. Phân khúc không thể được khôi phục. ::: Để xóa phân khúc trong Adapty Dashboard: 1. Vào **Profiles & Segments** trong menu chính của Adapty và chuyển sang tab [**Segments**](https://app.adapty.io/segments). 2. Nhấp vào nút **3 chấm** bên cạnh phân khúc và chọn **Delete**. 3. Nhập tên phân khúc vào trường xác nhận, sau đó nhấp **Delete forever**. :::info Nếu phân khúc đang được sử dụng, hộp thoại sẽ liệt kê các placement và A/B test đang tham chiếu đến nó. Để mở khóa việc xóa, mở từng placement hoặc A/B test trong danh sách và xóa phân khúc khỏi đối tượng của nó hoặc xóa hoàn toàn placement hay A/B test đó. Khi không còn gì tham chiếu đến phân khúc, bạn có thể xóa nó. ::: --- # File: event-feed --- --- title: "Event feed" description: "Theo dõi và phân tích hoạt động người dùng với event feed của Adapty." --- Event feed cho phép bạn theo dõi trực quan các [Sự kiện (Events)](events) được tạo bởi Adapty và kiểm tra trạng thái xuất dữ liệu sang các tích hợp bên thứ ba, bao gồm cả webhook. :::warning Event Feed không hiển thị: - **Giao dịch từ Server-side API v1**: Được tạo bằng [server-side API (phiên bản 1)](server-side-api-specs-legacy#requests). Hãy dùng [server-side API (phiên bản 2)](api-adapty/operations/setTransaction) để chúng xuất hiện. - **Sự kiện không có hồ sơ người dùng**: Các giao dịch đến trước khi SDK xác định được người dùng — ví dụ như thông báo từ server của cửa hàng. Để đưa chúng vào xuất dữ liệu, hãy bật **Include events without profile** trong tích hợp [S3](s3-exports) hoặc [Google Cloud Storage](google-cloud-storage). ::: <img src="/assets/shared/img/event-status.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <p> </p> :::note Trạng thái gửi của AppsFlyer, Facebook Ads và Branch có thể không chính xác vì chúng không phải lúc nào cũng trả về lỗi khi có sự cố xảy ra. ::: Để xem hồ sơ người dùng đã khởi tạo giao dịch, nhấp vào nút **View Profile** trong phần chi tiết sự kiện. --- # File: ab-tests --- --- title: "A/B test" description: "Tối ưu hóa giá gói đăng ký với A/B test trong Adapty để cải thiện tỷ lệ chuyển đổi." --- :::tip Bạn có thể nhận được kế hoạch A/B test cụ thể mà không cần tự nghiên cứu. [AI Growth Advisor](autopilot) kiểm tra paywall của bạn, so sánh với đối thủ cạnh tranh, và đưa ra gợi ý dựa trên dữ liệu ẩn danh từ hơn 20.000 ứng dụng đăng ký được Adapty theo dõi. ::: Tăng doanh thu ứng dụng bằng cách chạy A/B test trong Adapty. So sánh các flow, paywall và onboarding khác nhau để tìm ra phương án chuyển đổi tốt nhất — không cần thay đổi code. Ví dụ, bạn có thể test: - Giá gói đăng ký - Thiết kế, nội dung và bố cục paywall - Thời gian dùng thử và thời hạn gói đăng ký - Thiết kế onboarding ## Điều kiện tiên quyết \{#prerequisites\} Trước khi thiết lập A/B test, bạn cần có: - **Placements**: Một hoặc nhiều [placement](placements) nơi flow, paywall hoặc onboarding được hiển thị. - **Với flow**: Ít nhất hai [flow](adapty-flow-builder). - **Với paywall**: Ít nhất hai [paywall](paywalls). - **Với onboarding**: Ít nhất hai [onboarding](onboardings). :::warning Nếu bạn không sử dụng [Adapty Flow builder](adapty-flow-builder) hoặc [Adapty Paywall builder](adapty-paywall-builder), hãy [gửi lượt xem paywall tới Adapty](present-remote-config-paywalls#track-paywall-view-events) bằng `.logShowFlow()` (iOS SDK v4+) / `.logShowPaywall()`. Nếu không có phương thức này, Adapty sẽ không thể tính toán lượt xem paywall trong A/B test, và số liệu chuyển đổi sẽ không chính xác. ::: ## Các loại A/B test \{#ab-test-types\} Adapty hỗ trợ hai loại A/B test chính: - **Regular**: Chạy trên một placement flow/paywall/onboarding duy nhất. - **Crossplacement**: Chạy trên nhiều placement paywall, hiển thị cùng một biến thể cho người dùng ở khắp nơi. Hiện chỉ hỗ trợ cho paywall. Để so sánh đầy đủ về các loại, trường hợp sử dụng và quy tắc ưu tiên, xem [Các loại A/B test](ab-test-types). ## Các bước tiếp theo \{#next-steps\} - [Growth Autopilot](autopilot) — Phân tích paywall của bạn, nhận thông tin thị trường và tạo kế hoạch A/B test - [Các loại A/B test](ab-test-types) — Tìm hiểu về các loại test và khi nào nên dùng từng loại - [Tạo, chạy và dừng A/B test](run_stop_ab_tests) — Thiết lập và chạy bài test đầu tiên của bạn - [Kết quả và chỉ số A/B test](results-and-metrics) — Hiểu dữ liệu A/B test của bạn và chọn người chiến thắng --- # File: ab-test-types --- --- title: "Các loại A/B test" description: "Tìm hiểu về các loại A/B test trong Adapty." --- Adapty cung cấp hai loại A/B test, mỗi loại phù hợp với các tình huống kiểm thử khác nhau: - **A/B test thông thường:** A/B test được tạo cho một [flow](adapty-flow-builder)/[paywall](paywalls)/[onboarding](onboardings) placement duy nhất. - **A/B test đa placement:** A/B test được tạo cho nhiều paywall placement trong ứng dụng của bạn. Sau khi A/B test gán một <InlineTooltip tooltip="biến thể">Biến thể A/B test là các phiên bản thay thế của flow, paywall hoặc onboarding để kiểm thử.</InlineTooltip>, nó sẽ hiển thị biến thể đó nhất quán trên tất cả các phần đã chọn trong ứng dụng của bạn. :::warning A/B test đa placement chỉ khả dụng cho Adapty SDK từ phiên bản v3.5.0 trở lên. A/B test đa placement chỉ hoạt động với paywall. A/B test cho flow yêu cầu Adapty SDK v4.0.0+. A/B test cho onboarding yêu cầu Adapty SDK v3.8.0+ (iOS, Android, React Native, Flutter), v3.14.0+ (Unity), hoặc v3.15.0+ (Kotlin Multiplatform, Capacitor). Người dùng từ các phiên bản trước sẽ bỏ qua chúng. ::: Mỗi flow/paywall/onboarding được gán một trọng số để phân chia lưu lượng trong suốt quá trình kiểm thử. Ví dụ, với trọng số 70% và 30%, paywall đầu tiên được hiển thị cho khoảng 700 trong số 1.000 người dùng, paywall thứ hai cho khoảng 300 người. Trong A/B test đa placement, trọng số được đặt theo biến thể, không phải theo từng paywall. Cách thiết lập này cho phép bạn so sánh các flow, paywall khác nhau và đưa ra quyết định dựa trên dữ liệu cho chiến lược kiếm tiền của ứng dụng. ## Khi nào nên dùng loại nào \{#when-to-use-each-type\} Mỗi loại A/B test hữu ích trong các trường hợp: - **A/B test thông thường**: - Ứng dụng của bạn chỉ có một placement. - Bạn muốn chạy A/B test trên một placement duy nhất và theo dõi các thay đổi kinh tế chỉ cho placement đó, ngay cả khi ứng dụng có nhiều placement. - Bạn muốn chạy A/B test trên người dùng cũ (những người đã thấy ít nhất một Adapty paywall). - **A/B test đa placement**: - Bạn muốn đồng bộ hóa các biến thể trên nhiều placement. Ví dụ, bạn có thể thay đổi giá trong flow onboarding và trong phần cài đặt ứng dụng cùng một lúc. - Bạn muốn đánh giá tổng thể kinh tế ứng dụng. Chạy kiểm thử trên tất cả các placement giúp thống kê A/B test dễ phân tích hơn so với kiểm thử các placement riêng lẻ. - Bạn muốn chạy A/B test chỉ trên người dùng mới, tức là những người chưa từng thấy một Adapty paywall nào. - Bạn muốn sử dụng nhiều paywall trong một biến thể duy nhất: <img src="/assets/shared/img/ab-test-variants.png" alt="Ví dụ về nhiều paywall trong một biến thể A/B test đa placement" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sự khác biệt chính \{#key-differences\} | Tính năng | A/B Test thông thường | A/B Test đa placement | | ------------------------------- |--------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| | **Đối tượng kiểm thử** | Một flow/paywall/onboarding | Tập hợp paywall thuộc một biến thể | | **Tính nhất quán biến thể** | Biến thể được xác định riêng cho từng placement | Cùng một biến thể được dùng trên tất cả paywall placement | | **Nhắm mục tiêu đối tượng** | Định nghĩa theo từng flow/paywall/onboarding placement | Dùng chung trên tất cả paywall placement | | **Phân tích** | Bạn phân tích một flow/paywall/onboarding placement | Bạn phân tích toàn bộ ứng dụng trên các placement thuộc kiểm thử | | **Phân phối trọng số biến thể** | Theo từng flow/paywall/onboarding | Theo tập hợp paywall | | **Người dùng** | Tất cả người dùng | Chỉ người dùng mới (những người chưa thấy Adapty paywall) | | **Phiên bản Adapty SDK** | Với flow: v4.0.0+. Bất kỳ phiên bản nào với paywall. Với onboarding: v3.8.0+ (iOS, Android, React Native, Flutter), v3.14.0+ (Unity), v3.15.0+ (KMP, Capacitor) | 3.5.0+ | | **Phù hợp nhất cho** | Kiểm thử các thay đổi độc lập trong một flow/paywall/onboarding placement mà không xét đến kinh tế tổng thể của ứng dụng | Đánh giá chiến lược kiếm tiền tổng thể trên toàn ứng dụng | ## Logic lựa chọn A/B test \{#ab-test-selection-logic\} **A/B test đa placement có độ ưu tiên cao hơn A/B test thông thường.** Tuy nhiên, A/B test đa placement chỉ được hiển thị cho **người dùng mới** — những người chưa từng thấy một Adapty paywall nào (phương thức SDK `getPaywall` chưa bao giờ được gọi cho họ). Điều này đảm bảo tính nhất quán của kết quả trên các placement. Sơ đồ sau đây cho thấy logic Adapty sử dụng để chọn A/B test cho một placement: <img src="/assets/shared/img/ab-tests-scheme.webp" alt="Sơ đồ hiển thị logic lựa chọn A/B test cho một paywall placement" style={{ border: '1px solid #727272', /* border width and color */ width: '350px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trên trang **A/B Tests**, các kiểm thử paywall, onboarding, flow và đa placement xuất hiện trên các tab riêng biệt. <img src="ab-tests-tabs.webp" alt="Trang danh sách A/B test với các tab cho loại kiểm thử Regular, Onboarding và Crossplacement" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Giới hạn của A/B test đa placement \{#crossplacement-ab-test-limitations\} :::warning A/B test đa placement không thể bao gồm các placement thuộc flow hoặc onboarding. ::: A/B test đa placement đảm bảo rằng mỗi người dùng thấy cùng một biến thể trên tất cả các placement trong kiểm thử. Điều này tạo ra các giới hạn sau: * Chỉ người dùng mới mới có thể tham gia. Người dùng mới là người chưa thấy Adapty paywall và ứng dụng của họ chưa bao giờ gọi `getPaywall`. Adapty không thể đảm bảo chuỗi paywall nhất quán cho các người dùng khác. * Placement đầu tiên mà người dùng gặp sẽ xác định paywall Adapty hiển thị. Bạn không thể thay đổi việc phân công người dùng hoặc đăng ký cùng một người dùng vào nhiều hơn một A/B test đa placement. :::warning Sau khi người dùng nhận được một paywall đa placement, họ sẽ thấy nó trong 90 ngày, ngay cả sau khi bạn dừng kiểm thử. Để thay đổi thời gian này, trong cài đặt **General**, hãy điều chỉnh **[Cross-placement variation stickiness](general#9-cross-placement-variation-stickiness)**. ::: ## Độ ưu tiên của A/B test đa placement \{#crossplacement-ab-test-priority\} * A/B test đa placement luôn có độ ưu tiên cao hơn A/B test thông thường và A/B test onboarding. Nếu một người dùng mới đủ điều kiện cho cả A/B test đa placement và A/B test thông thường trên cùng một placement, A/B test đa placement sẽ được hiển thị. * Khi nhiều A/B test đa placement với cùng đối tượng chia sẻ cùng một placement, Adapty tự động gán độ ưu tiên kiểm thử dựa trên thứ tự chúng được thêm vào. Kiểm thử đầu tiên có độ ưu tiên cao nhất. Bạn không thể thay đổi điều này theo cách thủ công. * Các kiểm thử nhắm vào phân khúc người dùng nhỏ hơn tự động có độ ưu tiên cao hơn so với những kiểm thử nhắm vào phân khúc Tất cả người dùng. :::note Trong Analytics, một A/B test đa placement xuất hiện dưới dạng nhiều kiểm thử con, mỗi kiểm thử cho một placement. Các kiểm thử con theo mẫu đặt tên `<test-name> child-0`, `<test-name> child-1`, v.v. Việc đánh số khớp với thứ tự placement trên trang chi tiết A/B test. Để xem kết quả cho một placement cụ thể, hãy lọc theo **Placement**. ::: ## Các bước tiếp theo \{#next-steps\} - [Tạo, chạy và dừng A/B test](run_stop_ab_tests) — Thiết lập và khởi chạy kiểm thử đầu tiên của bạn - [Kết quả và chỉ số A/B test](results-and-metrics) — Phân tích hiệu suất và chọn biến thể chiến thắng --- # File: run_stop_ab_tests --- --- title: "Tạo, chạy và dừng A/B test" description: "Hướng dẫn từng bước để tạo, chạy và dừng A/B test trong Adapty." --- Bài viết này trình bày toàn bộ vòng đời của một A/B test trong Adapty: tạo test, chạy test và dừng test khi bạn sẵn sàng xem kết quả. ## Yêu cầu trước khi bắt đầu \{#prerequisites\} Trước khi thiết lập A/B test, bạn cần có: - Ít nhất hai [flow](adapty-flow-builder)/[paywall](paywalls)/[onboarding](onboardings) đã được tạo - Một [placement](placements) đã được cấu hình trong ứng dụng của bạn :::warning Nếu bạn không sử dụng [Adapty Flow builder](adapty-flow-builder) hoặc [Adapty Paywall builder](adapty-paywall-builder), hãy [gửi lượt xem paywall lên Adapty](present-remote-config-paywalls#track-paywall-view-events) bằng `.logShowPaywall()`. Nếu không có phương thức này, Adapty không thể tính lượt xem paywall trong test, và số liệu chuyển đổi sẽ không chính xác. ::: :::info A/B test trong Adapty hoạt động theo hai bước. Bạn tạo test trước và lưu dưới dạng bản nháp — test chưa chạy ngay lập tức. Khi đã sẵn sàng, bạn mới chạy test riêng. Điều này cho phép bạn xem lại cài đặt trước khi người dùng thấy nó. ::: ## Tạo A/B test \{#create-an-ab-test\} Khi tạo A/B test mới, bạn cần có ít nhất hai [flow](adapty-flow-builder)/[paywall](paywalls)/[onboarding](onboardings). Để tạo A/B test mới: 1. Vào mục [A/B tests](ab-tests) từ menu chính của Adapty. <img src="/assets/shared/img/go-to-abtests.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Ở góc trên bên phải, nhấn **Create A/B test**. <img src="/assets/shared/img/create-abtest.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Create the A/B test**, nhập **Test name**. Đây là trường bắt buộc. Hãy chọn tên mô tả rõ nội dung test để bạn dễ nhận biết khi xem kết quả. 4. Điền **Test goal** để mô tả mục tiêu bạn muốn đạt được (ví dụ: tăng gói đăng ký hoặc giảm tỷ lệ rời bỏ). 5. Nhấn **Select placement** và chọn placement cho flow, paywall hoặc onboarding. 6. Thiết lập nội dung test trong bảng **Variants**. Mỗi hàng là một biến thể, mỗi cột là một placement. Thêm paywall vào từng ô giao nhau. Theo mặc định, bảng có 2 biến thể và 1 placement. Bạn có thể thêm tối đa 20 biến thể. Khi bạn thêm placement thứ hai, test sẽ trở thành Crossplacement A/B test. Lưu ý rằng crossplacement A/B test chỉ khả dụng cho paywall. <img src="/assets/shared/img/abtest-variants.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> 7. Lưu test. Bạn có hai lựa chọn: 1. **Save as draft**: Test sẽ không chạy ngay. Bạn có thể khởi chạy sau từ placement hoặc danh sách A/B test. Dùng tùy chọn này để xem lại cài đặt trước khi khởi chạy. 2. **Run A/B test**: Khởi chạy test ngay lập tức. Test sẽ chạy ngay khi bạn nhấn nút này. Sau khi lưu dưới dạng bản nháp, tiếp tục đến phần [Chạy A/B test](#run-an-ab-test). ## Chỉnh sửa A/B test \{#edit-an-ab-test\} Bạn chỉ có thể chỉnh sửa các A/B test đang ở trạng thái bản nháp. Khi test đã chạy, bạn không thể thay đổi nó nữa. Để cập nhật test đang chạy, hãy dùng tùy chọn **Modify** — thao tác này tạo một bản sao với cùng tên để bạn có thể thực hiện thay đổi. Adapty sẽ dừng test gốc, và cả test gốc lẫn phiên bản đã chỉnh sửa sẽ hiển thị riêng biệt trong analytics của bạn. ## Chạy A/B test \{#run-an-ab-test\} Chạy A/B test trong Adapty có nghĩa là gán test đó vào một placement để test có thể bắt đầu hiển thị paywall và onboarding cho người dùng. 1. Vào mục [A/B tests](ab-tests) từ menu chính của Adapty. 2. Đảm bảo bạn đang xem đúng danh sách — các A/B test **Paywall**, **Flow**, **Onboardings** và **Crossplacement** được hiển thị trong các tab riêng biệt mà bạn có thể chuyển đổi qua lại. <img src="/assets/shared/img/ab-tests-tabs.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Chuyển sang tab **Drafts**. Chỉ các test ở dạng bản nháp mới có thể được khởi chạy. 4. Bên cạnh test bạn muốn khởi chạy, nhấn **Run A/B test**. <img src="/assets/shared/img/run-ab-test-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Cửa sổ **Edit A/B test** sẽ mở ra. Xem lại cài đặt và thực hiện các thay đổi cuối cùng nếu cần. Nếu thiếu placement hoặc đối tượng, hãy thêm vào ngay bây giờ. 6. Sau khi xem lại cài đặt, nhấn **Run A/B test** để bắt đầu. Sau khi khởi chạy test, bạn có thể theo dõi tiến trình và xem dữ liệu hiệu suất trên trang [Kết quả và chỉ số A/B test](results-and-metrics). ## Dừng A/B test \{#stop-an-ab-test\} Khi bạn dừng A/B test, test sẽ kết thúc và bạn có thể xem kết quả. Bạn cũng quyết định nội dung nào sẽ hiển thị cho người dùng trong các placement bị ảnh hưởng sau khi test kết thúc. <img src="/assets/shared/img/stop-ab-test.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 1. Mở mục [A/B tests](https://app.adapty.io/ab-tests) và chuyển sang tab **Live**. 2. Bên cạnh test bạn muốn dừng, nhấn vào menu ba chấm, rồi chọn **Stop A/B test**. 3. Trong cửa sổ **Stop the A/B test**, quyết định điều gì sẽ xảy ra sau khi test kết thúc. Bạn có ba lựa chọn: | Lựa chọn | Mô tả | |----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Display one of the tested paywalls/onboardings | Chọn paywall hoặc onboarding chiến thắng dựa trên kết quả test như doanh thu, xác suất tốt nhất (**P2BB**) và doanh thu trên 1K người dùng. Paywall hoặc onboarding này sẽ được hiển thị cho placement và đối tượng đã chọn. | | Select paywalls/onboardings that don't participate in A/B test | Chọn bất kỳ paywall hoặc onboarding nào không thuộc A/B test hiện tại. Dùng tùy chọn này khi không có biến thể nào đáp ứng mục tiêu của bạn. | | Don't show any specific paywall/onboarding | Đối với placement và đối tượng đã chọn, sẽ không có paywall hoặc onboarding cụ thể nào được chọn sau khi A/B test kết thúc. Thay vào đó, paywall hoặc onboarding tiếp theo dựa trên độ ưu tiên đối tượng sẽ được hiển thị. Đây là lựa chọn phù hợp nếu bạn muốn để hệ thống hiện tại quyết định paywall hoặc onboarding nào sẽ hiển thị, mà không cần chọn thủ công. | :::note Dừng A/B test là hành động không thể hoàn tác — test không thể được khởi chạy lại. Hãy đảm bảo bạn đã thu thập đủ dữ liệu trước khi quyết định dừng. ::: 4. Nhấn nút **Stop and complete this A/B test**. Sau khi A/B test kết thúc, test sẽ không còn hoạt động nữa, và các paywall hoặc onboarding trong test sẽ không còn hiển thị cho người dùng mới. Bạn vẫn có thể truy cập kết quả và chỉ số A/B test trên [trang chỉ số A/B test](results-and-metrics#metrics-controls) để xem lại hiệu suất của những người dùng đã tham gia trong thời gian test chạy. Các chỉ số có thể tiếp tục cập nhật khi các sự kiện mua hàng hoặc doanh thu mới được gán cho những người dùng đó. --- # File: ab-test-no-paywall-variants --- --- title: "Thêm biến thể A/B test không có flow hoặc paywall" description: "Chạy A/B test trong đó một biến thể bỏ qua flow hoặc paywall, sử dụng cờ Remote Config để kiểm soát việc hiển thị." --- Bạn có thể đo lường tác động của flow hoặc paywall bằng cách chạy A/B test với một biến thể trống. Một biến thể hiển thị flow/paywall; biến thể kia không hiển thị gì cả. Ứng dụng của bạn đọc một cờ từ Remote Config để quyết định có render hay không. ## Cách hoạt động \{#how-it-works\} Cách thiết lập sử dụng hai flow/paywall trong cùng một placement: - **Flow/Paywall A**: Flow hoặc paywall bạn muốn kiểm thử, với `show_paywall` được đặt thành `true` trong Remote Config của nó. - **Flow/Paywall B**: Một flow hoặc paywall trống với `show_paywall` được đặt thành `false` trong Remote Config của nó. Khi SDK trả về một flow hoặc paywall, ứng dụng của bạn đọc cờ `show_paywall`. Nếu cờ là `true`, ứng dụng render nó. Nếu cờ là `false`, ứng dụng bỏ qua việc render và người dùng tiếp tục mà không thấy gì. ## 1. Thêm cờ show_paywall vào Remote Config \{#1-add-the-show_paywall-flag-in-remote-config\} Bạn cần hai flow hoặc paywall trong cùng một placement: Flow/Paywall A (cái bạn muốn kiểm thử) và Flow/Paywall B (một cái trống). Thêm trường `show_paywall` vào mỗi cái để ứng dụng của bạn có thể phân nhánh theo cùng một key cho cả hai biến thể. Để thêm cờ vào Flow/Paywall A: 1. Mở mục [**Flows**](https://app.adapty.io/flows)/[**Paywalls**](https://app.adapty.io/paywalls) trong menu chính của Adapty và chọn Flow/Paywall A. 2. Mở mục **Remote config**. 3. Tạo một trường với tên `show_paywall` và giá trị `true`. Trong chế độ xem **JSON**, mục này trông như sau: ```json showLineNumbers { "show_paywall": true } ``` 4. Lưu các thay đổi. Lặp lại các bước tương tự cho Flow/Paywall B, nhưng đặt `show_paywall` thành `false`. Để biết thêm chi tiết về Remote Config, xem [Tùy chỉnh flow với Remote Config](customize-flow-with-remote-config) hoặc [Thiết kế paywall với Remote Config](customize-paywall-with-remote-config). :::tip Đặt `show_paywall` trên cả hai biến thể giúp đường dẫn code giống nhau cho cả hai nhóm và giúp việc mở rộng thêm biến thể sau này dễ dàng hơn. ::: ## 2. Thiết lập A/B test \{#2-set-up-the-ab-test\} 1. [Tạo một A/B test](run_stop_ab_tests) trên placement và thêm cả hai flow/paywall làm biến thể. 2. Đặt trọng số biến thể để phân chia lưu lượng giữa người dùng thấy flow/paywall và người dùng không thấy. ## 3. Kiểm tra cờ trong ứng dụng của bạn \{#3-check-the-flag-in-your-app\} Đọc `show_paywall` từ Remote Config được SDK trả về. Nếu cờ là `false`, bỏ qua việc render và để người dùng tiếp tục. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> ```swift showLineNumbers do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") let config = flow.remoteConfigs.first(where: { $0.locale == "en" }) ?? flow.remoteConfigs.first let showPaywall = config?.dictionary?["show_paywall"] as? Bool ?? true if showPaywall { // render the flow or paywall } } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Android"> ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value val showPaywall = paywall.remoteConfig?.dataMap?.get("show_paywall") as? Boolean ?: true if (showPaywall) { // Render the paywall } } is AdaptyResult.Error -> { // handle the error } } } ``` </TabItem> <TabItem value="react-native" label="React Native"> ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: "YOUR_PLACEMENT_ID" }); const showPaywall = paywall.remoteConfig?.data?.["show_paywall"] ?? true; if (showPaywall) { // Render the paywall } } catch (error) { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter"> ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID"); final bool showPaywall = paywall.remoteConfig?.dictionary?['show_paywall'] as bool? ?? true; if (showPaywall) { // Render the paywall } } on AdaptyError catch (adaptyError) { // handle the error } ``` </TabItem> <TabItem value="unity" label="Unity"> ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", (paywall, error) => { if (error != null) { // handle the error return; } var showPaywall = paywall.RemoteConfig?.Dictionary?["show_paywall"] as bool? ?? true; if (showPaywall) { // Render the paywall } }); ``` </TabItem> <TabItem value="kmp" label="Kotlin Multiplatform"> ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID" ).onSuccess { paywall -> val showPaywall = paywall.remoteConfig?.dataMap?.get("show_paywall") as? Boolean ?: true if (showPaywall) { // Render the paywall } }.onError { error -> // handle the error } ``` </TabItem> <TabItem value="capacitor" label="Capacitor"> ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID' }); const showPaywall = paywall.remoteConfig?.data?.['show_paywall'] ?? true; if (showPaywall) { // Render the paywall } } catch (error) { // handle the error } ``` </TabItem> </Tabs> Giá trị mặc định `true` giữ cho flow/paywall hiển thị khi cờ bị thiếu, do đó các flow/paywall hiện có không có cờ này sẽ không bị ảnh hưởng. :::important Nếu bạn tự render paywall (không dùng [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder)), hãy gọi [`logShowFlow` (iOS SDK v4+) / `logShowPaywall`](present-remote-config-paywalls#track-paywall-view-events) khi hiển thị Flow/Paywall A. Nếu không, Adapty không thể đếm lượt xem trong bài test. Không ghi lại lượt xem cho Flow/Paywall B vì nó không bao giờ được hiển thị. ::: ## Các bước tiếp theo \{#next-steps\} - [Tạo, chạy và dừng A/B test](run_stop_ab_tests) — Thiết lập bài test bao gồm cả hai biến thể - [Kết quả và chỉ số A/B test](results-and-metrics) — So sánh biến thể trống với flow/paywall của bạn --- # File: results-and-metrics --- --- title: "Kết quả và chỉ số A/B test" description: "Phân tích kết quả và các chỉ số quan trọng trong Adapty để cải thiện hiệu suất gói đăng ký và mức độ tương tác của người dùng trong ứng dụng." --- Khám phá dữ liệu và thông tin quan trọng từ [A/B test](ab-tests) của chúng tôi, so sánh các paywall và onboarding khác nhau để xem chúng ảnh hưởng thế nào đến hành vi người dùng, mức độ tương tác và tỷ lệ chuyển đổi. Bằng cách xem xét các chỉ số và kết quả ở đây, bạn có thể đưa ra những quyết định thông minh và cải thiện hiệu suất ứng dụng. Hãy đi sâu vào dữ liệu để tìm ra những thông tin có thể hành động và nâng cao sự thành công của ứng dụng. ## Kết quả A/B test \{#a-b-test-results\} Dưới đây là ba chỉ số mà Adapty cung cấp cho kết quả A/B test: <img src="/assets/shared/img/ab-test-results.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: **Revenue**: Chỉ số này hiển thị tổng số tiền thu được bằng USD từ các giao dịch mua và gia hạn, trừ đi các khoản hoàn tiền đã trả cho người dùng. Nó bao gồm cả lần mua đầu tiên và các lần gia hạn gói đăng ký tiếp theo. Revenue giúp bạn hiểu mỗi biến thể A/B test đang hoạt động thế nào về mặt tài chính và xác định biến thể nào mang lại nhiều doanh thu nhất. Tìm hiểu thêm về các chỉ số [paywall](paywall-metrics). **Probability to be best**: Adapty sử dụng một framework phân tích toán học mạnh mẽ để phân tích kết quả A/B test và cung cấp chỉ số gọi là Probability to be best. Chỉ số này đánh giá khả năng một biến thể cụ thể là lựa chọn hoạt động tốt nhất (về mặt doanh thu dài hạn) trong số tất cả các biến thể được kiểm tra. Chỉ số được biểu thị dưới dạng phần trăm từ 1% đến 100%. Để biết thông tin chi tiết về cách Adapty tính toán chỉ số này, vui lòng tham khảo [tài liệu.](maths-behind-it) Lựa chọn hoạt động tốt nhất, được xác định bởi Revenue per 1K user, được đánh dấu màu xanh lá cây và tự động được chọn làm lựa chọn mặc định. **Revenue per 1K users**: Chỉ số revenue per 1K users tính toán doanh thu trung bình được tạo ra trên mỗi 1.000 người dùng cho mỗi biến thể A/B test. Chỉ số này giúp bạn hiểu hiệu quả doanh thu của các biến thể, bất kể tổng số người dùng là bao nhiêu. Nó cho phép bạn so sánh hiệu suất của các biến thể khác nhau trên một thang đo chuẩn hóa và đưa ra quyết định sáng suốt dựa trên hiệu quả tạo ra doanh thu. **Prediction intervals for revenue 1K users**: Chỉ số revenue per 1K users cũng bao gồm các khoảng dự đoán. Các khoảng dự đoán này đại diện cho phạm vi mà doanh thu thực sự trên mỗi 1.000 người dùng cho một biến thể nhất định được dự đoán sẽ nằm trong đó, dựa trên dữ liệu hiện có và phân tích thống kê. Trong bối cảnh A/B testing, khi phân tích doanh thu được tạo ra bởi các biến thể khác nhau, chúng tôi tính toán doanh thu trung bình trên mỗi 1.000 người dùng cho mỗi biến thể. Vì doanh thu có thể khác nhau giữa các người dùng, các khoảng dự đoán cung cấp dấu hiệu rõ ràng về các giá trị hợp lý cho doanh thu trên mỗi 1.000 người dùng, có tính đến sự biến đổi và không chắc chắn liên quan đến quá trình dự đoán. Bằng cách tích hợp các khoảng dự đoán vào chỉ số revenue per 1K users, Adapty cho phép bạn đánh giá hiệu quả doanh thu của các biến thể A/B test trong khi xem xét phạm vi kết quả doanh thu tiềm năng. Thông tin này giúp bạn đưa ra quyết định dựa trên dữ liệu và tối ưu hóa chiến lược gói đăng ký một cách hiệu quả, bằng cách tính đến sự không chắc chắn trong quá trình dự đoán và các giá trị hợp lý cho doanh thu trên mỗi 1.000 người dùng. Bằng cách phân tích các chỉ số này do Adapty cung cấp, bạn có thể có được cái nhìn sâu sắc về hiệu suất tài chính, ý nghĩa thống kê và hiệu quả doanh thu của các biến thể A/B test, cho phép bạn đưa ra quyết định dựa trên dữ liệu và tối ưu hóa chiến lược gói đăng ký một cách hiệu quả. ## Chỉ số A/B test \{#ab-test-metrics\} Adapty cung cấp một bộ chỉ số toàn diện để giúp bạn đo lường hiệu quả hiệu suất của A/B test được thực hiện trên các biến thể paywall hoặc onboarding của bạn. Các chỉ số này được cập nhật liên tục theo thời gian thực, ngoại trừ lượt xem được cập nhật định kỳ. Hiểu các chỉ số này sẽ giúp bạn đánh giá hiệu quả của các biến thể khác nhau và đưa ra quyết định dựa trên dữ liệu để tối ưu hóa chiến lược paywall hoặc onboarding của bạn. Các chỉ số A/B test có sẵn trong danh sách A/B test, nơi bạn có thể có cái nhìn tổng quan về hiệu suất của tất cả A/B test. Chế độ xem toàn diện này cung cấp các chỉ số tổng hợp cho mỗi biến thể kiểm tra, cho phép bạn so sánh hiệu suất của chúng và xác định các khác biệt đáng kể. Để phân tích chi tiết hơn từng A/B test, bạn có thể truy cập các chỉ số chi tiết A/B Test. Phần này cung cấp các chỉ số chuyên sâu dành riêng cho A/B test đã chọn, cho phép bạn đi sâu vào hiệu suất của từng biến thể. Tất cả các chỉ số, ngoại trừ lượt xem, được gán cho sản phẩm trong paywall hoặc onboarding. ## Lọc chỉ số theo ngày cài đặt \{#filter-metrics-by-install-date\} Các chỉ số về paywall, trial và mua hàng có thể được nhóm theo hai loại ngày khác nhau: - **Ngày sự kiện** — khi paywall được xem, trial bắt đầu, hoặc giao dịch mua xảy ra. - **Ngày cài đặt** — khi người dùng lần đầu mở ứng dụng. Hai chế độ xem này có thể hiển thị các con số rất khác nhau cho cùng một khoảng thời gian. Hộp kiểm **Filter metrics by install date** kiểm soát chế độ nào được dashboard sử dụng: - **Bỏ chọn (mặc định)**: Các chỉ số được nhóm theo ngày sự kiện. - **Đã chọn**: Các chỉ số được nhóm theo ngày cài đặt. **Ví dụ.** Bạn đặt khoảng thời gian từ ngày 1–30 tháng 4 và xem các trial. - **Bỏ chọn**: Hiển thị các trial *bắt đầu* trong tháng 4, bất kể người dùng đó cài đặt ứng dụng khi nào. - **Đã chọn**: Hiển thị các trial từ những người dùng *đã cài đặt* trong tháng 4, bất kể trial của họ bắt đầu khi nào. Dùng chế độ xem theo ngày cài đặt để đo hiệu quả thu hút người dùng cho một cohort cụ thể. Dùng chế độ xem theo ngày sự kiện để đo hoạt động paywall hoặc onboarding trong một khoảng thời gian cụ thể. ## Điều khiển chỉ số \{#metrics-controls\} Hệ thống hiển thị các chỉ số dựa trên khoảng thời gian đã chọn và sắp xếp chúng theo tham số cột bên trái với ba mức thụt lề. ### Khoảng thời gian \{#time-ranges\} Bạn có thể chọn từ nhiều khoảng thời gian để phân tích dữ liệu chỉ số, cho phép bạn tập trung vào các khoảng thời gian cụ thể như ngày, tuần, tháng hoặc phạm vi ngày tùy chỉnh. <img src="/assets/shared/img/ab-test-time-ranges.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển analytics](controls-filters-grouping-compare-proceeds) ::: Adapty cung cấp các công cụ mạnh mẽ để lọc và tùy chỉnh phân tích chỉ số phù hợp với nhu cầu của bạn. Với trang chỉ số của Adapty, bạn có thể truy cập vào nhiều khoảng thời gian, tùy chọn nhóm và khả năng lọc khác nhau. - ✅ Lọc theo: Đối tượng, attribution, quốc gia, paywall, trạng thái paywall, nhóm paywall, onboarding, placement, quốc gia, cửa hàng, sản phẩm và cửa hàng sản phẩm. - ✅ Nhóm theo: Sản phẩm và cửa hàng. :::note Khi bạn lọc theo A/B test, các A/B test xuyên placement xuất hiện dưới dạng các test con riêng lẻ (ví dụ: `My test child-0`, `My test child-1`), mỗi placement một test. Xem [Giới hạn của A/B test xuyên placement](ab-test-types#crossplacement-ab-test-limitations) để biết chi tiết. ::: ## Biểu đồ chỉ số đơn lẻ \{#single-metrics-chart\} Một trong những thành phần chính của trang chỉ số paywall hoặc onboarding là phần biểu đồ, trực quan hóa các chỉ số đã chọn và tạo điều kiện phân tích dễ dàng. <img src="/assets/shared/img/e6b0674-Area.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Phần biểu đồ trên trang chỉ số A/B test bao gồm biểu đồ thanh ngang trực quan hóa các giá trị chỉ số đã chọn. Mỗi thanh trong biểu đồ tương ứng với một giá trị chỉ số và có kích thước tỷ lệ, giúp dễ dàng hiểu dữ liệu ngay lập tức. Đường ngang chỉ ra khoảng thời gian đang được phân tích, và cột dọc hiển thị các giá trị số của các chỉ số. Tổng giá trị của tất cả các giá trị chỉ số được hiển thị bên cạnh biểu đồ. Ngoài ra, nhấp vào biểu tượng mũi tên ở góc trên bên phải của phần biểu đồ sẽ mở rộng chế độ xem, hiển thị các chỉ số đã chọn trên toàn bộ đường của biểu đồ. ## Tóm tắt A/B test \{#ab-test-summary\} Bên cạnh biểu đồ chỉ số đơn lẻ, phần tóm tắt chi tiết A/B test được hiển thị, bao gồm thông tin về trạng thái, thời gian, placement và các chi tiết liên quan khác về A/B test. <img src="/assets/shared/img/90fa3f5-Area.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Định nghĩa các chỉ số \{#metrics-definitions\} Dưới đây là các chỉ số quan trọng có sẵn cho A/B test: <img src="/assets/shared/img/30c7b68-Area.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Revenue \{#revenue\} Revenue đại diện cho tổng số tiền thu được bằng USD từ các giao dịch mua và gia hạn từ A/B test. Nó bao gồm lần mua đầu tiên và các lần gia hạn gói đăng ký tiếp theo. Chỉ số revenue được tính trước khi khấu trừ hoa hồng App Store hoặc Play Store. Tìm hiểu thêm về các chỉ số [revenue của paywall](paywall-metrics#revenue). ### CR to purchases \{#cr-to-purchases\} Tỷ lệ chuyển đổi sang giao dịch mua đo lường hiệu quả của A/B test trong việc chuyển đổi lượt xem thành các giao dịch mua thực tế. Nó được tính bằng cách chia số lượng giao dịch mua cho số lượt xem. Ví dụ: nếu bạn có 10 giao dịch mua và 100 lượt xem, tỷ lệ chuyển đổi sang giao dịch mua sẽ là 10%. ### CR trials \{#cr-trials\} Tỷ lệ chuyển đổi (CR) sang dùng thử là số lượt dùng thử được bắt đầu từ A/B test chia cho số lượt xem. Tỷ lệ chuyển đổi sang dùng thử đo lường hiệu quả của A/B test trong việc chuyển đổi lượt xem thành các lượt kích hoạt dùng thử. Nó được tính bằng cách chia số lượt dùng thử được bắt đầu cho số lượt xem. ### Purchases \{#purchases\} Chỉ số purchases đại diện cho tổng số giao dịch được thực hiện trong paywall hoặc onboarding từ A/B test. Nó bao gồm các loại giao dịch mua sau: - Các giao dịch mua mới được thực hiện. - Chuyển đổi dùng thử của các lượt dùng thử đã được kích hoạt. - Hạ cấp, nâng cấp và chuyển đổi ngang giữa các gói đăng ký. - Khôi phục gói đăng ký (ví dụ: khi gói đăng ký hết hạn mà không có tự động gia hạn và sau đó được khôi phục). Xin lưu ý rằng các lần gia hạn không được tính vào chỉ số purchases. ### Trials \{#trials\} Chỉ số trials cho biết tổng số lượt dùng thử được kích hoạt từ A/B test. ### Trials cancelled \{#trials-cancelled\} Chỉ số trials cancelled đại diện cho số lượt dùng thử mà tính năng tự động gia hạn đã bị tắt. Điều này xảy ra khi người dùng hủy đăng ký thủ công khỏi bản dùng thử. ### Refunds \{#refunds\} Refunds cho A/B test đại diện cho số lượng giao dịch mua và gói đăng ký được hoàn tiền liên quan đến các biến thể đã được kiểm tra. ### Views \{#views\} Views là số lượt xem các paywall hoặc onboarding mà A/B test bao gồm. Nếu người dùng truy cập hai lần, điều này sẽ được tính là hai lượt truy cập. ### Unique views \{#unique-views\} Unique views là số lượt xem duy nhất của paywall hoặc onboarding. Nếu người dùng truy cập hai lần, điều này sẽ được tính là một lượt xem duy nhất. ### Probability to be the best \{#probability-to-be-the-best\} Chỉ số Probability to be the best định lượng khả năng một biến thể cụ thể trong A/B test là lựa chọn hoạt động tốt nhất trong số tất cả các paywall hoặc onboarding đã được kiểm tra. Nó cung cấp xác suất số cho biết hiệu suất tương đối của mỗi paywall hoặc onboarding. Chỉ số được biểu thị dưới dạng phần trăm từ 1% đến 100%. ### ARPU (Average revenue per user) \{#arpu-average-revenue-per-user\} Chỉ dành cho A/B test onboarding. Đo lường doanh thu trung bình được tạo ra từ mỗi người dùng trong một khoảng thời gian cụ thể. Nó được tính bằng cách chia tổng doanh thu cho số lượng người dùng duy nhất. ### ARPPU (Average revenue per paying user) \{#arppu-average-revenue-per-paying-user\} ARPPU là viết tắt của Average Revenue Per Paying User từ A/B test. Nó được tính bằng tổng doanh thu chia cho số lượng người dùng trả tiền duy nhất. Ví dụ: nếu bạn đã tạo ra $15.000 doanh thu từ 1.000 người dùng trả tiền, ARPPU sẽ là $15. ### ARPAS (Average revenue per active subscriber) \{#arpas-average-revenue-per-active-subscriber\} ARPAS là chỉ số cho phép bạn đo lường doanh thu trung bình được tạo ra trên mỗi người đăng ký hoạt động từ việc chạy A/B test. Nó được tính bằng cách chia tổng doanh thu cho số lượng người đăng ký đã kích hoạt dùng thử hoặc gói đăng ký. Ví dụ: nếu tổng doanh thu là $5.000 và bạn có 1.000 người đăng ký, ARPAS sẽ là $5. Chỉ số này giúp đánh giá tiềm năng kiếm tiền trung bình trên mỗi người đăng ký. ### Proceeds \{#proceeds\} Chỉ số proceeds cho A/B test đại diện cho số tiền thực tế nhận được bởi chủ ứng dụng bằng USD từ các giao dịch mua và gia hạn sau khi khấu trừ hoa hồng App Store / Play Store áp dụng. Nó phản ánh doanh thu ròng liên quan cụ thể đến các biến thể được kiểm tra trong A/B test, đóng góp trực tiếp vào thu nhập của ứng dụng. Để biết thêm thông tin về cách tính proceeds, bạn có thể tham khảo [tài liệu](analytics-cohorts#revenue-vs-proceeds) Adapty. ### Unique subscribers \{#unique-subscribers\} Chỉ số unique subscribers đại diện cho số lượng cá nhân riêng lẻ đã đăng ký hoặc kích hoạt dùng thử thông qua các biến thể trong A/B test. Nó chỉ tính mỗi người đăng ký một lần, bất kể số lượng gói đăng ký hoặc lượt dùng thử họ bắt đầu. ### Unique paid subscribers \{#unique-paid-subscribers\} Chỉ số unique paid subscribers đại diện cho số lượng cá nhân duy nhất đã hoàn thành thành công một giao dịch mua và trở thành người đăng ký trả tiền thông qua các biến thể trong A/B test. ### Refund rate \{#refund-rate\} Tỷ lệ hoàn tiền cho A/B test được tính bằng cách chia số lượng hoàn tiền liên quan đến các biến thể trong bài kiểm tra cho số lượng giao dịch mua lần đầu (không tính gia hạn). Ví dụ: nếu có 5 lần hoàn tiền và 1000 giao dịch mua lần đầu, tỷ lệ hoàn tiền sẽ là 0,5%. ### Unique CR purchases \{#unique-cr-purchases\} Tỷ lệ chuyển đổi duy nhất sang giao dịch mua cho A/B test được tính bằng cách chia số lượng giao dịch mua liên quan đến các biến thể trong bài kiểm tra cho số lượt xem duy nhất. Ví dụ: nếu có 10 giao dịch mua và 100 lượt xem duy nhất, tỷ lệ chuyển đổi duy nhất sang giao dịch mua sẽ là 10%. ### Unique CR trials \{#unique-cr-trials\} Tỷ lệ chuyển đổi duy nhất sang dùng thử cho A/B test được tính bằng cách chia số lượt dùng thử được bắt đầu liên quan đến các biến thể trong bài kiểm tra cho số lượt xem duy nhất. Ví dụ: nếu có 30 lượt dùng thử được bắt đầu và 100 lượt xem duy nhất, tỷ lệ chuyển đổi duy nhất sang dùng thử sẽ là 30%. ### Completions & unique completions \{#completions--unique-completions\} Chỉ dành cho A/B test onboarding. Completions đếm số lần người dùng hoàn thành onboarding của bạn thông qua các biến thể trong A/B test, tức là họ đi từ màn hình đầu tiên đến màn hình cuối cùng. Nếu ai đó hoàn thành hai lần, đó là hai **completions** nhưng chỉ một **unique completion**. ### Unique completions rate \{#unique-completions-rate\} Chỉ dành cho A/B test onboarding. Số lượng unique completion chia cho số lượt xem duy nhất. Chỉ số này giúp bạn hiểu cách mọi người tương tác với onboarding thông qua các biến thể trong A/B test và thực hiện thay đổi nếu bạn nhận thấy rằng mọi người bỏ qua nó. --- # File: maths-behind-it --- --- title: "Toán học đằng sau A/B test" description: "Hiểu toán học đằng sau phân tích gói đăng ký để có cái nhìn sâu hơn về doanh thu." --- A/B test là một kỹ thuật mạnh mẽ dùng để so sánh hiệu quả của hai phiên bản khác nhau của một flow, paywall hoặc onboarding. Mục tiêu cuối cùng là xác định phiên bản nào hiệu quả hơn dựa trên doanh thu trung bình trên mỗi người dùng trong 12 tháng. Tuy nhiên, việc chờ đủ một năm để thu thập dữ liệu và đưa ra quyết định là không thực tế. Vì vậy, doanh thu trên mỗi người dùng trong 2 tuần được dùng làm chỉ số đại diện, được chọn dựa trên phân tích dữ liệu lịch sử để xấp xỉ chỉ số mục tiêu. Để đạt được kết quả chính xác và đáng tin cậy, cần áp dụng một phương pháp thống kê mạnh mẽ có khả năng xử lý nhiều loại dữ liệu khác nhau. Thống kê Bayesian, một phương pháp phổ biến trong phân tích dữ liệu hiện đại, cung cấp một khung linh hoạt và trực quan cho A/B test. Bằng cách kết hợp kiến thức tiên nghiệm và cập nhật nó với dữ liệu mới, phương pháp Bayesian cho phép đưa ra quyết định tốt hơn trong điều kiện không chắc chắn. Tài liệu này cung cấp hướng dẫn toàn diện về phân tích toán học mà Adapty sử dụng để đánh giá kết quả A/B test và cung cấp thông tin có giá trị cho việc ra quyết định dựa trên dữ liệu. ## Phương pháp phân tích thống kê của Adapty \{#adaptys-approach-to-statistical-analysis\} Adapty áp dụng một phương pháp phân tích thống kê toàn diện để đánh giá hiệu suất của các A/B test và cung cấp thông tin chính xác, đáng tin cậy. Phương pháp của chúng tôi bao gồm các bước chính sau: 1. **Định nghĩa chỉ số:** Để thực hiện A/B test thành công, bạn cần xác định và định nghĩa chỉ số chính phù hợp với mục tiêu cụ thể của phân tích. Adapty đã tận dụng lượng lớn dữ liệu lịch sử từ các ứng dụng gói đăng ký để xác định chỉ số nào phù hợp làm chỉ số đại diện cho mục tiêu dài hạn là doanh thu trung bình sau 1 năm — và đó là ARPU sau 14 ngày. 2. **Xây dựng giả thuyết:** Chúng tôi tạo ra hai giả thuyết cho A/B test. Giả thuyết null (H0) giả định rằng không có sự khác biệt đáng kể giữa nhóm kiểm soát (A) và nhóm thử nghiệm (B). Giả thuyết thay thế (H1) cho rằng có sự khác biệt đáng kể giữa hai nhóm trở lên. 3. **Chọn phân phối:** Chúng tôi chọn họ phân phối tốt nhất dựa trên đặc điểm dữ liệu và chỉ số chúng tôi quan sát. Lựa chọn phổ biến nhất ở đây là phân phối log-normal (có tính đến các giá trị bằng không). 4. **Tính xác suất tốt nhất:** Sử dụng phương pháp Bayesian cho A/B test, chúng tôi tính xác suất là lựa chọn tốt nhất cho mỗi biến thể paywall hoặc onboarding tham gia vào bài test. Giá trị này có liên quan đến p-value mà chúng tôi đã dùng trước đây, nhưng về bản chất đây là một phương pháp khác, mạnh mẽ hơn và dễ hiểu hơn. 5. **Diễn giải kết quả:** Xác suất tốt nhất là đúng như tên gọi của nó. Xác suất càng lớn thì khả năng một lựa chọn cụ thể là tốt nhất càng cao. Bạn cần tự xác định ngưỡng để ra quyết định, điều này nên phụ thuộc vào nhiều yếu tố khác trong tình huống cụ thể của bạn, nhưng ngưỡng xác suất thường dùng là 95%. 6. **Khoảng dự đoán:** Adapty tính toán các khoảng dự đoán cho các chỉ số hiệu suất của mỗi nhóm, cung cấp một dải giá trị mà trong đó tham số tổng thể thực sự có khả năng nằm vào. Điều này giúp định lượng sự không chắc chắn liên quan đến các chỉ số hiệu suất được ước tính. ## Xác định cỡ mẫu \{#sample-size-determination\} Việc xác định cỡ mẫu phù hợp là rất quan trọng để có kết quả A/B test đáng tin cậy và có tính kết luận. Adapty xem xét các yếu tố như độ mạnh thống kê và kích thước hiệu ứng kỳ vọng — vẫn quan trọng ngay cả với phương pháp Bayesian — để đảm bảo cỡ mẫu đủ lớn. Các phương pháp ước tính cỡ mẫu cần thiết, riêng cho phương pháp Bayesian mà chúng tôi đang dùng, đảm bảo độ tin cậy của phân tích. Để tìm hiểu thêm về chức năng của A/B test, chúng tôi khuyến nghị tham khảo tài liệu của chúng tôi về [tạo](ab-tests) và [chạy A/B test](run_stop_ab_tests), cũng như tìm hiểu các [chỉ số và kết quả A/B test](results-and-metrics). Khung phân tích của Adapty cho A/B test hiện sử dụng phương pháp Bayesian, nhưng trọng tâm vẫn là định nghĩa chỉ số, xây dựng giả thuyết và lựa chọn phân phối. Tuy nhiên, thay vì xác định p-value, chúng tôi nay tính toán các phân phối hậu nghiệm và xác suất của mỗi biến thể là tốt nhất. Chúng tôi cũng xác định các khoảng dự đoán. Phương pháp được sửa đổi này, tuy vẫn toàn diện và thậm chí còn mạnh mẽ hơn, được thiết kế để cung cấp thông tin trực quan và dễ diễn giải hơn. Mục tiêu vẫn là giúp các doanh nghiệp tối ưu hóa chiến lược, cải thiện hiệu suất và thúc đẩy tăng trưởng dựa trên phân tích thống kê mạnh mẽ từ các A/B test của họ. --- # File: autopilot-how-it-works --- --- title: "AI Growth Advisor: Cách hoạt động" description: "Hiểu rõ logic đằng sau AI Growth Advisor và tin tưởng chúng tôi để phát triển doanh thu của bạn." --- [AI Growth Advisor](autopilot) giúp bạn xác định những thử nghiệm nào nên chạy, dựa trên dữ liệu hiệu suất thực tế của bạn và cách các ứng dụng tương tự trên thị trường đang hoạt động. Thay vì đoán mò xem điều gì có thể hiệu quả, bạn nhận được các đề xuất cụ thể về những thử nghiệm có khả năng cải thiện kết quả cao hơn. Bài viết này cung cấp cái nhìn minh bạch về cách AI Growth Advisor hoạt động — dữ liệu nào được sử dụng, cách đánh giá các cơ hội, và tại sao một số đề xuất nhất định xuất hiện. Mục tiêu là giúp bạn tự tin sử dụng nó như một phần trong quy trình tăng trưởng của mình. ## AI Growth Advisor thực sự làm gì \{#what-ai-growth-advisor-actually-does\} AI Growth Advisor phân tích các chỉ số ứng dụng và paywall của bạn để tìm ra những thử nghiệm có khả năng tăng doanh thu cao nhất. Nó xem xét: - **Thiết lập hiện tại của bạn**: giá cả, dùng thử, sản phẩm và mức độ chuyển đổi của chúng - **Xu hướng thị trường**: cách các ứng dụng tương tự cơ cấu ưu đãi và mức giá họ đặt ra - **Lịch sử thử nghiệm của bạn**: những A/B test bạn đã chạy và kết quả chúng mang lại - **Tiềm năng tăng trưởng**: những thay đổi nào có khả năng tạo ra sự khác biệt nhất Growth Advisor sử dụng AI để đánh giá tổng hợp các yếu tố trên và chuyển chúng thành các A/B test sẵn sàng chạy ngay. Bạn nhận được một kế hoạch hoàn chỉnh mà không cần tự nghiên cứu đối thủ hay đoán xem nên thử nghiệm điều gì tiếp theo. ## Dữ liệu đằng sau AI Growth Advisor \{#the-data-behind-ai-growth-advisor\} Mỗi khuyến nghị được xây dựng từ ba nguồn dữ liệu chính phối hợp với nhau. #### Dữ liệu riêng của ứng dụng \{#your-apps-own-data\} AI Growth Advisor xem xét hiệu suất hiện tại của ứng dụng: - Chỉ số chuyển đổi trên các paywall của bạn - Cấu trúc giá và sản phẩm Điều này giúp AI Growth Advisor có nền tảng cơ sở để bắt đầu trước khi đề xuất bất kỳ thay đổi nào. :::note Chúng tôi không dùng dữ liệu hiệu suất của ứng dụng bạn để huấn luyện khuyến nghị cho các ứng dụng khác. Dữ liệu của bạn được bảo mật hoàn toàn. ::: #### Phân tích paywall \{#paywall-analysis\} AI Growth Advisor phân tích ảnh chụp màn hình paywall của bạn và so sánh thiết kế với các mẫu đã được kiểm chứng từ những app hoạt động tốt nhất trong danh mục của bạn. Công cụ này đánh giá bố cục, nội dung, cách trình bày các gói đăng ký, cũng như các yếu tố tối ưu chuyển đổi như huy hiệu tiết kiệm hay phần đánh giá. Phân tích này tạo ra hai loại đề xuất: - **Đề xuất dựa trên điểm chuẩn** từ những gì các ứng dụng hoạt động tốt nhất làm khác biệt — mỗi đề xuất được hỗ trợ bởi một thống kê cụ thể (ví dụ: "Được sử dụng bởi 72% ứng dụng Giáo dục hoạt động tốt nhất"). - **Đề xuất phân tích hình ảnh** được AI tạo ra từ ảnh chụp màn hình của bạn, bao gồm các cải tiến về nội dung, thay đổi bố cục và các điều chỉnh thiết kế khác. Những đề xuất này được đưa trực tiếp vào [kế hoạch tăng trưởng](autopilot-growth-plan#view-the-growth-plan) của bạn dưới dạng các giả thuyết mà bạn có thể [triển khai dưới dạng A/B test](autopilot-execute-plan). #### Dữ liệu đối thủ cạnh tranh \{#competitor-data\} AI Growth Advisor so sánh cài đặt của bạn với các ứng dụng tương tự trên thị trường bằng cách sử dụng thông tin công khai như giá cả, cấu trúc gói đăng ký và các mô hình phổ biến trong danh mục của bạn. Những so sánh này mang tính đặc thù theo từng quốc gia, vì giá cả và cấu trúc của đối thủ cạnh tranh khác nhau tùy thị trường. Dữ liệu giá của đối thủ cạnh tranh đến từ các nguồn bên thứ ba và nguồn công khai như App Store — khác biệt so với dữ liệu mạng Adapty được ẩn danh hóa dùng trong phân tích chỉ số. Bằng cách này, bạn đang thử nghiệm những chiến lược đã thực sự hiệu quả với các ứng dụng tương tự, chứ không phải những ý tưởng ngẫu nhiên. Khi xem phân tích, bạn có thể so sánh các benchmark và mức giá của đối thủ cạnh tranh trực tiếp cạnh nhau. Nếu các ứng dụng tương tự đang hoạt động tốt hơn với mức giá hay cấu trúc khác, đó là tín hiệu tốt cho thấy cách tiếp cận tương tự cũng có thể hiệu quả với bạn. :::tip AI Growth Advisor tự động chọn các đối thủ cạnh tranh phù hợp dựa trên những đối thủ mà bạn thực sự có thể cạnh tranh được. Chúng tôi thường khuyến nghị bạn nên theo gợi ý này thay vì thêm các ứng dụng quá vượt trội hoặc quá thua kém so với bạn. Nếu ứng dụng của bạn thuộc nhiều danh mục, bạn có thể điều chỉnh danh sách để tập trung vào phân khúc thị trường phù hợp nhất. ::: #### Điểm chuẩn ngành \{#industry-benchmarks\} AI Growth Advisor dựa trên dữ liệu ẩn danh từ 20.000 ứng dụng gói đăng ký được Adapty theo dõi để cho bạn thấy mình đang ở đâu so với mức trung bình của danh mục trong một quốc gia cụ thể. Dữ liệu được tổng hợp trên toàn mạng lưới và không gắn với bất kỳ ứng dụng cụ thể nào. Ví dụ, phễu chuyển đổi và doanh thu trên mỗi lượt cài đặt của bạn được so sánh với mức trung bình của các ứng dụng trong cùng danh mục và quốc gia. Điều này giúp bạn nhận ra mình đang kém hơn, ngang bằng, hay đã vượt trội so với mức chung. #### Dữ liệu thị trường theo khu vực địa lý \{#geographic-market-data\} AI Growth Advisor phân tích từng thị trường địa lý riêng lẻ — dựa trên các mẫu dữ liệu từ mạng lưới 20.000 ứng dụng của Adapty — để xác định những nơi mà việc điều chỉnh giá theo khu vực có thể tạo ra thêm doanh thu. Với mỗi quốc gia, công cụ này đánh giá: - **Tỷ lệ chuyển đổi**: So sánh tỷ lệ từ lượt cài đặt sang người dùng trả phí với mức trung bình toàn cầu. Tỷ lệ cao hơn có thể cho thấy còn dư địa để tăng giá; tỷ lệ thấp hơn có thể là dấu hiệu người dùng nhạy cảm với giá. - **Chỉ số giá**: Vị trí của quốc gia trong [Adapty Pricing Index](https://uploads.adapty.io/adapty_pricing_index.pdf), phản ánh sức mua của người dân tại đó. Bạn có thể thực hiện theo các đề xuất này bằng cách tạo A/B test từ [gợi ý định giá theo khu vực địa lý](autopilot-growth-plan#geo-pricing-hypotheses) trong kế hoạch tăng trưởng của bạn. ## AI Growth Advisor quyết định đề xuất như thế nào \{#how-ai-growth-advisor-decides-what-to-recommend\} AI Growth Advisor tạo ra một tập hợp các gợi ý để cải thiện tỷ lệ chuyển đổi paywall của bạn. Các gợi ý này được thiết kế để thử nghiệm từng cái một nhằm đo lường chính xác tác động của từng thay đổi. Đây là cách AI Growth Advisor đưa ra các gợi ý: 1. **Tìm ra những cơ hội lớn nhất** AI Growth Advisor xem xét giá cả, sản phẩm và hiệu suất funnel của bạn, sau đó so sánh với các xu hướng ngành và các ứng dụng tương tự. Phân tích chạy theo đơn vị tiền tệ của thị trường chính của bạn — không chỉ USD — để các đề xuất về giá phù hợp với những gì người dùng thực sự thanh toán. Nó tìm kiếm những điểm bạn có nhiều dư địa cải thiện nhất, dù đó là điều chỉnh giá, thêm thời gian dùng thử, hay thay đổi cấu trúc ưu đãi. 2. **Chọn thử nghiệm tiếp theo** Mỗi giả thuyết được tạo ra dựa trên lịch sử thử nghiệm hiện có của bạn. AI Growth Advisor biết bạn đã chạy những thử nghiệm nào, cái nào thắng, và hướng nào vẫn còn đáng khám phá. Gợi ý tiếp theo được xây dựng dựa trên những gì gợi ý trước đó tiết lộ, thay vì tuân theo một trình tự cố định. 3. **Chạy các bài test winner vs. challenger** Sau mỗi thử nghiệm, biến thể thắng trở thành baseline mới của bạn. Kết quả đó định hình gợi ý tiếp theo trong kế hoạch tăng trưởng của bạn — AI Growth Advisor giữ lại những gì đã hiệu quả, loại bỏ những gì không hiệu quả, và chọn bài test tiếp theo từ đó. 4. **Giữ cho mọi thứ thực tế** AI Growth Advisor chỉ đề xuất các test bạn có thể chạy với sản phẩm và cấu hình hiện tại, hoặc với những thay đổi nhỏ như tạo sản phẩm mới hay điều chỉnh giá. Mục tiêu là giữ cho việc kiểm thử nhanh chóng và dễ quản lý. 5. **Giải thích lý do** Với mỗi đề xuất, AI Growth Advisor cung cấp một giả thuyết rõ ràng giải thích chính xác tại sao test này đáng chạy. Bạn sẽ thấy các chỉ số hiện tại của mình so với đối thủ và mức trung bình ngành, cơ hội là gì, và những chỉ số quan trọng nào chúng tôi kỳ vọng sẽ cải thiện. Điều này biến việc thử nghiệm thành một quy trình có thể lặp lại, nơi mỗi bài test đều dạy cho bạn điều gì đó và giúp bạn tiến gần hơn đến một paywall hiệu quả hơn. ## Điều gì xảy ra sau mỗi thí nghiệm \{#what-happens-after-each-experiment\} Các gợi ý không bao giờ cạn. Mỗi A/B test hoàn thành sẽ trở thành cơ sở cho các thí nghiệm tiếp theo. Miễn là bạn tiếp tục thử nghiệm, AI Growth Advisor sẽ tiếp tục đề xuất những điều cần thử tiếp theo. Để làm mới dữ liệu thị trường cơ bản, hãy chạy lại phân tích trên cùng một placement. Mỗi lần chạy lại sẽ lấy dữ liệu giá của đối thủ cạnh tranh, chuẩn benchmark chuyển đổi và xu hướng danh mục được cập nhật, đồng thời bổ sung các giả thuyết mới được xác định vào kế hoạch tăng trưởng của bạn mà không làm ảnh hưởng đến những gì đã có. Các giả thuyết do AI tạo ra, giả thuyết tùy chỉnh và các A/B test đang chạy của bạn đều được giữ nguyên qua các lần chạy lại. Sau khi đã tối ưu hóa baseline, bạn có thể tiếp tục cạnh tranh với các đối thủ mạnh hơn. Cách tiếp cận lặp đi lặp lại này giúp bạn không ngừng tối đa hóa doanh thu khi ứng dụng phát triển và thị trường thay đổi. :::tip Sẵn sàng thử chưa? Khởi chạy [AI Growth Advisor](autopilot-analysis) để phân tích các paywall của bạn và tạo kế hoạch tăng trưởng với các A/B test. Dùng [wizard tích hợp sẵn](autopilot-execute-plan) để triển khai các test phức tạp một cách liền mạch: nó sẽ hướng dẫn bạn qua từng bước tạo sản phẩm, nhân bản paywall và thiết lập phân khúc. ::: --- # File: autopilot-analysis --- --- title: "Phân tích Paywall và Thị trường" description: "Tạo kế hoạch tăng trưởng dựa trên dữ liệu phù hợp với ứng dụng của bạn." --- Làm theo các bước trong bài viết để chạy phân tích AI Growth Advisor và tạo kế hoạch tăng trưởng. Nếu bạn đã tạo kế hoạch tăng trưởng cho placement mục tiêu, phân tích này sẽ tạo ra các giả thuyết mới để bạn lựa chọn. :::tip Hãy đảm bảo bạn đáp ứng các [yêu cầu để phân tích](autopilot#prerequisites) trước khi bắt đầu. ::: ## Phân tích Paywall \{#paywall-analysis\} ### Chọn paywall để phân tích \{#select-a-paywall-for-analysis\} 1. Mở trang **AI Growth Advisor** và nhấp vào nút [Get Growth plan](https://app.adapty.io/ab-tests/analysis/start). 2. Trên trang **Paywall Diagnostic**, chọn **Placement** và **Paywall** từ các menu thả xuống. Adapty tự động chọn placement có doanh thu cao nhất và paywall hàng đầu của nó. Để phân tích một paywall khác, hãy đổi placement trước. 3. Tải lên ảnh chụp màn hình. AI Growth Advisor cần một ảnh chụp màn hình để phân tích thiết kế và nội dung paywall của bạn. 4. Xem lại các sản phẩm đang hoạt động của paywall. Các thẻ sản phẩm ở bên phải hiển thị thời hạn gói đăng ký, giá và thời gian dùng thử của từng sản phẩm. 5. Nhấp vào **Confirm & Analyze** để tiếp tục. Adapty sẽ phân tích paywall của bạn và hiển thị báo cáo chẩn đoán. ### Báo cáo phân tích paywall \{#paywall-analysis-report\} Sau khi bạn chọn một paywall và tải lên ảnh chụp màn hình, Adapty sẽ phân tích paywall của bạn theo các design pattern đã được kiểm chứng, chỉ ra cả những điểm tốt lẫn những cơ hội cải thiện. #### Những gì đang hoạt động tốt \{#whats-working-well\} Phần này làm nổi bật những pattern đã được thiết lập mà bạn đang áp dụng để tối đa hóa tỷ lệ chuyển đổi. Ví dụ: huy hiệu tiết kiệm nổi bật, phần đánh giá người dùng ấn tượng, hoặc bảng so sánh gói đăng ký rõ ràng. #### Những gì cần cải thiện trên paywall của bạn \{#what-to-fix-on-your-paywall\} Adapty chia các đề xuất của mình thành hai loại: - **Đề xuất dựa trên benchmark**: Các gợi ý dựa trên dữ liệu từ những ứng dụng hoạt động tốt nhất trong danh mục của bạn. Mỗi đề xuất đi kèm một chỉ số benchmark (ví dụ: "Được sử dụng bởi 72% ứng dụng Education có hiệu suất cao nhất") và mô tả những gì cần thay đổi. - **Đề xuất từ phân tích hình ảnh**: Các gợi ý do AI tạo ra dựa trên ảnh chụp màn hình paywall của bạn. Bao gồm: cải thiện nội dung, thay đổi bố cục và nhiều hơn nữa. :::tip [Kế hoạch tăng trưởng](autopilot-growth-plan#view-the-growth-plan) của bạn sẽ bao gồm các giả thuyết dựa trên các khuyến nghị được đánh chuẩn. Bạn có thể thêm các đề xuất phân tích hình ảnh vào kế hoạch theo cách thủ công. ::: Nhấp vào **Get Market Insights** để tiếp tục. ## Phân tích Thị trường và Đối thủ cạnh tranh \{#market-and-competitor-analysis\} :::note Phân tích thị trường và đối thủ cạnh tranh yêu cầu hoàn thành [phân tích paywall](#paywall-analysis) trước. ::: Phân tích Market Insights so sánh giá cả và chỉ số chuyển đổi của ứng dụng bạn với các đối thủ cạnh tranh và mức trung bình ngành. Các so sánh được thực hiện theo từng quốc gia. Để cung cấp benchmark, Adapty tổng hợp và phân tích dữ liệu từ các ứng dụng trên App Store trong danh mục phụ và quốc gia của bạn — thông tin này không có sẵn ở nơi khác. ### Chọn đối thủ cạnh tranh \{#select-competitors\} Chọn tối đa 5 đối thủ cạnh tranh để so sánh. Adapty sẽ tự động chọn 5 đối thủ và gợi ý thêm 5 đối thủ nữa. Bạn có thể thêm ứng dụng thủ công bằng liên kết App Store. Để có kết quả tốt hơn, hãy chọn các ứng dụng có MRR cao hơn của bạn. Nhấn **Generate report** để xác nhận danh sách và chờ phân tích hoàn tất. ### Chọn quốc gia \{#select-a-country\} Sử dụng menu thả xuống Country để chọn một trong các quốc gia hàng đầu của bạn để phân tích chi tiết. ### Phân phối doanh thu \{#revenue-distribution\} Biểu đồ phân phối doanh thu này cho thấy doanh thu của bạn đến từ những quốc gia nào, kèm theo tỷ lệ phần trăm chi tiết. Nó làm nổi bật 5 quốc gia hàng đầu của bạn, là trọng tâm của phần còn lại trong phân tích. ### Giá của đối thủ cạnh tranh \{#competitor-pricing\} Bảng giá đối thủ cạnh tranh so sánh giá gói đăng ký của paywall với giá của các đối thủ tại [quốc gia đã chọn](#select-a-country). Bảng bao gồm các cột riêng cho từng thời hạn gói đăng ký. ### Phễu chuyển đổi \{#conversion-funnel\} Biểu đồ hiển thị tỷ lệ chuyển đổi của bạn — Views-to-Trial, Trial-to-Paid và Views-to-Paid — bên cạnh mức trung bình của các ứng dụng tương tự. ### Phân phối doanh thu theo thời hạn \{#revenue-distribution-by-duration\} Biểu đồ này cho thấy các thời hạn gói đăng ký nào đóng góp nhiều nhất vào doanh thu của bạn, so với mức trung bình ngành. Nếu doanh thu của bạn tập trung quá nhiều vào một thời hạn, điều đó có thể cho thấy cơ hội tối ưu hóa chiến lược định giá. ### Activation ARPU Biểu đồ **Activation ARPU: your app vs. category** so sánh doanh thu trung bình trên mỗi lượt cài đặt mới của ứng dụng bạn với mức trung bình của danh mục. Dùng kết hợp với [phễu chuyển đổi](#conversion-funnel): - Phễu chuyển đổi cho thấy có bao nhiêu người dùng trả tiền. - Activation ARPU cho thấy thu nhập trung bình trên mỗi người dùng. Tỷ lệ chuyển đổi cao nhưng Activation ARPU thấp có thể cho thấy mức giá đang bị định quá thấp. Chỉ số này dựa trên **cohort**. Adapty lấy những người dùng đã cài đặt ứng dụng trong 90 ngày gần nhất, rồi chia tổng doanh thu họ tạo ra cho số lượng người dùng đó. #### So sánh với các chỉ số khác \{#comparison-to-other-metrics\} Activation ARPU sẽ không khớp với các giá trị ARPU bạn thấy ở những nơi khác trong dashboard — mỗi chỉ số đo lường một thứ khác nhau. - **[Biểu đồ phân tích ARPU](arpu)**: Bao gồm các lần gia hạn từ các cohort cũ hơn, vì vậy con số này cao hơn Activation ARPU nhiều lần. - **[Biểu đồ doanh thu](revenue), bộ lọc Khoảng thời gian được đặt thành "Activation"**: Chỉ tính lần thanh toán đầu tiên của mỗi người dùng. Không tính các lần gia hạn do cohort thực hiện trong cửa sổ 90 ngày. - **[Doanh thu cohort](analytics-cohorts) (90 ngày)**: Chỉ số tương đương gần nhất — sử dụng chỉ số này làm tham chiếu. ## Các bước tiếp theo \{#next-steps\} Đọc [Thực thi kế hoạch tăng trưởng của bạn](autopilot-execute-plan) để tìm hiểu cách chạy các A/B test dựa trên kết quả phân tích. Bạn luôn có thể xem lại báo cáo phân tích từ kế hoạch tăng trưởng — chuyển sang tab **Analysis Results**. --- # File: autopilot-growth-plan --- --- title: "Quản lý kế hoạch tăng trưởng của bạn" description: "Thêm các giả thuyết tùy chỉnh, lưu trữ chúng và cập nhật kế hoạch tăng trưởng." --- Sau khi hoàn thành [phân tích](autopilot-analysis), Adapty sẽ trình bày kế hoạch tăng trưởng của bạn — một danh sách các **giả thuyết cải tiến có thể thực hiện được**. Mỗi mục gợi ý một mức giá mới hoặc cải tiến thiết kế. Mở một giả thuyết để [thử nghiệm với A/B test](autopilot-execute-plan). Mỗi placement có kế hoạch tăng trưởng riêng. Khi điều kiện thị trường thay đổi, bạn có thể chạy lại phân tích để cập nhật các gợi ý. Các lần chạy trước vẫn được lưu trong lịch sử phiên bản. ## Giả thuyết \{#hypotheses\} Chuyển đổi giữa các tab ở đầu trang kế hoạch tăng trưởng để lọc giả thuyết theo loại: - **Top priority** bao gồm những giả thuyết có tác động cao nhất, xứng đáng được bạn ưu tiên xem xét. Khi không có giả thuyết nào đủ điều kiện, tab này sẽ bị ẩn. - **All** hiển thị tất cả giả thuyết trong kế hoạch đang hoạt động của bạn. - **Pricing** là các giả thuyết khám phá mức giá mới hoặc cấu hình dùng thử. Mỗi giả thuyết dựa trên một khuyến nghị cụ thể từ báo cáo chẩn đoán paywall hoặc báo cáo thông tin thị trường. - **Visual** là các đề xuất cải thiện thiết kế. Chúng có thể liên quan đến thay đổi nội dung, bố cục hoặc các yếu tố hình ảnh khác. - Giả thuyết [**Geo-pricing**](#geo-pricing-hypotheses) kiểm tra các điều chỉnh giá theo từng quốc gia. - Giả thuyết [**Archived**](#archive-a-hypothesis) là những đề xuất bạn đã xóa khỏi kế hoạch đang hoạt động. Bạn có thể khôi phục chúng bất cứ lúc nào. Bạn có thể [thêm giả thuyết của riêng mình](#add-your-own-hypothesis) hoặc [lưu trữ](#archive-a-hypothesis) những giả thuyết bạn không muốn kiểm tra. Hãy kiểm tra từng giả thuyết một, theo bất kỳ thứ tự nào. Các A/B test về định giá theo khu vực địa lý là ngoại lệ — vì đối tượng của chúng không trùng nhau, nên có thể chạy song song. ### Giả thuyết định giá theo địa lý \{#geo-pricing-hypotheses\} :::important Sản phẩm mua một lần không đủ điều kiện để tối ưu hóa giá theo vùng. ::: Mở tab **Geo-pricing** để xem danh sách các đề xuất định giá theo địa lý. Mỗi đề xuất nhắm đến một quốc gia với một mức thay đổi giá duy nhất và chạy như một A/B test riêng biệt. Adapty phát hiện các quốc gia cần điều chỉnh giá và cung cấp các đề xuất dựa trên dữ liệu được xác thực bởi [Adapty Pricing Index](https://uploads.adapty.io/adapty_pricing_index.pdf). <br /> #### Cách Adapty đưa ra gợi ý giá theo khu vực địa lý \{#how-adapty-makes-geo-pricing-suggestions\} - Các đề xuất giá dựa trên dữ liệu App Store. A/B test được tạo ra có thể chạy trên cả App Store và Google Play. - Phần trăm thay đổi giá được áp dụng đồng đều cho tất cả các thời hạn gói đăng ký. - Tất cả các mức giá được làm tròn đến mức giá App Store gần nhất. - Giá hiển thị theo đơn vị tiền tệ địa phương (ví dụ: EUR hoặc GBP) nếu Adapty có dữ liệu giao dịch cho quốc gia đó. Khi không có dữ liệu địa phương, giá sẽ hiển thị bằng USD. ### Huy hiệu trạng thái giả thuyết \{#hypothesis-status-badges\} 1. **Tia sét** — đánh dấu các gợi ý có độ ưu tiên cao nhất. 2. **Trạng thái đồng bộ sản phẩm** — xuất hiện khi cần thực hiện một thao tác với sản phẩm để khởi chạy A/B test. - **Draft** — thiết lập sản phẩm chưa hoàn chỉnh (phía Adapty) - **Action required** — thiết lập sản phẩm chưa hoàn chỉnh (phía cửa hàng) - **Pending...** — Adapty đang chờ cửa hàng hoàn tất quá trình xét duyệt hoặc đồng bộ lần đầu. - **Approved** — sản phẩm đã được cửa hàng phê duyệt và sẵn sàng để kiểm thử. - **Rejected** — cửa hàng đã từ chối sản phẩm. - **Not connected** — sản phẩm chưa được liên kết với cửa hàng. 3. **Trạng thái A/B test** — xuất hiện khi bạn khởi chạy A/B test: - **Draft test** — A/B test đã được tạo nháp nhưng chưa chạy. - **Running** — A/B test đang hoạt động. - **Completed** — A/B test đã kết thúc. - **Archived test** — A/B test đã được lưu trữ mà không có kết luận. ## Nhận giả thuyết mới từ AI \{#get-new-ai-generated-hypotheses\} Sau mỗi lần test, Adapty tự động cập nhật các giả thuyết của bạn dựa trên kết quả. Để cập nhật giá của đối thủ, benchmark tỷ lệ chuyển đổi và xu hướng danh mục mới nhất, nhấn **Update** Refresh ở phần header của growth plan — hoặc **Update Analysis** trên trang chủ AI Growth Advisor. Nhấn **Get New Ideas** khi được nhắc. Adapty mở trình hướng dẫn phân tích với placement của bạn đã được chọn sẵn. Lặp lại quá trình phân tích paywall và nghiên cứu đối thủ, Adapty xác định các giả thuyết mới từ kết quả thu được. Chọn những giả thuyết bạn muốn thêm, hoặc nhấp **Add All To Plan** Plus để chấp nhận tất cả. Các giả thuyết mới được thêm sẽ xuất hiện ở đầu danh sách. Các giả thuyết do AI tạo ra trước đó, giả thuyết tùy chỉnh và bất kỳ A/B test nào đang chạy đều không bị ảnh hưởng. Nếu bạn thoát khỏi một lần cập nhật trước khi hoàn thành, bạn có thể **tiếp tục** để hoàn tất, hoặc **hủy bỏ** để bắt đầu lại từ đầu. ## Thêm giả thuyết của riêng bạn \{#add-your-own-hypothesis\} Nhấp vào **Add Hypothesis** Plus để tạo gợi ý về giá hoặc thiết kế của riêng bạn. Điền vào biểu mẫu: tiêu đề, mô tả và loại giả thuyết (**Monetization** hoặc **Visual**). - Chọn các chỉ số bạn muốn cải thiện từ menu thả xuống. - Giả thuyết Monetization cũng yêu cầu chọn sản phẩm kiểm tra. ## Lưu trữ giả thuyết \{#archive-a-hypothesis\} Để lưu trữ một giả thuyết, nhấp vào nút Close, sau đó nhấp **Skip** để xác nhận. Bạn có thể tùy chọn giải thích lý do — điều này giúp Adapty cải thiện các gợi ý trong tương lai. Giả thuyết sẽ chuyển đến tab **Archived**. Để khôi phục giả thuyết đã lưu trữ vào kế hoạch đang hoạt động, nhấp vào **Restore** trên thẻ. ## Xem lại và tái sử dụng các giả thuyết cũ \{#revisit-and-reuse-old-hypotheses\} Để duyệt các gợi ý được tạo ra từ các lần phân tích trước, hãy nhấp vào **Clock** Clock trong tiêu đề growth plan. Modal **Version history** liệt kê tất cả các lần chạy trước cho placement đó — ngày, paywall, và số lượng giả thuyết bạn đã chấp nhận vào thời điểm đó. Nhấp vào một lần chạy trước đó để xem các giả thuyết mà nó đã tạo ra. Bạn có thể thêm bất kỳ giả thuyết nào trong số đó vào kế hoạch hiện tại bằng **Add to Plan** Plus — hữu ích khi bạn muốn xem lại các gợi ý mà mình chưa thực hiện lần trước. --- # File: autopilot-execute-plan --- --- title: "Thực hiện kế hoạch tăng trưởng" description: "Khởi chạy A/B test từ các giả thuyết trong kế hoạch tăng trưởng của bạn." --- Bạn có thể chạy các bài test theo bất kỳ thứ tự nào, nhưng cần chạy **từng bài một**. Vì đối tượng của các A/B test định giá theo khu vực địa lý không chồng chéo nhau, chúng có thể chạy song song. Sau khi mỗi bài test kết thúc, hãy đưa chiến lược thắng cuộc lên vòng tiếp theo. Mỗi vòng sẽ giúp bạn tìm ra cấu hình hiệu quả hơn cho ứng dụng của mình. Theo ước tính của chúng tôi, việc chạy toàn bộ bộ test được đề xuất có thể tăng doanh thu của bạn **lên đến 80%**. :::important Mỗi gợi ý đều bao gồm thời lượng tối thiểu cho A/B test. Hãy tuân theo các khuyến nghị này để có dữ liệu chính xác nhất trước khi chuyển sang giai đoạn tiếp theo. Bạn sẽ cần dừng A/B test theo cách thủ công. ::: Mở một hypothesis và nhấp **Set Up & Run Test** để khởi động trình hướng dẫn tạo A/B test. ## Bước 1: Xem giả thuyết \{#step-1-view-the-hypothesis\} Bước đầu tiên trình bày tổng quan về giả thuyết. Nó phác thảo các thay đổi được đề xuất và giải thích lý do đằng sau chúng. Nhấn "Set up & Run Test" để chuyển sang bước tiếp theo. {/* TODO: REPLACE SCREENSHOT */} ## Bước 2: Tạo sản phẩm mới \{#step-2-create-new-products\} Nếu test liên quan đến việc thay đổi giá, bước thứ hai sẽ giúp bạn tạo sản phẩm mới cho biến thể test. Các giả thuyết về giao diện sẽ bỏ qua bước này. * Nhấn **Create a new product and push to stores** để tạo sản phẩm mới từ đầu. * Nhấn **Connect an existing product** nếu sản phẩm cần thiết đã tồn tại trong cấu hình cửa hàng của bạn. ## Bước 3: Thiết lập phân khúc và paywall \{#step-3-set-up-segment-and-paywall\} Bước thứ ba cho phép bạn thiết lập biến thể test của paywall. Adapty sẽ nhắc bạn nhân đôi paywall và áp dụng các thay đổi được đề xuất. Với **các giả thuyết định giá theo khu vực địa lý**, trình hướng dẫn sẽ nhắc bạn chọn một phân khúc theo khu vực địa lý hiện có hoặc tạo mới. Nhấn **Next** khi paywall mới đã sẵn sàng và phân khúc đã được thiết lập. ## Bước 4: Xem lại & khởi chạy \{#step-4-review--launch\} Bước cuối cùng là bản tóm tắt của test sắp diễn ra. Bao gồm: - Các chỉ số chính cho cả **Variant A so với Variant B** — tên paywall, danh sách sản phẩm, thời gian dùng thử và giá. - **Duration** (thời lượng), **Traffic** (phân chia lưu lượng), và **Subscribers** (cỡ mẫu tối thiểu) cho test. - Phần **How to interpret results** mô tả những tín hiệu nào cho thấy test thành công. Xem lại cấu hình và nhấn **Launch Test** để bắt đầu A/B test. --- # File: how-adapty-analytics-works --- --- title: "Cách Adapty Analytics hoạt động" description: "Tìm hiểu cách Adapty Analytics hoạt động để theo dõi hiệu suất gói đăng ký một cách hiệu quả." --- Bài viết này mô tả cách Adapty Analytics hoạt động: dữ liệu nào được hiển thị, dữ liệu đến từ đâu, và dữ liệu được xử lý như thế nào. Bài viết cũng giải thích các quyết định thiết kế làm cho Adapty Analytics khác biệt, và những quyết định này mang lại lợi ích gì cho bạn. ## Adapty Analytics so với analytics của cửa hàng \{#adapty-analytics-vs-store-analytics\} - **Đa dạng dữ liệu**: Các cửa hàng chỉ có thể hiển thị dữ liệu của chính họ và không thể truy cập hành vi người dùng bên trong ứng dụng. Adapty có thể kết hợp dữ liệu từ nhiều cửa hàng, cùng với các nguồn bổ sung — nền tảng marketing và mạng quảng cáo. SDK của Adapty theo dõi tương tác của người dùng với các paywall và onboarding. - **Tần suất cập nhật**: Các cửa hàng ứng dụng thường cập nhật dữ liệu một lần mỗi ngày, điều này có thể hạn chế khả năng đưa ra quyết định theo thời gian thực của bạn. Adapty cung cấp analytics [gần như theo thời gian thực](#data-processing). - **Chỉ số nâng cao**: Các cửa hàng ứng dụng chỉ hiển thị các chỉ số cơ bản như lượt tải, doanh thu và tỷ lệ giữ chân người dùng. Adapty còn tính toán các chỉ số nâng cao hơn, chẳng hạn như doanh thu định kỳ hoặc doanh thu trung bình trên mỗi người dùng. Các mục chuyên biệt phân tích các vấn đề liên quan đến gói đăng ký: tỷ lệ rời bỏ, lỗi thanh toán, v.v. Xem bài viết [Bảng so sánh chỉ số](metric-comparison-table) để có danh sách đầy đủ. - **Dự đoán**: Adapty sử dụng các thuật toán học máy tiên tiến để [dự đoán LTV và doanh thu trong tương lai](predicted-ltv-and-revenue). ## Dữ liệu và nguồn dữ liệu \{#data-and-its-sources\} Adapty Analytics xử lý các dữ liệu sau đây thành [biểu đồ và đồ thị](analytics): - [Sự kiện gói đăng ký](events) được tạo ra trong suốt vòng đời người dùng — bắt đầu dùng thử, mua hàng, gia hạn, hủy, lỗi thanh toán, hoàn tiền. Adapty tổng hợp các sự kiện này thành [biểu đồ phân tích](analytics) và chuyển tiếp chúng theo thời gian thực đến [webhook](webhook), [event feed](event-feed), và [các tích hợp dựa trên sự kiện](analytics-integration). - [Dữ liệu giao dịch](revenue) — doanh thu, hoàn tiền, quốc gia của người mua, v.v. - **Dữ liệu ứng dụng** như số lượt cài đặt hoặc [tương tác với paywall](paywalls). - [Dữ liệu attribution cho giao dịch](attribution-integration): nguồn lưu lượng truy cập và chiến dịch quảng cáo. Dữ liệu này đến từ các nguồn sau: - <InlineTooltip tooltip="Adapty SDK">[iOS](ios-sdk-overview), [Android](android-sdk-overview), [React Native](react-native-sdk-overview), [Flutter](flutter-sdk-overview), [Unity](unity-sdk-overview), [Kotlin Multiplatform](kmp-sdk-overview), [Capacitor](capacitor-sdk-overview) </InlineTooltip> báo cáo dữ liệu hành vi người dùng từ bên trong ứng dụng. Nếu Adapty quản lý luồng mua hàng của bạn, SDK chia sẻ thông tin trực tiếp về các sự kiện mua hàng. Nếu bạn dùng [chế độ observer](observer-vs-full-mode), SDK nhận [báo cáo sự kiện](report-transactions-observer-mode) mà bạn thiết lập thủ công. - Các cửa hàng dùng giao tiếp server-to-server để thông báo cho Adapty về các giao dịch (dùng thử, gia hạn gói đăng ký, hủy, v.v.). - Các dịch vụ [attribution](attribution-integration) bên thứ ba (Appsflyer, Adjust, Branch, v.v.) chia sẻ dữ liệu về nguồn traffic và chiến dịch quảng cáo. Nếu bạn thiết lập [Adapty Attribution](adapty-user-acquisition), Adapty có thể tự xử lý dữ liệu chiến dịch quảng cáo mà không cần qua bước này. - Người dùng có thể [nhập thủ công dữ liệu giao dịch lịch sử](importing-historical-data-to-adapty) để Adapty phân tích và hiển thị. Sự cố với một trong các nguồn có thể ảnh hưởng đến chất lượng tổng thể của dữ liệu analytics. Xem phần [Khắc phục sự cố](#troubleshooting) để biết thêm thông tin. ## Tích hợp bên thứ ba \{#third-party-integrations\} Bạn có thể bật [Adapty User Acquisition](adapty-user-acquisition) để mở rộng khả năng analytics của Adapty với dữ liệu chiến dịch quảng cáo. Điều này sẽ giúp bạn khám phá mối tương quan giữa chi phí chiến dịch quảng cáo và hành vi người dùng. Tương tự, bạn có thể [xuất](analytics-integration) dữ liệu analytics sang các nền tảng bên thứ ba hoặc [máy chủ riêng](webhook), và phân tích dữ liệu từ Adapty trên một nền tảng khác. ## Xử lý dữ liệu \{#data-processing\} Adapty cung cấp phân tích gần như theo thời gian thực, giúp người dùng phản ứng nhanh chóng với những thay đổi trong các chỉ số quan trọng. - **Biểu đồ Analytics**: dữ liệu xuất hiện sau **15–30 phút** kể từ khi giao dịch xảy ra. Adapty cần thời gian này để xác thực giao dịch, áp dụng hoa hồng và thuế, rồi tổng hợp dữ liệu. - **[Event feed](event-feed)**: cập nhật theo thời gian thực, ngay khi các cửa hàng gửi sự kiện. - **[Webhooks](webhook) và các tích hợp dựa trên sự kiện** (AppsFlyer, Branch, v.v.): Adapty chuyển tiếp sự kiện ngay khi chúng xảy ra — độ trễ 15–30 phút của analytics không áp dụng ở đây. Dịch vụ nhận có thể có thêm thời gian xử lý riêng. Mỗi nguồn dữ liệu có thời điểm ghi nhận riêng. Cùng một sự kiện có thể xuất hiện ở các thời điểm hơi khác nhau trong biểu đồ, Event Feed và các tích hợp của bạn. Sự chênh lệch nhỏ giữa chúng là điều bình thường. ## Hoa hồng và thuế \{#commissions-and-taxes\} Khi xem các biểu đồ liên quan đến doanh thu, bạn có thể chọn giữa **Gross revenue**, **Revenue after commissions**, và **Revenue after commissions and taxes**. ### Hoa hồng \{#commissions\} Các cửa hàng sẽ khấu trừ hoa hồng từ mỗi giao dịch. Nếu tổ chức của bạn đã đăng ký chương trình hoa hồng giảm, hãy thay đổi cài đặt Adapty để điều chỉnh cách tính tỷ lệ hoa hồng: * [App Store Small Business Program](app-store-small-business-program) * [Chương trình phí dịch vụ giảm](google-reduced-service-fee) của Google Các cửa hàng tự động báo cáo liệu có các yếu tố khác làm giảm hoa hồng giao dịch của bạn hay không: * [Gia hạn cho gói đăng ký App Store từ 1 năm trở lên](https://developer.apple.com/app-store/subscriptions/) — hoa hồng 15% * Mức phí theo từng quốc gia (ví dụ: [21% cho ứng dụng App Store phân phối tại Nhật Bản](https://developer.apple.com/support/app-distribution-in-japan/#business-terms)) ### Thuế \{#taxes\} **Adapty không tính thuế.** Apple và Google là bên xác định mức thuế áp dụng cho từng giao dịch và báo cáo lại cho Adapty, Adapty chỉ hiển thị giá trị đó như nguyên bản. Mức thuế hiển thị cho một giao dịch cụ thể phụ thuộc vào: - **Quốc gia thanh toán của người mua** và mức thuế địa phương áp dụng tại đó. - **Quy tắc xử lý thuế của cửa hàng**. Ở một số khu vực pháp lý, cửa hàng thu và nộp thuế thay cho nhà phát triển; ở nơi khác, nhà phát triển tự chịu trách nhiệm. - Đối với giao dịch App Store, **danh mục thuế** được gán cho ứng dụng hoặc in-app purchase (sách, tin tức, video, v.v.) — các danh mục có thể bị đánh thuế theo mức khác nhau tùy theo quy định địa phương. Thuế suất có thể khác nhau đáng kể giữa các ứng dụng — và thậm chí giữa các giao dịch trong cùng một ứng dụng — do sự kết hợp của quốc gia người mua, quy tắc xử lý của các cửa hàng, và (đối với App Store) danh mục thuế được chỉ định. Để biết các quy tắc chính thức, hãy tham khảo tài liệu chính thức của các cửa hàng: - [App Store: Tìm hiểu về thuế](https://developer.apple.com/help/app-store-connect/making-payments-to-apple/understanding-taxes/) - [Google Play: Thuế suất và VAT](https://support.google.com/googleplay/android-developer/answer/138000) ## Xử lý sự cố \{#troubleshooting\} :::link Bài viết chính: [Sự khác biệt và Xử lý sự cố](discrepancies-and-troubleshooting) ::: * Một nguồn dữ liệu bị cấu hình sai hoặc bị thiếu có thể ảnh hưởng tiêu cực đến toàn bộ hệ thống analytics. Nếu bạn gặp vấn đề về dữ liệu, hãy đảm bảo rằng các tích hợp với cửa hàng và các nền tảng bên thứ ba đã được cấu hình và đang hoạt động. * Nếu bạn so sánh các biểu đồ Adapty với các nền tảng phân tích khác, bạn có thể nhận thấy sự chênh lệch. Đây là hành vi bình thường có thể xuất phát từ sự khác biệt trong quá trình xử lý dữ liệu. Đọc bài viết [hướng dẫn về sự chênh lệch](discrepancies-and-troubleshooting) để tìm hiểu các nguyên nhân phổ biến gây ra sự chênh lệch dữ liệu. --- # File: metric-comparison-table --- --- title: "So sánh các chỉ số khác nhau" description: "Bảng tham chiếu cho các chỉ số phân tích của Adapty, được phân loại theo danh mục." --- Đây là tổng quan về các chỉ số có sẵn trong Adapty Analytics. Sử dụng tài liệu này để hiểu từng chỉ số đo lường điều gì và sự khác biệt giữa các chỉ số liên quan. Để hiểu sâu hơn về cách Adapty xử lý dữ liệu phân tích, xem [Cách Adapty Analytics hoạt động](how-adapty-analytics-works). :::note Bài viết này không đề cập đến các chỉ số [Adapty Attribution](adapty-user-acquisition). Đọc [Phân tích Adapty Attribution](ua-analytics) để tìm hiểu thêm về các chỉ số chiến dịch quảng cáo (Spend, CPI, ROAS, CTR, và các chỉ số khác). ::: ## Chỉ số toàn cục \{#global-metrics\} Chỉ số toàn cục theo dõi hiệu suất của toàn bộ ứng dụng, trên tất cả các placement và paywall. ### Doanh thu \{#revenue\} Các chỉ số này đo lường số tiền ứng dụng tạo ra và từ những nguồn nào. | Chỉ số | Mô tả | Điểm khác biệt | |--------|-------------|----------------| | [Doanh thu](revenue) | Tổng doanh thu từ gói đăng ký và sản phẩm mua một lần, trừ hoàn tiền | Doanh thu thực tế tạo ra. Có thể hiển thị doanh thu gộp, doanh thu sau hoa hồng, hoặc doanh thu sau thuế và hoa hồng tùy theo [tùy chọn biểu đồ](controls-filters-grouping-compare-proceeds) | | [MRR](mrr) | Doanh thu định kỳ hàng tháng từ các gói đăng ký đang hoạt động | Doanh thu hàng tháng có thể dự đoán của ứng dụng. Không bao gồm sản phẩm mua một lần và gói đăng ký không tự gia hạn. | | [ARR](arr) | Doanh thu định kỳ hàng năm từ các gói đăng ký đang hoạt động | Tính giống MRR nhưng theo quy mô năm. Hữu ích để dự báo doanh thu hàng năm | | [ARPU](arpu) | Doanh thu trung bình trên mỗi người dùng | Chia doanh thu cho tổng số người dùng — cả người dùng trả tiền lẫn không trả tiền. Cho thấy mỗi người dùng tạo ra bao nhiêu doanh thu trung bình | | [ARPPU](arppu) | Doanh thu trung bình trên mỗi người dùng trả tiền | Chỉ tính những người dùng đã thực hiện mua hàng trong khoảng thời gian đã chọn, bao gồm cả giao dịch đã hoàn tiền. Luôn cao hơn ARPU | | [LTV (giá trị vòng đời)](ltv) | Doanh thu từ khách hàng trả tiền chia cho số lượng khách hàng trả tiền trong một cohort | Giá trị thực tế trên mỗi khách hàng trả tiền theo thời gian. Khác với ARPPU (một khoảng thời gian), LTV thể hiện tổng doanh thu trong suốt mối quan hệ với khách hàng. Có thể xem theo số lần gia hạn hoặc theo ngày lịch | | [Predicted LTV](predicted-ltv-and-revenue) | Giá trị vòng đời ước tính trên mỗi người dùng trong một cohort | Ước tính hướng về tương lai. Khác với LTV đã thực hiện, dự báo giá trị tương lai dựa trên mô hình giữ chân cohort lịch sử. Có sẵn cho 3, 6, 9, 12, 18 và 24 tháng | | [Predicted revenue](predicted-ltv-and-revenue) | Tổng doanh thu ước tính mà một cohort sẽ tạo ra | Ước tính hướng về tương lai. Khác với Doanh thu đã thực hiện, dự đoán tổng doanh thu một cohort sẽ tạo ra trong khoảng thời gian đã chọn. Cập nhật hàng ngày | | [Non-subscriptions](non-subscriptions) | Số lượng in-app purchase: consumable, non-consumable và gói đăng ký không tự gia hạn | Không bao gồm gói đăng ký tự gia hạn. | | [Sự kiện hoàn tiền](refund-events) | Số lượng giao dịch mua hoặc gói đăng ký đã được hoàn tiền | Được tính theo ngày hoàn tiền, không phải ngày mua ban đầu. | | [Số tiền hoàn trả](refund-money) | Tổng số tiền đã hoàn trả trong khoảng thời gian đã chọn | Tác động tài chính của các khoản hoàn tiền. Tính trước khi trừ phí cửa hàng. Khác với Sự kiện hoàn tiền (là số đếm), chỉ số này hiển thị số tiền thực tế | ### Người đăng ký và chuyển đổi \{#subscribers-and-conversion\} Các chỉ số này theo dõi cách người dùng vào ứng dụng và di chuyển qua các bước trong phễu. | Chỉ số | Mô tả | Điểm khác biệt chính | |--------|-------------|----------------| | [Lượt cài đặt](installs) | Số lần cài đặt ứng dụng trong kỳ | Tính một trong các trường hợp sau tùy theo [định nghĩa lượt cài đặt](general#4-installs-definition-for-analytics): <br /> • Lượt cài đặt theo thiết bị (người dùng cài lại ứng dụng sẽ được tính lại) <br /> • Người dùng duy nhất (chỉ tính người dùng đã đặt `customer_user_id`. Người dùng ẩn danh bị loại hoàn toàn — nếu không có người dùng nào được xác định, số đếm là 0) | | [Dùng thử mới](new-trials) | Số lượt kích hoạt dùng thử trong kỳ | Tính mọi lượt bắt đầu dùng thử, kể cả khi bản dùng thử đã hết hạn hoặc đã chuyển sang trả phí vào thời điểm bạn xem biểu đồ | | [Đang dùng thử](active-trials) | Số lượt dùng thử chưa hết hạn | Chỉ tính các lượt dùng thử còn hoạt động vào cuối kỳ | | [Gói đăng ký mới](reactivated-subscriptions) | Gói đăng ký được kích hoạt lần đầu trong kỳ, bao gồm cả lần mua đầu tiên không qua dùng thử và chuyển đổi từ dùng thử sang trả phí | Không tính gia hạn và tái kích hoạt. Không giống sự kiện tích hợp `subscription_started` — chỉ tính lần mua đầu tiên không qua dùng thử; còn chuyển đổi từ dùng thử sẽ kích hoạt `trial_converted` | | [Gói đăng ký đang hoạt động](active-subscriptions) | Số gói đăng ký trả phí chưa hết hạn | Không tính bản dùng thử và các gói đăng ký đã hủy gia hạn | | [Cài đặt → Dùng thử](analytics-conversion#install---trial) | Tỷ lệ phần trăm người cài đặt bắt đầu dùng thử | Mẫu số bao gồm tất cả người cài đặt, không chỉ người xem paywall, nên tỷ lệ có thể thấp hơn Xem paywall → Dùng thử. Hai chỉ số này cũng có thể chênh nhau nếu ứng dụng không ghi lại lượt xem paywall. Điều này có thể xảy ra với paywall tùy chỉnh không gọi `logShowFlow` (iOS SDK v4+) / `logShowPaywall`, hoặc khi người dùng bắt đầu dùng thử từ [in-app purchase được quảng bá](https://developer.apple.com/documentation/storekit/supporting-promoted-in-app-purchases-in-your-app). | | [Xem paywall → Dùng thử](analytics-conversion#paywall-view---trial) | Tỷ lệ phần trăm người xem paywall bắt đầu dùng thử | Chỉ tính người dùng đã xem paywall, nên tỷ lệ có thể cao hơn Cài đặt → Dùng thử | | [Dùng thử → Trả phí](analytics-conversion#trial---paid) | Tỷ lệ phần trăm người dùng thử mua gói đăng ký | Đo lường chất lượng và hiệu quả chuyển đổi của bản dùng thử. Khác với Cài đặt → Trả phí, chỉ tập trung vào người dùng đã hoàn thành dùng thử | | [Cài đặt → Trả phí](analytics-conversion#install---paid) | Tỷ lệ phần trăm người cài đặt mua gói đăng ký đầu tiên | Tính tất cả người cài đặt, không chỉ người xem paywall. Tỷ lệ có thể thấp hơn Xem paywall → Trả phí. Bao gồm cả mua trực tiếp và chuyển đổi từ dùng thử sang trả phí | | [Xem paywall → Trả phí](analytics-conversion#paywall-view---paid) | Tỷ lệ phần trăm người xem paywall cuối cùng mua gói đăng ký | Chỉ tính người dùng đã xem paywall, nên tỷ lệ có thể cao hơn Cài đặt → Trả phí. Bao gồm cả người dùng đã hoàn thành dùng thử trước | ### Giữ chân người dùng và gia hạn gói đăng ký \{#retention-and-subscription-renewal\} Các chỉ số này theo dõi mức độ giữ chân người dùng trả phí của ứng dụng theo thời gian. | Chỉ số | Mô tả | Điểm khác biệt | |--------|-------------|----------------| | [Retention](analytics-retention) | Tỷ lệ người đăng ký ban đầu còn lại sau mỗi kỳ thanh toán — gia hạn lần 1, lần 2, v.v. | Theo dõi người đăng ký từ lần thanh toán đầu tiên. Khác với các chỉ số theo từng kỳ bên dưới, luôn so sánh với nhóm ban đầu, cho bạn thấy toàn cảnh chỉ trong một cái nhìn | | [Paid to 2nd period](analytics-conversion#paid---2nd-period) | Tỷ lệ phần trăm người đăng ký lần đầu đã gia hạn sang kỳ thứ hai | Đo lường sự chuyển tiếp giữa hai kỳ liền kề cụ thể. Khác với Retention, tập trung vào lần gia hạn quan trọng nhất — lần đầu tiên | | [2nd to 3rd period](analytics-conversion#2nd-period---3rd-period) | Tỷ lệ phần trăm gia hạn từ kỳ thứ 2 sang kỳ thứ 3 | Phản ánh độ ổn định giữ chân người dùng sớm sau lần gia hạn đầu tiên | | [3rd to 4th period](analytics-conversion#3rd-period---4th-period) | Tỷ lệ phần trăm gia hạn từ kỳ thứ 3 sang kỳ thứ 4 | Chỉ số giữ chân người dùng giai đoạn giữa | | [4th to 5th period](analytics-conversion#4th-period---5th-period) | Tỷ lệ phần trăm gia hạn từ kỳ thứ 4 sang kỳ thứ 5 | Chỉ số đo lường mức độ trung thành dài hạn | | [6 Months+](analytics-conversion#6-months-) | Tỷ lệ phần trăm người đăng ký lần đầu vẫn duy trì gói đăng ký sau 6 tháng | Đo theo thời gian thực tế, không phải số lần gia hạn. Người đăng ký hàng năm được tính là còn duy trì ở mốc 6 tháng dù chưa gia hạn | | [1 Year+](analytics-conversion#1-year-) | Tỷ lệ phần trăm người đăng ký lần đầu vẫn duy trì gói đăng ký sau 12 tháng | Mốc đánh giá giữ chân người dùng hàng năm | | [2 Years+](analytics-conversion#2-years-) | Tỷ lệ phần trăm người đăng ký lần đầu vẫn duy trì gói đăng ký sau 24 tháng | Mốc đánh giá giữ chân người dùng dài hạn | ### Churn Các chỉ số này đo lường số lượng người đăng ký và người dùng dùng thử mà ứng dụng mất đi. | Chỉ số | Mô tả | Điểm khác biệt chính | |--------|-------------|----------------| | [Trials renewal cancelled](trials-renewal-cancelled) | Các bản dùng thử mà người dùng đã tắt tự động gia hạn | Người dùng vẫn giữ quyền truy cập dùng thử cho đến khi hết hạn nhưng sẽ không tự động chuyển sang trả phí. Khác với Subscriptions renewal cancelled ở chỗ áp dụng cho người dùng dùng thử chưa thanh toán | | [Expired (churned) trials](expired-churned-trials) | Các bản dùng thử đã hết hạn — người dùng mất quyền truy cập tính năng cao cấp | Người dùng đã mất quyền truy cập. Được tính theo ngày hết hạn, dù người dùng có thể đã hủy gia hạn từ kỳ trước. Có thể phân nhóm theo lý do (tự nguyện hoặc lỗi thanh toán) | | [Subscriptions renewal cancelled](cancelled-subscriptions) | Các gói đăng ký mà người dùng đã tắt tự động gia hạn | Người dùng vẫn có quyền truy cập cho đến khi kỳ hiện tại kết thúc. Đây là tín hiệu có nguy cơ rời bỏ, chứ chưa phải rời bỏ thực sự — người dùng có thể bật lại tự động gia hạn trước khi kỳ hết hạn | | [Churned (expired) subscriptions](churned-expired-subscriptions) | Các gói đăng ký đã hết hạn — người dùng mất quyền truy cập tính năng cao cấp | Rời bỏ thực sự. Người dùng đã mất quyền truy cập. Được tính theo ngày hết hạn, dù người dùng có thể đã hủy gia hạn từ kỳ trước. Có thể phân nhóm theo lý do (tự nguyện hoặc lỗi thanh toán) | ### Vấn đề thanh toán và thu hồi doanh thu \{#billing-issues-and-revenue-recovery\} Các chỉ số này theo dõi mức độ hiệu quả của ứng dụng trong việc thu hồi doanh thu bị mất do vấn đề thanh toán. | Chỉ số | Mô tả | Điểm khác biệt chính | |--------|-------------|----------------| | [Thời gian ân hạn](grace-period) | Các gói đăng ký bước vào thời gian ân hạn do lỗi thanh toán | Bao gồm cả người dùng đã hết thời gian ân hạn và mất quyền truy cập | | [Thời gian ân hạn → đã thanh toán](analytics-conversion#grace-period---paid) | Tỷ lệ người dùng trong thời gian ân hạn đã gia hạn trước khi thời gian ân hạn kết thúc | Là tỷ lệ (%). Trả lời câu hỏi "bao nhiêu phần trăm người dùng trong thời gian ân hạn đã khôi phục?" | | [Đã chuyển đổi trong thời gian ân hạn](grace-period-converted) | Số lượng tuyệt đối các gói đăng ký trong thời gian ân hạn đã gia hạn thành công | Cùng sự kiện với Thời gian ân hạn → đã thanh toán, nhưng hiển thị dưới dạng số lượng thay vì tỷ lệ phần trăm | | [Doanh thu chuyển đổi trong thời gian ân hạn](grace-period-converted-revenue) | Doanh thu từ các giao dịch khôi phục trong thời gian ân hạn | Tác động tài chính của tính năng thời gian ân hạn | | [Lỗi thanh toán](billing-issue) | Các gói đăng ký bước vào trạng thái lỗi thanh toán | Bắt đầu sau khi thời gian ân hạn hết hạn. Khác với Thời gian ân hạn, chỉ tính những người dùng đã thực sự mất quyền truy cập cao cấp | | [Lỗi thanh toán → đã thanh toán](analytics-conversion#billing-issue---paid) | Tỷ lệ người dùng gặp lỗi thanh toán đã gia hạn trước khi chu kỳ thanh toán kết thúc | Là tỷ lệ (%). Trả lời câu hỏi "bao nhiêu phần trăm người dùng gặp lỗi thanh toán đã khôi phục?" | | [Đã chuyển đổi lỗi thanh toán](billing-issue-converted) | Số lượng tuyệt đối các gói đăng ký gặp lỗi thanh toán đã gia hạn thành công | Số lượng gói đăng ký gặp lỗi thanh toán đã gia hạn thành công. Cùng sự kiện với Lỗi thanh toán → đã thanh toán, nhưng hiển thị dưới dạng số lượng thay vì tỷ lệ phần trăm | | [Doanh thu chuyển đổi lỗi thanh toán](billing-issue-converted-revenue) | Doanh thu từ các giao dịch khôi phục lỗi thanh toán | Tác động tài chính của việc khôi phục lỗi thanh toán | ## Chỉ số paywall, placement và onboarding \{#paywall-placement-and-onboarding-metrics\} Các chỉ số này được tính toán cho từng [paywall](paywall-metrics), [placement](placement-metrics) và onboarding riêng lẻ. Chúng đo lường hiệu suất của một paywall hoặc placement cụ thể thay vì toàn bộ ứng dụng. Cột **Associated global metric** hiển thị chỉ số tương ứng trong phần analytics tổng thể. | Chỉ số | Mô tả | Điểm khác biệt | Chỉ số toàn cục liên quan | |--------|-------------|----------------|---------------| | [Proceeds](paywall-metrics#proceeds) | Doanh thu sau thuế và hoa hồng cho một placement riêng lẻ | Tương đương với [Revenue](revenue) sau thuế và hoa hồng | [Revenue](revenue) | | [ARPPU](paywall-metrics#arppu) | Doanh thu trung bình trên mỗi người dùng trả phí cho paywall hoặc placement này | Cách tính giống ARPPU toàn cục nhưng chỉ áp dụng cho một paywall hoặc placement | [ARPPU](arppu) | | [ARPAS](paywall-metrics#arpas) | Doanh thu chia cho số lượng người đăng ký đang hoạt động (bao gồm dùng thử và trả phí) | Tính cả người dùng dùng thử. Khác ARPPU ở chỗ phản ánh tiềm năng doanh thu của toàn bộ cơ sở người đăng ký | — | | [Views](paywall-metrics#views) | Tổng số lần một paywall hoặc placement được hiển thị | Tính mọi lần hiển thị. Một người dùng xem cùng một paywall hai lần sẽ được tính là 2 lượt xem | — | | [Unique views](paywall-metrics#unique-views) | Số người dùng duy nhất đã xem một paywall hoặc placement | Mỗi người dùng chỉ được tính một lần dù họ xem bao nhiêu lần. Khác Views ở chỗ đo lường phạm vi tiếp cận thay vì tần suất tương tác | — | | [CR to purchases](paywall-metrics#cr-to-purchases) | Số lượt mua chia cho tổng số lượt xem | Sử dụng tổng số lượt xem (kể cả lượt xem lặp lại của cùng một người dùng) làm mẫu số | [Paywall view to paid](analytics-conversion#paywall-view---paid) | | [Unique CR to purchases](paywall-metrics#unique-conversion-rate-cr-to-purchases) | Số lượt mua chia cho số lượt xem duy nhất | Sử dụng lượt xem duy nhất làm mẫu số. Tỷ lệ cao hơn CR không duy nhất vì người xem lặp lại chỉ được tính một lần | [Paywall view to paid](analytics-conversion#paywall-view---paid) | | [CR to trials](paywall-metrics#unique-cr-to-trials) | Số lượt bắt đầu dùng thử chia cho tổng số lượt xem | Đo lường mức độ hiệu quả của paywall trong việc chuyển đổi lượt xem thành dùng thử | [Paywall view to trial](analytics-conversion#paywall-view---trial) | | [Unique CR to trials](paywall-metrics#unique-cr-to-trials) | Số lượt bắt đầu dùng thử chia cho số lượt xem duy nhất | Tính như CR to trials nhưng dùng số người xem duy nhất làm mẫu số | [Paywall view to trial](analytics-conversion#paywall-view---trial) | | [Purchases](paywall-metrics#purchases) | Tổng số giao dịch của paywall này: mua mới, chuyển đổi từ dùng thử, nâng cấp, hạ cấp và gói đăng ký quay lại | Không tính gia hạn. | [Revenue](revenue) | | [Trials](paywall-metrics#trials) | Tổng số lượt dùng thử được kích hoạt qua paywall này | Chỉ tính cho paywall này | [New trials](new-trials) | | [Trials canceled](paywall-metrics#trials-canceled) | Số lượt dùng thử mà người dùng đã tắt tự động gia hạn | Chỉ tính cho các lượt dùng thử của paywall này | [Trials renewal cancelled](trials-renewal-cancelled) | | [Refund rate](paywall-metrics#refund-rate) | Số hoàn tiền chia cho số lượt mua lần đầu (không tính gia hạn) | Là tỷ lệ (%), không phải số lượng. Chuẩn hóa hoàn tiền so với số lượt mua | [Refund events](refund-events) (số lượng, không phải tỷ lệ) | | Completions | Số lần người dùng hoàn thành một onboarding flow từ màn hình đầu đến màn hình cuối | Chỉ áp dụng cho placement và onboarding. Tính mọi lần hoàn thành, kể cả lặp lại bởi cùng một người dùng | — | | Unique completions | Số người dùng duy nhất đã hoàn thành một onboarding flow | Chỉ áp dụng cho placement và onboarding. Mỗi người dùng chỉ được tính một lần. Khác Completions ở chỗ đo lường số cá nhân thực sự hoàn thành flow | — | | Unique completions rate | Số unique completions chia cho số lượt xem duy nhất | Chỉ áp dụng cho placement và onboarding. Đo lường hiệu quả của onboarding: tỷ lệ người dùng bắt đầu thực sự hoàn thành | — | --- # File: overview --- --- title: "Trang tổng quan Analytics" description: "Xem nhiều biểu đồ analytics của Adapty trên cùng một trang để có cái nhìn tổng quan về hiệu suất ứng dụng của bạn" --- [Trang Tổng quan](https://app.adapty.io/overview) hiển thị các chỉ số tổng hợp cho tất cả ứng dụng của bạn ở một nơi. Đây là trang chủ của dashboard và cũng có thể truy cập từ menu bên trái. Để xem dữ liệu cho một ứng dụng cụ thể, hãy mở từng [biểu đồ](charts) riêng lẻ. ## Biểu đồ \{#charts\} Trang Tổng quan hiển thị một tập hợp tùy chỉnh các [biểu đồ analytics](charts) của Adapty. Để xem mô tả và so sánh từng biểu đồ có sẵn, hãy xem [Bảng so sánh chỉ số](metric-comparison-table). Để tùy chỉnh biểu đồ nào xuất hiện và theo thứ tự nào, nhấp vào **Edit** ở góc trên bên phải. Từ đó bạn có thể xóa, thêm hoặc sắp xếp lại các biểu đồ: Các biểu đồ sau đây có sẵn: - [Revenue](revenue) - [MRR](mrr) - [ARR](arr) - [ARPU](arpu) - [ARPPU](arppu) - [ARPAS](placement-metrics#arpas) - [Installs](installs) - [New trials](new-trials) - [New subscriptions](reactivated-subscriptions) - [Active trials](active-trials) - [Active subscriptions](active-subscriptions) - [New non-subscriptions](non-subscriptions) - [Refund events](refund-events) - [Refund money](refund-money) - [Subscriptions renewal canceled](cancelled-subscriptions) - [Conversion rate from Install to Trial, Install to Paid, and Trial to Paid](analytics-conversion) ## Điều khiển \{#controls\} Trang Tổng quan hỗ trợ hầu hết các [điều khiển analytics](controls-filters-grouping-compare-proceeds), bao gồm lọc, nhóm và so sánh theo khoảng thời gian. Tính năng duy nhất của Tổng quan là nhóm và lọc theo ứng dụng. Vì trang này tổng hợp dữ liệu từ tất cả ứng dụng của bạn, chế độ xem theo từng ứng dụng cho thấy mỗi ứng dụng đóng góp như thế nào vào các chỉ số kinh doanh của bạn: ## Số lượt cài đặt và múi giờ \{#install-count-and-timezone\} Trang Tổng quan tổng hợp dữ liệu từ tất cả ứng dụng của bạn bằng **cài đặt múi giờ và cách đếm lượt cài đặt riêng** — các giá trị theo từng ứng dụng không áp dụng ở đây. - **Installs**: chọn cách đếm lượt cài đặt. **By device installations** tính mỗi lần cài đặt trên thiết bị — kể cả cài đặt lại — là riêng biệt. **By unique users** chỉ đếm lần cài đặt đầu tiên của mỗi người dùng được xác định. Để thay đổi cài đặt, nhấp vào **Edit Metrics** và chọn một [tùy chọn khác](general#4-installs-definition-for-analytics) từ menu thả xuống. - **Timezone**: Để thay đổi múi giờ của Tổng quan, nhấp vào **Edit Metrics** và chọn múi giờ từ menu thả xuống. Tính năng này đặc biệt hữu ích khi các ứng dụng khác nhau trong tài khoản của bạn sử dụng các múi giờ khác nhau. --- # File: controls-filters-grouping-compare-proceeds --- --- title: "Các điều khiển phân tích" description: "Lọc, nhóm và so sánh dữ liệu phân tích Adapty của bạn." --- Adapty cung cấp các điều khiển để tinh chỉnh dữ liệu trong từng tab phân tích: khoảng thời gian, so sánh kỳ, lọc, nhóm và hiển thị biểu đồ. Tính khả dụng khác nhau tùy theo tab. **Các điều khiển theo tab phân tích:** | Điều khiển | Charts | Cohorts | Funnels | Retention | Conversion | LTV | | :--- | :---: | :---: | :---: | :---: | :---: | :---: | | Khoảng ngày | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | So sánh kỳ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | Lọc | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Nhóm | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | | Hiển thị biểu đồ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | Xem bảng | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Xuất CSV | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Hoa hồng và thuế | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ### Đặt khoảng ngày \{#set-the-date-range\} Dùng lịch **Date range** phía trên mỗi biểu đồ để chọn khoảng thời gian. Dữ liệu phân tích Adapty sử dụng **múi giờ UTC**; [trang Overview](overview) có múi giờ riêng có thể cấu hình. #### Khoảng thời gian đặt sẵn \{#preset-ranges\} Dùng tùy chọn **Custom** để chỉ định ngày bắt đầu và kết thúc tùy ý. Các preset có sẵn: | Preset | Bắt đầu | Kết thúc | | --- | --- | --- | | Last 7 days | 6 ngày trước | Hôm nay | | Last 28 days | 27 ngày trước | Hôm nay | | Last month | Cùng ngày trong tháng trước | Hôm nay | | Last 3 months | 3 tháng trước | Hôm nay | | Last 6 months | 6 tháng trước | Hôm nay | | Last year | 1 năm trước | Hôm nay | | Previous month | Ngày đầu tiên của tháng trước | Ngày cuối cùng của tháng trước | | This month | Ngày 1 của tháng hiện tại | Hôm nay | | This quarter | Ngày 1 của quý hiện tại | Hôm nay | | This year | Ngày 1 tháng 1 của năm hiện tại | Hôm nay | :::tip Dùng **Last 28 days** để theo dõi các sản phẩm đăng ký theo tuần — khoảng này bao gồm đúng bốn chu kỳ tuần đầy đủ, tránh bị lệch do tuần không trọn vẹn. ::: #### Thang thời gian \{#time-scale\} Mỗi điểm dữ liệu trên biểu đồ đại diện cho một khoảng thời gian — chọn ngày, tuần, tháng, quý hoặc năm từ dropdown. Ngày và tuần thể hiện biến động ngắn hạn; tháng, quý và năm thể hiện xu hướng dài hơn. Trong phân tích [cohort](analytics-cohorts) và [LTV](ltv), cài đặt này được gọi là **cohort length** — xem các bài viết đó để biết thêm chi tiết. ### So sánh hai kỳ \{#compare-two-time-periods\} Nhấp vào tùy chọn so sánh bên cạnh lịch để xếp chồng kỳ hiện tại với kỳ trước đó. Mặc định, Adapty so sánh với kỳ ngay trước đó có cùng độ dài. Để thay đổi khoảng so sánh, nhấp lại vào tùy chọn và chọn khoảng tùy chỉnh. Kết quả so sánh hiển thị: - **Trên biểu đồ** — các đường, vùng hoặc cột xếp chồng, khi chọn không hoặc một nhóm. - **Dưới dạng giá trị số** — chênh lệch giữa hai kỳ, hiển thị màu xanh (cao hơn) hoặc đỏ (thấp hơn). - **Trong tooltip** — di chuột qua bất kỳ điểm dữ liệu nào để xem chênh lệch tại điểm đó. ### Lọc và nhóm dữ liệu \{#filter-and-group-data\} **Lọc** để giới hạn biểu đồ chỉ hiển thị dữ liệu khớp với một hoặc nhiều thuộc tính (ví dụ: một quốc gia hoặc sản phẩm cụ thể). **Nhóm** để chia tổng biểu đồ thành các chuỗi riêng biệt — mỗi chuỗi cho một giá trị thuộc tính. Ví dụ: nhóm Revenue theo quốc gia để có đường doanh thu riêng cho từng quốc gia thay vì một tổng chung. **Các thuộc tính lọc và nhóm khả dụng:** | Thuộc tính | Lọc | Nhóm | Mô tả | | --- | :---: | :---: | --- | | Attribution | ✅ | ✅ | Reported by, Status, Channel, Campaign, Ad Group, Ad Set và Creative (Keyword). Yêu cầu [tích hợp attribution](attribution-integration). | | Audience | ✅ | ✅ | [Đối tượng](audience) mà người dùng thuộc về. | | Renewal status | ❌ | ✅ | Gói đăng ký có gia hạn trong kỳ tiếp theo không. | | Period | ✅ | ✅ | Giai đoạn trong vòng đời gói đăng ký: **Trial**, **Activation** (lần thanh toán đầu tiên), hoặc **Renewal 1**–**Renewal 5**, **Renewals 6+** (các lần gia hạn tiếp theo). | | Country | ✅ | ✅ | Quốc gia cửa hàng của người dùng. Nếu không có, Adapty suy ra từ mã tiền tệ hoặc IP thiết bị. | | Offer Type | ✅ | ✅ | Ưu đãi áp dụng cho giao dịch: <ul><li>**Introductory** — ưu đãi giới thiệu cho kỳ đăng ký ban đầu. Dùng **Offer Discount Type** để phân biệt giữa intro có phí và dùng thử miễn phí.</li><li>**Promotional** — App Store Promotional Offers và các tương đương.</li><li>**Offer Code** — mã promo khách hàng nhập trong cửa hàng.</li><li>**No offer** — không áp dụng ưu đãi.</li></ul> | | Offer ID | ✅ | ✅ | ID ưu đãi cụ thể. | | Offer Discount Type | ✅ | ✅ | Mô hình định giá của ưu đãi giới thiệu hoặc ưu đãi: **Free Trial**, **Pay As You Go**, hoặc **Pay Up Front**. Kết hợp với **Offer Type** để phân biệt, ví dụ, intro dùng thử miễn phí với intro có phí. | | Paywall | ✅ | ✅ | [Paywall](paywalls) được dùng cho giao dịch mua. | | A/B tests | ✅ | ❌ | [A/B test](ab-tests) đang hoạt động trong thời điểm mua. | | Placement | ✅ | ✅ | [Placement](placements) nơi giao dịch mua diễn ra. | | Store | ✅ | ✅ | Cửa hàng xử lý giao dịch: App Store, Google Play, Stripe, v.v. | | Product | ✅ | ✅ | [Sản phẩm](product) — gói đăng ký và sản phẩm mua một lần. | | Duration | ✅ | ✅ | Thời hạn của sản phẩm. | | Segment | ✅ | ✅ | [Phân khúc](segments) người dùng. Nhóm theo phân khúc để so sánh hiệu suất phân khúc với **All users**. <ul><li>Funnels không hỗ trợ nhóm theo phân khúc.</li><li>Nếu bạn thay đổi thuộc tính tùy chỉnh sau khi phân khúc dùng nó, Adapty có thể loại trừ người dùng khỏi phân khúc trong phân tích. Dữ liệu vẫn hiển thị giá trị trước đó.</li></ul> | | Refund Reason | ✅ | ✅ | Lý do giao dịch bị hoàn tiền (ví dụ: **Refund** hoặc **Upgraded**). Có trên các biểu đồ hoàn tiền và giải quyết vấn đề thanh toán. | | Expiration reason | ❌ | ✅ | Lý do gói đăng ký hoặc dùng thử hết hạn: **Cancelled by customer**, **Billing issue**, **Customer hasn't agreed to price increase**, **Unknown**, hoặc **Refund**. Có trên biểu đồ Expired (Churned) subscriptions và Expired (Churned) trials. | | Cohort (chỉ LTV) | ❌ | ✅ | Trên biểu đồ LTV, nhóm theo độ dài cohort: **Day**, **Week**, **Month**, hoặc **Year**. Thay thế Group by Attribution trên biểu đồ này. | Không phải mọi chế độ xem phân tích đều hỗ trợ tất cả thuộc tính lọc hoặc nhóm trên. ARPU và Installs trong tab Charts chỉ giới hạn ở Attribution, Country, Segment, Store và (chỉ lọc) A/B tests. Các tab LTV, Cohorts, Funnels, Retention và Conversion mỗi tab hỗ trợ một tập hợp con khác nhau. Để biết hỗ trợ chính xác, xem bài viết cho biểu đồ hoặc tab đó. ### Cách xác định quốc gia \{#how-country-is-determined\} Mỗi giao dịch được gắn tag quốc gia tại thời điểm tạo. Nguồn xác định quốc gia đó, theo thứ tự ưu tiên, là: 1. **Quốc gia IP thiết bị** của người dùng tại thời điểm giao dịch. 2. **Quốc gia cửa hàng** của người dùng — quốc gia tài khoản App Store hoặc Google Play của họ. 3. **Quốc gia IP gần nhất** đã biết của người dùng. Quốc gia cửa hàng không khả dụng với thanh toán web (Stripe, Paddle), quyền truy cập cấp thủ công, hoặc các giao dịch mà cửa hàng không cung cấp. Trong những trường hợp đó, Adapty dùng quốc gia dựa trên IP. Vì quốc gia được ghi nhận theo từng giao dịch, một người dùng đổi quốc gia App Store sau khi cài đặt sẽ có các giá trị quốc gia khác nhau trên các giao dịch trước và sau lần đổi đó. Các giao dịch cũ giữ nguyên quốc gia ban đầu. **GB và United Kingdom.** Dữ liệu quốc gia được lưu dưới dạng mã ISO 3166-1 alpha-2 (tức là "GB", không phải "United Kingdom"). Lớp hiển thị dashboard ánh xạ mã sang tên đầy đủ qua bảng tra cứu có chứa alias kế thừa `'UK' → 'United Kingdom'` — đó là lý do cả hai có thể xuất hiện là tùy chọn khi tạo phân khúc. ### Thay đổi kiểu hiển thị biểu đồ \{#change-the-chart-visualization\} Chọn cách hiển thị biểu đồ từ dropdown kiểu hiển thị: - **Stacked column** — mỗi cột hiển thị tổng, chia thành các đoạn màu theo nhóm. - **Stacked area** — tương tự stacked column, nhưng với các vùng tô màu nối các điểm dữ liệu. - **Line** — một đường cho mỗi nhóm, không tô màu. - **100% stacked column** — mỗi cột đạt chiều cao toàn bộ biểu đồ; các đoạn hiển thị tỷ lệ phần trăm của mỗi nhóm thay vì giá trị thực. Hữu ích để xem tỷ lệ theo thời gian. - **100% stacked area** — tương tự 100% stacked column, nhưng với các vùng tô thay vì cột. ### Xem dữ liệu dạng bảng \{#view-data-as-a-table\} Phía dưới mỗi biểu đồ là bảng dữ liệu tương ứng, với ngày làm cột. Hàng và cột Total hiển thị các tổng hợp không thấy trên biểu đồ. ### Xuất dữ liệu ra CSV \{#export-data-to-csv\} Nhấp nút **Export** để tải dữ liệu gốc của biểu đồ dưới dạng file CSV. :::tip Để truy cập theo lập trình hoặc theo lịch định kỳ, dùng [Export API](export-analytics-api) thay thế — API trả về cùng dữ liệu như file CSV tải xuống. ::: ### Hiển thị doanh thu gộp hoặc ròng \{#display-gross-or-net-revenue\} Với các biểu đồ liên quan đến doanh thu ([Revenue](revenue), [MRR](mrr), [ARR](arr), [ARPU](arpu), [ARPPU](arppu)), Adapty cung cấp dropdown với ba chế độ hiển thị: - **Gross revenue** — tổng doanh thu trước khi trừ bất kỳ khoản nào. - **Proceeds after store commission** — doanh thu trừ hoa hồng cửa hàng, vẫn bao gồm thuế. - **Proceeds after store commission and taxes** — doanh thu trừ cả hoa hồng lẫn thuế. Để biết chi tiết về cách tính hoa hồng và thuế, xem [Commissions and taxes](how-adapty-analytics-works#commissions-and-taxes) trong *Cách Adapty Analytics hoạt động*. --- # File: revenue --- --- title: "Doanh thu" description: "Theo dõi và phân tích doanh thu ứng dụng của bạn bằng thông tin chi tiết về gói đăng ký từ Adapty." --- Biểu đồ Doanh thu hiển thị tổng doanh thu thu được từ cả gói đăng ký lẫn sản phẩm mua một lần, trừ đi phần doanh thu đã được hoàn lại. Đây là chỉ số chính để theo dõi hiệu quả tài chính của ứng dụng. Chuyển sang độ phân giải theo tháng để đánh giá xu hướng tổng thể trong 12 tháng gần nhất. Nhóm biểu đồ theo sản phẩm, phân khúc người dùng, hoặc nguồn attribution để xem doanh thu đến từ đâu, và theo dõi tỷ lệ mới-so-với-gia-hạn để hiểu phần nào đang thúc đẩy tăng trưởng. ## Cách tính \{#calculation\} :::warning Máy tính bên dưới **không tính đến** [hoa hồng cửa hàng và thuế](how-adapty-analytics-works#commissions-and-taxes). Hãy so sánh kết quả với số liệu **doanh thu gộp** của bạn. ::: Doanh thu là tổng của tất cả các giao dịch có tính phí trong kỳ (gói đăng ký mới, gia hạn, chuyển đổi dùng thử, sản phẩm mua một lần) trừ đi các khoản hoàn tiền được xử lý trong kỳ: **Doanh thu = tổng giao dịch − hoàn tiền**. Toàn bộ giá trị của mỗi giao dịch được ghi nhận vào ngày mua, không phân bổ theo thời gian của gói đăng ký. Biểu đồ hiển thị doanh thu gộp theo mặc định. Dùng [tùy chọn biểu đồ](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue) để chuyển đổi giữa doanh thu gộp, sau hoa hồng, hoặc sau hoa hồng và thuế. <CompoundCalculator client:load heading="Doanh thu" formuLatex="\sum P_i \times Q_i - D" variables={[ { nameInTheFormula: "P", variableName: "price", variableDescription: "Giá mỗi đơn vị", variableValue: 10 }, { nameInTheFormula: "Q", variableName: "qty", variableDescription: "Số lượng", variableValue: 1, isInteger: true }, { nameInTheFormula: "D", variableName: "refunds", variableDescription: "Số tiền hoàn lại", variableValue: 35, global: true } ]} rowFormula="price * qty" resultFormula="_sum - refunds" defaultRows={[ { price: 10, qty: 5 }, { price: 50, qty: 10 }, { price: 100, qty: 1 } ]} /> ## Xử lý hoàn tiền \{#refund-handling\} Doanh thu trừ đi từng khoản hoàn tiền vào ngày hoàn tiền được xử lý — không phải ngày mua ban đầu. Biểu đồ có thể hiển thị giá trị âm cho một nhóm hoặc ngày cụ thể khi số tiền hoàn trong nhóm đó vượt quá doanh thu mới. Để so sánh đầy đủ giữa các chỉ số, xem [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ## Đơn vị tiền tệ \{#currency\} Adapty hiển thị tất cả các biểu đồ tiền tệ bằng **đô la Mỹ (USD)**, bất kể đơn vị tiền tệ gốc của giao dịch. Điều này bao gồm Revenue, MRR, ARR, ARPU, ARPPU, LTV, predicted revenue, refund money, và các con số doanh thu trong cohort và báo cáo A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty chuyển đổi mỗi giao dịch sang USD theo tỷ giá từ [currencylayer.com](https://currencylayer.com/) được cập nhật mỗi 8 giờ, **cố định tại thời điểm giao dịch**. Các giá trị USD lịch sử sẽ không được tính lại khi tỷ giá ngoại hối thay đổi. Giá trị theo tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price_local` và `currency` trong webhook - Các cột `_local` (như `revenue_local` và `proceeds_local`) và `currency` trong S3, GCS, và BigQuery exports - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo tiền tệ địa phương, hãy lấy giá trị tiền tệ địa phương theo từng giao dịch từ bản export và tự tổng hợp. ## Giá gia hạn \{#renewal-pricing\} Adapty tính doanh thu gia hạn theo giá hiện tại của sản phẩm, kể cả với những người dùng đang ở mức giá cũ khi họ đăng ký lần đầu. Sau khi bạn thay đổi giá trong App Store Connect hoặc Google Play, các chỉ số Revenue, MRR và ARR trên dashboard dành cho những người đăng ký hiện tại có thể khác với doanh thu thực tế thu được — Adapty áp dụng giá mới, dù cửa hàng vẫn giữ mức giá cũ cho những người dùng đó. Để kiểm tra, hãy so sánh trường `price` theo từng giao dịch trong bản xuất dữ liệu S3, GCS hoặc BigQuery với dashboard cho cùng các giao dịch đó. Trường trong bản xuất phản ánh những gì cửa hàng đã báo cáo (giá mà khách hàng thực sự đã trả); còn dashboard phản ánh giá sản phẩm hiện tại. ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Tùy chọn Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Quốc gia, Loại ưu đãi, ID ưu đãi, Loại giảm giá ưu đãi, Paywall, A/B test, Placement, Kỳ, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Kỳ, Trạng thái gia hạn, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Audience, Placement, Thời hạn, Loại ưu đãi, Loại giảm giá ưu đãi, ID ưu đãi, Phân khúc và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để so sánh song song các chỉ số này, xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [MRR](mrr) - [ARR](arr) - [ARPU](arpu) - [ARPPU](arppu) --- # File: mrr --- --- title: "MRR" description: "Hiểu và tối ưu hóa Doanh thu định kỳ hàng tháng (MRR) trong Adapty." --- Biểu đồ Doanh thu định kỳ hàng tháng (MRR) hiển thị doanh thu từ các gói đăng ký trả phí đang hoạt động, được chuẩn hóa thành con số hàng tháng. Nó cho thấy doanh thu ổn định mà mô hình kinh doanh gói đăng ký của bạn tạo ra, bất kể thời hạn gói đăng ký. Để xem mỗi cohort người dùng đóng góp như thế nào vào doanh thu định kỳ theo thời gian, hãy nhóm biểu đồ theo tháng mua đầu tiên và chuyển sang độ phân giải hàng tháng. Chế độ xem vùng xếp chồng hiển thị đóng góp của từng cohort theo từng tháng. ## Cách tính \{#calculation\} :::warning Máy tính bên dưới **không tính đến** [hoa hồng cửa hàng và thuế](how-adapty-analytics-works#commissions-and-taxes). Hãy so sánh kết quả với các tính toán **doanh thu gộp** của bạn. ::: MRR chuẩn hóa doanh thu của mỗi gói đăng ký thành mức tương đương hàng tháng — một gói đăng ký hàng năm với giá $240 đóng góp $20 mỗi tháng, không phải $240 một lần. Điều này giúp MRR ổn định bất kể các chu kỳ thanh toán gói đăng ký được phân bổ như thế nào. MRR là tổng của (giá × số người đăng ký đang hoạt động ÷ chu kỳ thanh toán tính bằng tháng) trên tất cả các loại gói đăng ký của bạn. Gói đăng ký hàng tuần sử dụng chu kỳ thanh toán là ≈0,23 tháng. <SimpleCalculator client:load heading="MRR" formuLatex="\sum_{subscriptions}^{}\frac{P_s\times N_s}{D_m}" variables={[ { nameInTheFormula: "P_s", variableName: "subscriptionPrice", variableDescription: "Price", variableValue: 10 }, { nameInTheFormula: "N_s", variableName: "activeSubs", variableDescription: "Subscribers", variableValue: 1, isInteger: true }, { nameInTheFormula: "D_m", variableName: "duration", variableDescription: "Subscription period", variableValue: 1, options: [ { label: "Weekly", value: 0.23 }, { label: "Monthly", value: 1 }, { label: "2 months", value: 2 }, { label: "3 months", value: 3 }, { label: "6 months", value: 6 }, { label: "Annual", value: 12 } ] } ]} formulaCalculation="(subscriptionPrice * activeSubs) / duration" isSum={true} defaultRows={[ { subscriptionPrice: 240, activeSubs: 2, duration: 12}, { subscriptionPrice: 30, activeSubs: 10, duration: 1}, { subscriptionPrice: 10, activeSubs: 20, duration: 0.23}, ]} /> MRR không tính đến các sản phẩm không tạo ra doanh thu định kỳ: - sản phẩm mua một lần - consumable - gói đăng ký không tự gia hạn Tệp người dùng của bạn có thể tạo ra doanh thu ổn định thông qua các sản phẩm mua một lần. Nhưng doanh thu này không được tính vào MRR vì bản thân các giao dịch mua không có tính định kỳ. ## Xử lý hoàn tiền \{#refund-handling\} Khi một gói đăng ký được hoàn tiền, MRR sẽ xóa đóng góp của nó khỏi mọi ngày trên biểu đồ mà trước đó đã được tính. Các giá trị MRR trong quá khứ có thể giảm sau khi hoàn tiền được ghi nhận. Để xem so sánh đầy đủ giữa các chỉ số, hãy xem [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ## Đơn vị tiền tệ \{#currency\} Adapty hiển thị tất cả các biểu đồ tiền tệ bằng **đô la Mỹ (USD)**, bất kể đơn vị tiền tệ gốc của giao dịch. Điều này bao gồm Revenue, MRR, ARR, ARPU, ARPPU, LTV, predicted revenue, refund money, và các con số doanh thu trong cohort và báo cáo A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty chuyển đổi mỗi giao dịch sang USD theo tỷ giá từ [currencylayer.com](https://currencylayer.com/) được cập nhật mỗi 8 giờ, **cố định tại thời điểm giao dịch**. Các giá trị USD lịch sử sẽ không được tính lại khi tỷ giá ngoại hối thay đổi. Giá trị theo tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price_local` và `currency` trong webhook - Các cột `_local` (như `revenue_local` và `proceeds_local`) và `currency` trong S3, GCS, và BigQuery exports - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo tiền tệ địa phương, hãy lấy giá trị tiền tệ địa phương theo từng giao dịch từ bản export và tự tổng hợp. ## Giá gia hạn \{#renewal-pricing\} Adapty tính doanh thu gia hạn theo giá hiện tại của sản phẩm, kể cả với những người dùng đang ở mức giá cũ khi họ đăng ký lần đầu. Sau khi bạn thay đổi giá trong App Store Connect hoặc Google Play, các chỉ số Revenue, MRR và ARR trên dashboard dành cho những người đăng ký hiện tại có thể khác với doanh thu thực tế thu được — Adapty áp dụng giá mới, dù cửa hàng vẫn giữ mức giá cũ cho những người dùng đó. Để kiểm tra, hãy so sánh trường `price` theo từng giao dịch trong bản xuất dữ liệu S3, GCS hoặc BigQuery với dashboard cho cùng các giao dịch đó. Trường trong bản xuất phản ánh những gì cửa hàng đã báo cáo (giá mà khách hàng thực sự đã trả); còn dashboard phản ánh giá sản phẩm hiện tại. ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Quốc gia, Loại ưu đãi, ID ưu đãi, Loại giảm giá ưu đãi, Paywall, A/B test, Placement, Khoảng thời gian, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Khoảng thời gian, Trạng thái gia hạn, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Audience, Placement, Thời hạn, Loại ưu đãi, Loại giảm giá ưu đãi, ID ưu đãi, Phân khúc và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để so sánh các chỉ số này cạnh nhau, hãy xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [Revenue](revenue) - [ARR](arr) - [ARPU](arpu) - [ARPPU](arppu) --- # File: arr --- --- title: "ARR" description: "Theo dõi Doanh thu Định kỳ Hàng năm (ARR) và tối ưu hóa chiến lược gói đăng ký của bạn." --- Biểu đồ Doanh thu Định kỳ Hàng năm hiển thị doanh thu từ tất cả các gói đăng ký tự gia hạn đang hoạt động, được chuẩn hóa theo một năm. Biểu đồ coi bất kỳ gói đăng ký đã thanh toán và chưa hết hạn nào là đang hoạt động. ARR là chỉ số quan trọng để theo dõi mức tăng trưởng của doanh nghiệp gói đăng ký và dự đoán doanh thu trong tương lai. ## Cách tính \{#calculation\} :::warning Máy tính bên dưới **không tính đến** [hoa hồng cửa hàng và thuế](how-adapty-analytics-works#commissions-and-taxes). Hãy so sánh kết quả với các tính toán **doanh thu gộp** của bạn. ::: ARR là phiên bản hàng năm hóa của doanh thu gói đăng ký định kỳ. Chỉ số này hữu ích nhất khi gói đăng ký hàng năm là sản phẩm chính của bạn — với các doanh nghiệp chủ yếu có gói đăng ký hàng tháng hoặc hàng tuần, [MRR](mrr) sẽ mang lại thông tin hữu ích hơn. ARR là tổng của (giá × người dùng đang đăng ký ÷ kỳ thanh toán tính theo năm) trên tất cả các loại gói đăng ký. Dùng 1/12 cho hàng tháng, 1/52 cho hàng tuần. <SimpleCalculator client:load heading="ARR" formuLatex="\sum \frac{P_s \times U_s}{D_y}" variables={[ { nameInTheFormula: "P_s", variableName: "price", variableDescription: "Subscription price", variableValue: 240 }, { nameInTheFormula: "U_s", variableName: "subs", variableDescription: "Active paid subs", variableValue: 2, isInteger: true }, { nameInTheFormula: "D_y", variableName: "periods", variableDescription: "Subscription period", variableValue: 1, options: [ { label: "Weekly", value: "1/52" }, { label: "Monthly", value: "1/12" }, { label: "2 months", value: "2/12" }, { label: "3 months", value: "3/12" }, { label: "6 months", value: "6/12" }, { label: "Annual", value: 1 } ] } ]} formulaCalculation="(price * subs ) / periods" isSum={true} defaultRows={[ { price: 240, subs: 2, periods: "1" }, { price: 30, subs: 10, periods: "1/12" }, { price: 10, subs: 20, periods: "1/52" } ]} /> ## Xử lý hoàn tiền \{#refund-handling\} Khi một gói đăng ký được hoàn tiền, ARR sẽ loại bỏ phần đóng góp của nó khỏi mọi ngày trên biểu đồ mà trước đó đã được tính. Các giá trị ARR trong quá khứ có thể giảm sau khi có hoàn tiền. Để xem so sánh đầy đủ giữa các chỉ số, hãy xem [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ## Đơn vị tiền tệ \{#currency\} Adapty hiển thị tất cả các biểu đồ tiền tệ bằng **đô la Mỹ (USD)**, bất kể đơn vị tiền tệ gốc của giao dịch. Điều này bao gồm Revenue, MRR, ARR, ARPU, ARPPU, LTV, predicted revenue, refund money, và các con số doanh thu trong cohort và báo cáo A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty chuyển đổi mỗi giao dịch sang USD theo tỷ giá từ [currencylayer.com](https://currencylayer.com/) được cập nhật mỗi 8 giờ, **cố định tại thời điểm giao dịch**. Các giá trị USD lịch sử sẽ không được tính lại khi tỷ giá ngoại hối thay đổi. Giá trị theo tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price_local` và `currency` trong webhook - Các cột `_local` (như `revenue_local` và `proceeds_local`) và `currency` trong S3, GCS, và BigQuery exports - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo tiền tệ địa phương, hãy lấy giá trị tiền tệ địa phương theo từng giao dịch từ bản export và tự tổng hợp. ## Định giá gia hạn \{#renewal-pricing\} Adapty tính doanh thu gia hạn theo giá hiện tại của sản phẩm, kể cả với những người dùng đang ở mức giá cũ khi họ đăng ký lần đầu. Sau khi bạn thay đổi giá trong App Store Connect hoặc Google Play, các chỉ số Revenue, MRR và ARR trên dashboard dành cho những người đăng ký hiện tại có thể khác với doanh thu thực tế thu được — Adapty áp dụng giá mới, dù cửa hàng vẫn giữ mức giá cũ cho những người dùng đó. Để kiểm tra, hãy so sánh trường `price` theo từng giao dịch trong bản xuất dữ liệu S3, GCS hoặc BigQuery với dashboard cho cùng các giao dịch đó. Trường trong bản xuất phản ánh những gì cửa hàng đã báo cáo (giá mà khách hàng thực sự đã trả); còn dashboard phản ánh giá sản phẩm hiện tại. ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Quốc gia, Loại ưu đãi, ID ưu đãi, Loại giảm giá ưu đãi, Paywall, A/B test, Placement, Kỳ, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Kỳ, Trạng thái gia hạn, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Audience, Placement, Thời hạn, Loại ưu đãi, Loại giảm giá ưu đãi, ID ưu đãi, Phân khúc và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để xem so sánh song song giữa các chỉ số này, hãy xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [Doanh thu](revenue) - [MRR](mrr) - [ARPU](arpu) - [ARPPU](arppu) --- # File: arpu --- --- title: "ARPU" description: "Phân tích Doanh thu Trung bình trên mỗi Người dùng (ARPU) để tối ưu hóa doanh thu." --- Biểu đồ ARPU (doanh thu trung bình trên mỗi người dùng) hiển thị doanh thu trung bình tạo ra trên mỗi người dùng trong một khoảng thời gian nhất định. Chỉ số này được tính bằng cách chia tổng doanh thu của một cohort khách hàng cho số lượng người dùng trong cohort đó. Dùng ARPU để so sánh hiệu suất doanh thu giữa các phân khúc người dùng — theo nguồn attribution, quốc gia, hoặc sản phẩm. ## Cách tính \{#calculation\} :::warning Máy tính bên dưới **không tính đến** [hoa hồng cửa hàng và thuế](how-adapty-analytics-works#commissions-and-taxes). Hãy so sánh kết quả với tính toán **doanh thu gộp** của bạn. ::: ARPU cho thấy doanh thu trung bình mà ứng dụng của bạn tạo ra trên mỗi người dùng — một tiêu chuẩn phổ biến để đo lường hiệu quả kiếm tiền. ARPU là doanh thu trong kỳ (trừ đi hoàn tiền) chia cho tổng số người dùng ứng dụng trong kỳ đó. <CompoundCalculator client:load heading="ARPU" formuLatex="\frac{\sum P_i \times Q_i - D}{U_p}" variables={[ { nameInTheFormula: "P", variableName: "price", variableDescription: "Product price", variableValue: 10 }, { nameInTheFormula: "Q", variableName: "qty", variableDescription: "Products purchased", variableValue: 1, isInteger: true }, { nameInTheFormula: "D", variableName: "refunds", variableDescription: "Amount refunded", variableValue: 35, global: true }, { nameInTheFormula: "Up", variableName: "users", variableDescription: "Total users", variableValue: 160, global: true, isInteger: true } ]} rowFormula="price * qty" resultFormula="(_sum - refunds) / users" defaultRows={[ { price: 10, qty: 5 }, { price: 50, qty: 10 }, { price: 100, qty: 1 } ]} /> ## Xử lý hoàn tiền \{#refund-handling\} Các khoản hoàn tiền được trừ khỏi tử số doanh thu vào ngày xử lý hoàn tiền. Để xem so sánh đầy đủ giữa các chỉ số, hãy tham khảo [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ## Đơn vị tiền tệ \{#currency\} Adapty hiển thị tất cả các biểu đồ tiền tệ bằng **đô la Mỹ (USD)**, bất kể đơn vị tiền tệ gốc của giao dịch. Điều này bao gồm Revenue, MRR, ARR, ARPU, ARPPU, LTV, predicted revenue, refund money, và các con số doanh thu trong cohort và báo cáo A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty chuyển đổi mỗi giao dịch sang USD theo tỷ giá từ [currencylayer.com](https://currencylayer.com/) được cập nhật mỗi 8 giờ, **cố định tại thời điểm giao dịch**. Các giá trị USD lịch sử sẽ không được tính lại khi tỷ giá ngoại hối thay đổi. Giá trị theo tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price_local` và `currency` trong webhook - Các cột `_local` (như `revenue_local` và `proceeds_local`) và `currency` trong S3, GCS, và BigQuery exports - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo tiền tệ địa phương, hãy lấy giá trị tiền tệ địa phương theo từng giao dịch từ bản export và tự tổng hợp. ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển phân tích](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Quốc gia, A/B test, Phân khúc, và Cửa hàng. - ✅ Nhóm theo: Quốc gia, Cửa hàng, Phân khúc, và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để so sánh song song các chỉ số này, hãy xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [Doanh thu](revenue) - [MRR](mrr) - [ARPPU](arppu) - [ARR](arr) --- # File: arppu --- --- title: "ARPPU" description: "Tìm hiểu về ARPPU (Doanh thu trung bình trên mỗi người dùng trả phí) và tác động của nó đến việc kiếm tiền từ ứng dụng của bạn." --- Biểu đồ Doanh thu trung bình trên mỗi người dùng trả phí (ARPPU) hiển thị doanh thu trung bình trên mỗi người dùng trả phí. Biểu đồ này thể hiện doanh thu thực tế được tạo ra từ những khách hàng có trả tiền, chia cho số lượng khách hàng, sau khi trừ đi các khoản hoàn tiền. Hãy nhóm ARPPU theo attribution để xem kênh thu hút nào mang lại người dùng trả phí có giá trị cao hơn. ## Cách tính \{#calculation\} :::warning Máy tính bên dưới **không tính đến** [hoa hồng cửa hàng và thuế](how-adapty-analytics-works#commissions-and-taxes). Hãy so sánh kết quả với số liệu **doanh thu gộp** của bạn. ::: ARPPU thể hiện doanh thu trung bình trên mỗi người dùng trả phí — thường cao hơn nhiều so với [ARPU](arpu) vì những người dùng không trả tiền không được tính vào mẫu số. ARPPU là doanh thu trong kỳ (trừ hoàn tiền) chia cho số người dùng trả phí trong kỳ đó. <CompoundCalculator client:load heading="ARPPU" formuLatex="\frac{\sum P_i \times Q_i - D}{U_p}" variables={[ { nameInTheFormula: "P", variableName: "price", variableDescription: "Giá sản phẩm", variableValue: 10 }, { nameInTheFormula: "Q", variableName: "qty", variableDescription: "Sản phẩm đã mua", variableValue: 1, isInteger: true }, { nameInTheFormula: "D", variableName: "refunds", variableDescription: "Số tiền hoàn trả", variableValue: 35, global: true }, { nameInTheFormula: "Up", variableName: "users", variableDescription: "Người dùng trả phí", variableValue: 16, global: true, isInteger: true } ]} rowFormula="price * qty" resultFormula="(_sum - refunds) / users" defaultRows={[ { price: 10, qty: 5 }, { price: 50, qty: 10 }, { price: 100, qty: 1 } ]} /> ## Xử lý hoàn tiền \{#refund-handling\} Các khoản hoàn tiền được trừ khỏi tử số doanh thu theo ngày xử lý hoàn tiền. Người dùng đã mua hàng nhưng sau đó được hoàn tiền vẫn được tính vào mẫu số người dùng trả phí, vì vậy việc hoàn tiền nhiều sẽ kéo ARPPU giảm nhanh hơn dự kiến. Để xem so sánh đầy đủ giữa các chỉ số, hãy xem [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ## Đơn vị tiền tệ \{#currency\} Adapty hiển thị tất cả các biểu đồ tiền tệ bằng **đô la Mỹ (USD)**, bất kể đơn vị tiền tệ gốc của giao dịch. Điều này bao gồm Revenue, MRR, ARR, ARPU, ARPPU, LTV, predicted revenue, refund money, và các con số doanh thu trong cohort và báo cáo A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty chuyển đổi mỗi giao dịch sang USD theo tỷ giá từ [currencylayer.com](https://currencylayer.com/) được cập nhật mỗi 8 giờ, **cố định tại thời điểm giao dịch**. Các giá trị USD lịch sử sẽ không được tính lại khi tỷ giá ngoại hối thay đổi. Giá trị theo tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price_local` và `currency` trong webhook - Các cột `_local` (như `revenue_local` và `proceeds_local`) và `currency` trong S3, GCS, và BigQuery exports - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo tiền tệ địa phương, hãy lấy giá trị tiền tệ địa phương theo từng giao dịch từ bản export và tự tổng hợp. ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Paywall, A/B test, Placement, Kỳ, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Kỳ, Trạng thái gia hạn, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Phân khúc và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để so sánh chi tiết các chỉ số này, hãy xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [Doanh thu](revenue) - [MRR](mrr) - [ARPU](arpu) - [ARR](arr) --- # File: installs --- --- title: "Lượt cài đặt" description: "Theo dõi lượt cài đặt ứng dụng và hiểu tác động của chúng đối với gói đăng ký với Adapty." --- Biểu đồ Lượt cài đặt hiển thị số người dùng đã cài đặt ứng dụng của bạn trong khoảng thời gian đã chọn. Điều gì được tính là một lượt cài đặt — và mỗi lượt cài đặt được nhóm như thế nào — phụ thuộc vào cài đặt đếm lượt cài đặt. Bài viết này giải thích cách chọn chế độ đếm phù hợp và cách [xử lý các sự khác biệt có thể xảy ra](#troubleshooting) giữa các nguồn phân tích khác nhau. ## Điều gì được tính là một lượt cài đặt \{#what-counts-as-an-install\} SDK của Adapty đăng ký một "lượt cài đặt" và gửi nó đến Adapty khi người dùng khởi chạy ứng dụng của bạn lần đầu tiên. Điều này dẫn đến hai hệ quả: - Một lượt cài đặt xuất hiện trong Adapty khi người dùng mở ứng dụng lần đầu tiên, có thể là vài giờ hoặc vài ngày sau khi họ tải xuống. - Nếu người dùng tải xuống nhưng không bao giờ mở ứng dụng, Adapty sẽ không tính họ. **Múi giờ báo cáo** của bạn trong App Settings sẽ quyết định mỗi lượt cài đặt rơi vào ngày nào. Một lượt cài đặt lúc 23:30 UTC ngày 1 tháng 6 sẽ được tính vào ngày 2 tháng 6 nếu múi giờ báo cáo của bạn là +02:00, trong khi App Store Connect hoặc Google Play có thể hiển thị vào ngày 1 tháng 6. ### Chế độ đếm \{#counting-modes\} Cài đặt **Installs definition for analytics** xác định điều gì được tính là một lượt cài đặt mới. Để thay đổi, mở [App Settings → General → Installs definition for analytics](general#4-installs-definition-for-analytics). | Chế độ | Điều gì được tính | Ví dụ | Chỉ số bên thứ ba | Sự khác biệt có thể xảy ra | | --- | --- | --- | --- | --- | | **New device_ids** (khuyến nghị) | **Mọi lượt cài đặt ứng dụng** — bao gồm cả cài đặt lại. Xác thực, tạo hồ sơ người dùng và nâng cấp phiên bản không được tính thêm. | Một người dùng trên 5 thiết bị = 5 lượt cài đặt. <br /> <br /> Cài đặt lại trên cùng một thiết bị = 2 lượt cài đặt. | App Store: <br /> **Total Active Devices** <br /> <br /> Google Play: **Devices** | **Cao hơn số lượt tải ứng dụng** nếu việc cài đặt lại phổ biến. <br /> <br /> **Thấp hơn số lượt tải ứng dụng** nếu nhiều người dùng tải xuống mà không mở ứng dụng. | | **New customer_user_ids** | Chỉ tính **lượt cài đặt đầu tiên** theo [người dùng đã xác định](identifying-users). Các thiết bị bổ sung và người dùng ẩn danh không được tính. | Một người dùng trên 5 thiết bị = 1 lượt cài đặt. <br /> <br /> Cài đặt lại, đăng nhập lại = không có lượt cài đặt mới. <br /> <br /> Sử dụng ứng dụng không có tài khoản = không có lượt cài đặt mới. | Thống kê đăng ký từ hệ thống xác thực của ứng dụng | **Luôn bằng 0** nếu bạn không xác định người dùng. | | **New profiles in Adapty** (cũ) | Đếm mọi lượt cài đặt và cài đặt lại, **cũng như các hồ sơ người dùng ẩn danh được tạo khi đăng xuất**. | Một người dùng, một thiết bị, 3 lần đăng xuất = 4 lượt cài đặt. | Không có | **Cao hơn tất cả các chỉ số bên ngoài**. Đếm mỗi hồ sơ người dùng ẩn danh được tạo khi đăng xuất là một lượt cài đặt. | Sử dụng **New device_ids** trừ khi bạn có lý do cụ thể để chuyển đổi. ## Xử lý sự khác biệt \{#troubleshooting\} ### Số liệu của Adapty cao hơn App Store Connect hoặc Google Play \{#adaptys-count-is-higher-than-app-store-connect-or-google-play\} Hai nguyên nhân có thể xảy ra: - **Cài đặt lại.** Nếu [chế độ đếm](#counting-modes) của bạn được đặt thành **New device_ids**, Adapty đếm cả lần khởi chạy đầu tiên và các lần cài đặt lại tiếp theo. "Total Downloads" của App Store Connect chỉ đếm lượt tải xuống ban đầu. - **Ngày khởi chạy lần đầu ≠ ngày tải xuống.** Các cửa hàng ghi nhận theo ngày tải xuống. Người dùng mở ứng dụng muộn sẽ rơi vào các ngày khác nhau. Để so sánh chính xác hơn, hãy mở **App Store Connect → Total Active Devices** hoặc **Google Play → Devices**. Các chỉ số đó ở cấp thiết bị và gần hơn với chế độ **New device_ids** của Adapty. ### Số liệu của Adapty bằng 0 \{#adaptys-count-is-zero\} Nếu chế độ đếm của bạn là **New customer_user_ids**, nhưng bạn không <InlineTooltip tooltip="xác thực người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), [Unity](unity-identifying-users), [Kotlin Multiplatform](kmp-quickstart-identify), [Capacitor](capacitor-quickstart-identify)</InlineTooltip>, Adapty sẽ không đăng ký bất kỳ lượt cài đặt nào. Trong chế độ đó, các lượt cài đặt ẩn danh bị loại trừ. Hãy chuyển sang **New device_ids** hoặc triển khai xác định người dùng. ### Số liệu của Adapty khác với AppsFlyer hoặc Adjust \{#adaptys-count-differs-from-appsflyer-or-adjust\} Các MMP ghi nhận lượt cài đặt theo sự kiện khởi tạo SDK của riêng họ hoặc sự kiện first-touch. Những sự kiện này diễn ra theo lịch trình khác với lần khởi chạy SDK đầu tiên của Adapty — một số sự khác biệt là bình thường. ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển phân tích](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Quốc gia, A/B test, Phân khúc và Cửa hàng. - ✅ Nhóm theo: Quốc gia, Cửa hàng, Phân khúc và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để so sánh song song các chỉ số này, xem [Bảng so sánh chỉ số](metric-comparison-table#subscribers-and-conversion). - [Gói đăng ký mới](reactivated-subscriptions) - [Gói đăng ký đang hoạt động](active-subscriptions) - [Dùng thử mới](new-trials) --- # File: active-subscriptions --- --- title: "Gói đăng ký đang hoạt động" description: "Theo dõi và quản lý các gói đăng ký đang hoạt động với tính năng phân tích mạnh mẽ của Adapty." --- Biểu đồ Gói đăng ký đang hoạt động hiển thị số lượng gói đăng ký trả phí duy nhất chưa hết hạn vào cuối mỗi khoảng thời gian được chọn. Biểu đồ này bao gồm các in-app subscription thông thường (chưa hết hạn) đã bắt đầu và hiện đang hoạt động, đồng thời loại trừ cả bản dùng thử miễn phí lẫn các gói đăng ký đã hủy gia hạn. Đây là thước đo phản ánh quy mô và mức tăng trưởng của cơ sở người dùng đăng ký. ## Cách tính \{#calculation\} Chỉ số gói đăng ký đang hoạt động đếm số gói đăng ký trả phí chưa hết hạn vào cuối mỗi khoảng thời gian. Đối với các gói đăng ký không có thời gian ân hạn, việc hết hạn xảy ra khi ngày gia hạn tiếp theo đến mà không có lần gia hạn thành công nào. Ví dụ: 500 gói đăng ký đang hoạt động vào cuối tháng trước, cộng thêm 50 gói mới trong tháng này, trừ đi 25 gói đã hết hạn trong tháng này = 525 gói đăng ký đang hoạt động vào cuối tháng này. ## Xử lý hoàn tiền \{#refund-handling\} Khi một gói đăng ký được hoàn tiền, Adapty sẽ xóa nó khỏi số lượng đang hoạt động — cả hiện tại lẫn hồi tố cho các ngày trong quá khứ. Để xem so sánh đầy đủ giữa các chỉ số, hãy xem [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Các điều khiển phân tích](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Loại ưu đãi, ID ưu đãi, Loại giảm giá ưu đãi, Paywall, A/B test, Placement, Khoảng thời gian, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Khoảng thời gian, Trạng thái gia hạn, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Loại ưu đãi, Loại giảm giá ưu đãi, ID ưu đãi, Phân khúc và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh song song các chỉ số này, hãy xem [Bảng so sánh chỉ số](metric-comparison-table#subscribers-and-conversion). - [Gói đăng ký đã rời bỏ (hết hạn)](churned-expired-subscriptions) - [Gói đăng ký đã hủy](cancelled-subscriptions) - [Sản phẩm mua một lần](non-subscriptions) --- # File: reactivated-subscriptions --- --- title: "Gói đăng ký mới" description: "Theo dõi gói đăng ký mới trong Adapty để giám sát lượt chuyển đổi lần đầu và chuyển đổi từ dùng thử miễn phí sang trả phí." --- Biểu đồ Gói đăng ký mới hiển thị số lượng gói đăng ký mới (được kích hoạt lần đầu) trong ứng dụng của bạn. Chỉ số này cho biết số lượng gói đăng ký mới bắt đầu trong một khoảng thời gian cụ thể, bao gồm cả các gói đăng ký bắt đầu từ đầu lẫn các bản dùng thử miễn phí chuyển đổi thành gói trả phí. Chỉ số này không bao gồm các lần gia hạn gói đăng ký hoặc các gói đăng ký đã được khởi động lại. ## Cách tính \{#calculation\} Chỉ số gói đăng ký mới đếm số lần kích hoạt gói đăng ký lần đầu trong kỳ — bao gồm cả các gói bắt đầu từ đầu và các bản dùng thử miễn phí chuyển đổi thành gói trả phí. ## Xử lý hoàn tiền \{#refund-handling\} Gói đăng ký mới **không** trừ các khoản hoàn tiền — số lượng bao gồm cả các gói đăng ký đã được hoàn tiền sau đó. Để đánh giá tác động thực, hãy so sánh với [Sự kiện hoàn tiền](refund-events). Để xem so sánh đầy đủ giữa các chỉ số, xem [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Công cụ điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Loại ưu đãi, ID ưu đãi, Loại giảm giá ưu đãi, Paywall, A/B test, Placement, Khoảng thời gian, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Trạng thái gia hạn, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Loại ưu đãi, Loại giảm giá ưu đãi, ID ưu đãi, Phân khúc và Attribution. ## Chỉ số tương tự \{#similar-metrics\} Để xem so sánh song song giữa các chỉ số này, xem [Bảng so sánh chỉ số](metric-comparison-table#subscribers-and-conversion). - [Gói đăng ký đang hoạt động](active-subscriptions) - [Gói đăng ký đã hủy (hết hạn)](churned-expired-subscriptions) - [Gói đăng ký đã hủy](cancelled-subscriptions) - [Sản phẩm mua một lần](non-subscriptions) --- # File: non-subscriptions --- --- title: "Sản phẩm mua một lần" description: "Tìm hiểu cách quản lý sản phẩm mua một lần trong Adapty và theo dõi giao dịch mua của người dùng một cách hiệu quả." --- Biểu đồ Sản phẩm mua một lần đếm các in-app purchase không phải gói đăng ký tự gia hạn: consumable, non-consumable và gói đăng ký không tự gia hạn. Không tính các lần gia hạn. :::note "Sản phẩm mua một lần" rộng hơn "sản phẩm mua một lần duy nhất" — consumable và gói đăng ký không tự gia hạn đều có thể được mua nhiều hơn một lần. ::: ## Cách tính \{#calculation\} Mỗi in-app purchase dạng sản phẩm mua một lần thuộc một trong ba loại: - **Consumable**: các mục người dùng có thể mua nhiều lần, chẳng hạn như thức ăn cá trong ứng dụng câu cá hoặc tiền tệ trong game. - **Non-consumable**: các mục người dùng mua một lần và dùng mãi mãi, chẳng hạn như một đường đua trong game hoặc phiên bản không có quảng cáo. - **Gói đăng ký không tự gia hạn**: các gói đăng ký hết hạn sau một khoảng thời gian nhất định và không tự động gia hạn, chẳng hạn như quyền truy cập một năm vào thư viện nội dung. Nội dung có thể tĩnh, nhưng gói đăng ký sẽ không gia hạn khi hết hạn. :::note Biểu đồ này chỉ đếm các sự kiện mua và không trừ các giao dịch đã được hoàn tiền. Nếu bạn có sản phẩm mua một lần thường xuyên bị hoàn tiền, số lượng hiển thị sẽ cao hơn số lượng giao dịch thực sự tạo ra doanh thu. ::: ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Các tùy chọn kiểm soát Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Paywall, A/B test, Placement, Phân khúc, Cửa hàng và Sản phẩm. - ✅ Nhóm theo: Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Phân khúc và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để so sánh các chỉ số này cạnh nhau, xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [Gói đăng ký đang hoạt động](active-subscriptions) - [Gói đăng ký mới](reactivated-subscriptions) - [Gói đăng ký đã hủy (hết hạn)](churned-expired-subscriptions) - [Gói đăng ký đã hủy](cancelled-subscriptions) --- # File: cancelled-subscriptions --- --- title: "Gia hạn gói đăng ký bị hủy" description: "Quản lý hiệu quả các gói đăng ký bị hủy với công cụ quản lý của Adapty." --- Biểu đồ Gia hạn gói đăng ký bị hủy hiển thị số lượng gói đăng ký đã tắt trạng thái tự động gia hạn (do người dùng hủy). Khi trạng thái tự động gia hạn của một gói đăng ký bị tắt, gói đó sẽ không tự động gia hạn cho kỳ tiếp theo. Tuy nhiên, người dùng vẫn được truy cập các tính năng cao cấp của ứng dụng cho đến khi kỳ hiện tại kết thúc. ## Cách tính \{#calculation\} Chỉ số gia hạn gói đăng ký bị hủy đếm số gói đăng ký có tự động gia hạn bị tắt trong kỳ. Người dùng vẫn giữ quyền truy cập cao cấp cho đến khi kỳ thanh toán hiện tại kết thúc, nhưng gói đăng ký sẽ không tự động gia hạn sau đó. ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Paywall, A/B test, Placement, Kỳ, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Phân khúc và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh trực tiếp các chỉ số này, xem [Bảng so sánh chỉ số](metric-comparison-table#churn). - [Gói đăng ký đang hoạt động](active-subscriptions) - [Gói đăng ký đã rời bỏ (hết hạn)](churned-expired-subscriptions) - [Gói đăng ký mới](reactivated-subscriptions) - [Sản phẩm không phải gói đăng ký](non-subscriptions) --- # File: churned-expired-subscriptions --- --- title: "Gói đăng ký đã hủy (hết hạn)" description: "Quản lý các gói đăng ký đã hủy và hết hạn để cải thiện khả năng giữ chân người dùng." --- Biểu đồ gói đăng ký đã hủy (hết hạn) hiển thị số lượng gói đăng ký đã hết hạn, tức là người dùng không còn quyền truy cập vào các tính năng cao cấp của ứng dụng. Thông thường, điều này xảy ra khi người dùng quyết định ngừng thanh toán vào cuối kỳ đăng ký hoặc gặp sự cố thanh toán. Hãy nhóm theo lý do hết hạn để phân biệt giữa churn tự nguyện và churn do vấn đề thanh toán. ## Cách tính \{#calculation\} Chỉ số gói đăng ký đã hủy (hết hạn) đếm số gói đăng ký đã hết hạn trong khoảng thời gian — người dùng mất quyền truy cập vào các tính năng cao cấp. Bao gồm cả những người dùng chọn không gia hạn lẫn những người mất gói đăng ký do sự cố thanh toán. ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Các điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Paywall, A/B test, Placement, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Lý do hết hạn, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Phân khúc và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh các chỉ số này cạnh nhau, xem [Bảng so sánh chỉ số](metric-comparison-table#churn). - [Gói đăng ký đang hoạt động](active-subscriptions) - [Gói đăng ký mới](reactivated-subscriptions) - [Gói đăng ký đã hủy](cancelled-subscriptions) - [Sản phẩm mua một lần](non-subscriptions) --- # File: active-trials --- --- title: "Active trials" description: "Theo dõi và quản lý các bản dùng thử gói đăng ký đang hoạt động với Adapty analytics." --- Biểu đồ active trials trong Adapty hiển thị số lượng bản dùng thử miễn phí chưa hết hạn đang hoạt động vào cuối một khoảng thời gian nhất định. Hoạt động có nghĩa là các gói đăng ký chưa hết hạn; do đó, người dùng vẫn có quyền truy cập vào các tính năng trả phí của ứng dụng. ## Cách tính \{#calculation\} Chỉ số active trials đếm số bản dùng thử miễn phí chưa hết hạn vào cuối mỗi kỳ. Hủy gia hạn tự động không loại bỏ một bản dùng thử khỏi số đếm — chỉ khi hết hạn mới bị loại. Ví dụ: 100 active trials hôm qua, cộng thêm 10 bản mới hôm nay, trừ đi 5 bản hết hạn hôm nay = 105 active trials hôm nay. ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Country, Offer Type, Offer ID, Offer Discount Type, Paywall, A/B tests, Placement, Segment, Store, Product và Duration. - ✅ Nhóm theo: Period, Renewal status, Product, Country, Store, Paywall, Audience, Placement, Duration, Offer Type, Offer Discount Type, Offer ID, Segment và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh các chỉ số này cạnh nhau, xem [Bảng so sánh chỉ số](metric-comparison-table#subscribers-and-conversion). - [New trials](new-trials) - [Trial renewal cancelled](trials-renewal-cancelled) - [Expired trials](expired-churned-trials) --- # File: new-trials --- --- title: "Các trial mới" description: "Quản lý các trial gói đăng ký mới và tối ưu hóa tỷ lệ chuyển đổi từ trial sang trả phí." --- Biểu đồ trial mới hiển thị số lượng trial được kích hoạt trong khoảng thời gian đã chọn. Dùng biểu đồ này để theo dõi lượng trial từ các chiến dịch quảng cáo và các hoạt động thu hút người dùng khác. ## Cách tính \{#calculation\} Chỉ số trial mới đếm số trial được bắt đầu trong kỳ, bất kể chúng có còn hoạt động vào cuối kỳ hay không. Ví dụ: nếu 50 người dùng bắt đầu trial trong tháng 5, điểm dữ liệu tháng 5 sẽ hiển thị 50 — dù một số trial đã hết hạn hoặc đã chuyển sang trả phí vào thời điểm bạn xem biểu đồ. ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Quốc gia, Loại ưu đãi, ID ưu đãi, Loại giảm giá ưu đãi, Paywall, A/B test, Placement, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Sản phẩm, Quốc gia, Cửa hàng, Paywall, Audience, Placement, Thời hạn, Loại ưu đãi, Loại giảm giá ưu đãi, ID ưu đãi, Phân khúc và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh các chỉ số này theo dạng bảng, xem [Bảng so sánh chỉ số](metric-comparison-table#subscribers-and-conversion). - [Trial đang hoạt động](active-trials) - [Trial đã hủy gia hạn](trials-renewal-cancelled) - [Trial đã hết hạn](expired-churned-trials) --- # File: trials-renewal-cancelled --- --- title: "Hủy gia hạn dùng thử" description: "Tìm hiểu về gia hạn dùng thử, hủy gia hạn và các luồng đăng ký với thông tin chi tiết từ Adapty." --- Biểu đồ Hủy gia hạn dùng thử hiển thị số lượng gói dùng thử đã bị hủy gia hạn (do người dùng hủy). Khi tắt gia hạn cho gói dùng thử, gói đó sẽ không tự động chuyển sang gói đăng ký có trả phí, nhưng người dùng vẫn được sử dụng các tính năng premium của ứng dụng cho đến khi hết kỳ hiện tại. ## Cách tính \{#calculation\} Chỉ số hủy gia hạn dùng thử đếm số gói dùng thử mà người dùng đã tắt tự động gia hạn trong kỳ. Người dùng vẫn giữ quyền truy cập dùng thử cho đến khi hết hạn, nhưng gói dùng thử sẽ không tự động chuyển sang gói đăng ký có trả phí. ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Các điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Paywall, A/B test, Placement, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Phân khúc và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh các chỉ số này cạnh nhau, xem [Bảng so sánh chỉ số](metric-comparison-table#churn). - [Dùng thử mới](new-trials) - [Dùng thử đang hoạt động](active-trials) - [Dùng thử đã hết hạn](expired-churned-trials) --- # File: expired-churned-trials --- --- title: "Các bản dùng thử đã hết hạn (churn)" description: "Quản lý hiệu quả các bản dùng thử đã hết hạn và churn với analytics của Adapty." --- Biểu đồ Expired (churned) trials hiển thị số lượng bản dùng thử đã hết hạn, khiến người dùng mất quyền truy cập vào các tính năng cao cấp của ứng dụng. Trong hầu hết các trường hợp, điều này xảy ra khi người dùng quyết định không trả tiền cho ứng dụng hoặc gặp sự cố thanh toán. ## Cách tính \{#calculation\} Chỉ số expired trials đếm các bản dùng thử đã kết thúc trong kỳ — người dùng mất quyền truy cập vào các tính năng cao cấp. Điều này bao gồm cả những người dùng chủ động không chuyển đổi lẫn những người không chuyển đổi được do sự cố thanh toán. Nhóm theo **Expiration reason** để tách biệt churn tự nguyện khỏi churn do lỗi thanh toán. ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Country, Paywall, A/B tests, Placement, Segment, Store, Product và Duration. - ✅ Nhóm theo: Expiration reason, Product, Country, Store, Paywall, Audience, Placement, Duration, Segment và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh các chỉ số này cạnh nhau, xem [Bảng so sánh chỉ số](metric-comparison-table#churn). - [Bản dùng thử mới](new-trials) - [Bản dùng thử đang hoạt động](active-trials) - [Bản dùng thử đã hủy gia hạn](trials-renewal-cancelled) --- # File: refund-events --- --- title: "Sự kiện hoàn tiền" description: "Quản lý sự kiện hoàn tiền trong Adapty để giảm tỷ lệ rời bỏ và tối ưu hóa doanh thu." --- Biểu đồ Refund events hiển thị số lượng giao dịch mua và gói đăng ký đã được hoàn tiền. Adapty gắn mỗi sự kiện hoàn tiền với ngày hoàn tiền được thực hiện, không phải ngày bắt đầu gói đăng ký. ## Cách tính \{#calculation\} Adapty đếm tất cả các giao dịch mua hoặc gói đăng ký được hoàn tiền trong khoảng thời gian đã chọn. Mỗi lần hoàn tiền được gán cho ngày xảy ra, không phải ngày bắt đầu gói đăng ký. Hoàn tiền cho các bản dùng thử (trial) bị loại trừ vì trial không tạo ra doanh thu. ## Cách các chỉ số xử lý hoàn tiền \{#how-metrics-handle-refunds\} Các chỉ số khác nhau xử lý hoàn tiền theo những cách khác nhau. Cùng một sự kiện hoàn tiền có thể làm giảm một biểu đồ ngay lập tức, làm giảm biểu đồ khác theo cách hồi tố (thay đổi giá trị của các kỳ trong quá khứ), hoặc hoàn toàn không ảnh hưởng đến biểu đồ thứ ba. Ma trận dưới đây trình bày quy tắc áp dụng cho từng chỉ số. | Chỉ số | Hoàn tiền được áp dụng? | Ngày attribution | Có thể âm không? | Ghi chú | | --- | --- | --- | --- | --- | | [Revenue](revenue) | Có | Ngày hoàn tiền — không phải ngày mua ban đầu | Có — vào những ngày hoàn tiền vượt doanh thu mới | Revenue = tổng giao dịch − hoàn tiền. | | [MRR](mrr) | Có, hồi tố | Gói đăng ký bị xóa khỏi tất cả các kỳ nó đang hoạt động | Không | Giá trị các kỳ trước có thể giảm sau khi hoàn tiền. | | [ARR](arr) | Có, hồi tố | Giống MRR | Không | Giá trị các kỳ trước có thể giảm sau khi hoàn tiền. | | [ARPU](arpu) | Có | Ngày hoàn tiền | Có (trong các kỳ hoàn tiền nhiều) | Hoàn tiền trừ vào tử số doanh thu. | | [ARPPU](arppu) | Có, chỉ tử số | Ngày hoàn tiền | Có (trong các kỳ hoàn tiền nhiều) | Hoàn tiền trừ vào tử số doanh thu. Người dùng đã hoàn tiền vẫn được tính vào mẫu số người dùng trả tiền, nên hoàn tiền nhiều sẽ kéo ARPPU xuống nhanh hơn dự kiến. | | [Active subscriptions](active-subscriptions) | Có, hồi tố | Gói đăng ký bị xóa khỏi số đếm | Không | | | [New subscriptions](reactivated-subscriptions) | **Không** | — | Không | Số đếm bao gồm cả các gói đăng ký bị hoàn tiền sau đó. So sánh với [Refund events](refund-events) để xem tác động ròng. | | [Refund money](refund-money) / [Refund events](refund-events) | Hoàn tiền **chính là** dữ liệu | Ngày hoàn tiền | Không (luôn ≥ 0) | | | [Retention](analytics-retention) | **Không** | — | Không | Người dùng đã hoàn tiền vẫn được tính trên đường cong retention. Điều này có thể khiến Retention trông cao hơn [Active subscriptions](active-subscriptions) hoặc [Revenue](revenue) cho cùng một cohort. | | [Cohort revenue](analytics-cohorts) | Có, lũy kế | Ngày hoàn tiền | Không (các khoản trừ lũy kế không đẩy cohort revenue xuống dưới 0) | Hoàn tiền trừ vào cohort revenue khi phát sinh. Với các chỉ số cohort khác, xem [Cohorts > Refund handling](analytics-cohorts#refund-handling). | | [Paywall metrics](paywall-metrics) / [A/B test metrics](results-and-metrics) (số đếm) | **Không** | — | Không | Số Subscribers, Paying Subscribers và ARPPU trên các trang này không trừ hoàn tiền. | | GCS / S3 exports | Hoàn tiền là hàng sự kiện riêng | `event_datetime` = thời điểm hoàn tiền | Các cột ròng có thể âm khi tổng hợp | Hàng hoàn tiền mang `is_refund = true` (S3/GCS) hoặc loại sự kiện `subscription_refunded` (webhooks). | ### Giá trị âm \{#negative-values\} Trong các chế độ xem tổng hợp (biểu đồ Revenue, analytics tùy chỉnh từ exports), một chỉ số có thể hiển thị giá trị âm cho một kỳ hoặc nhóm cụ thể khi hoàn tiền trong khoảng đó vượt quá doanh thu mới trong cùng khoảng. Đây không phải lỗi — đó là phép tính hoạt động đúng như thiết kế. Ví dụ: một quốc gia không có giao dịch mua mới vào thứ Ba, nhưng hôm đó lại xử lý một khoản hoàn tiền $100 cho một giao dịch mua cũ. Doanh thu của quốc gia đó trong ngày thứ Ba sẽ hiển thị là −$100. ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Refund Reason, Country, Offer Type, Offer ID, Offer Discount Type, Paywall, A/B tests, Placement, Period, Segment, Store, Product, và Duration. - ✅ Nhóm theo: Refund Reason, Product, Country, Store, Paywall, Audience, Placement, Duration, Offer Type, Offer Discount Type, Offer ID, Segment, và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh các chỉ số này cạnh nhau, xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [Refund money](refund-money) - [Billing issue](billing-issue) - [Grace period](grace-period) --- # File: refund-money --- --- title: "Hoàn tiền" description: "Tìm hiểu cách xử lý hoàn tiền cho các gói đăng ký trong Adapty mà không bị mất doanh thu." --- Biểu đồ Hoàn tiền hiển thị số tiền đã được hoàn trong khoảng thời gian đã chọn. Adapty gắn mỗi sự kiện hoàn tiền với ngày phát sinh, vì vậy doanh thu sẽ giảm trong cùng khoảng thời gian đó. ## Cách tính \{#calculation\} Adapty chỉ tính các giao dịch tạo ra doanh thu — gói đăng ký mới có trả phí, gia hạn và sản phẩm mua một lần. Bản dùng thử miễn phí không tạo ra doanh thu và không thể hoàn tiền nên sẽ bị loại trừ. Mỗi khoản hoàn tiền được gắn với ngày xử lý, vì vậy mức giảm doanh thu xuất hiện trong cùng khoảng thời gian đó. :::info Số tiền hoàn được tính trước khi khấu trừ phí của cửa hàng. ::: ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Lý do hoàn tiền, Quốc gia, Loại ưu đãi, ID ưu đãi, Loại giảm giá ưu đãi, Paywall, A/B test, Placement, Khoảng thời gian, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Lý do hoàn tiền, Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Loại ưu đãi, Loại giảm giá ưu đãi, ID ưu đãi, Phân khúc và Attribution. ## Quản lý yêu cầu hoàn tiền \{#refund-request-management\} Refund saver giúp người dùng Adapty xử lý các yêu cầu hoàn tiền từ App Store của Apple một cách hiệu quả hơn thông qua tính năng tự động hóa. Công cụ này tiết kiệm thời gian và giảm thiểu mất mát doanh thu bằng cách đơn giản hóa quy trình. Với thông báo theo thời gian thực và thông tin chi tiết có thể hành động, công cụ này giúp bạn dễ dàng xử lý các yêu cầu hoàn tiền trong khi vẫn tuân thủ các hướng dẫn của Apple. Tìm hiểu thêm về [Refund saver](refund-saver). ## Chỉ số tương tự \{#similar-metrics\} Để so sánh các chỉ số này, xem [Bảng so sánh chỉ số](metric-comparison-table#revenue). - [Sự kiện hoàn tiền](refund-events) - [Vấn đề thanh toán](billing-issue) - [Thời gian ân hạn](grace-period) --- # File: grace-period --- --- title: "Thời gian ân hạn" description: "Tìm hiểu cách thời gian ân hạn của gói đăng ký hoạt động và cải thiện khả năng giữ chân người dùng." --- Biểu đồ Thời gian ân hạn hiển thị số lượng gói đăng ký đã chuyển sang trạng thái thời gian ân hạn do [sự cố thanh toán](billing-issue). Trong thời gian này, gói đăng ký vẫn còn hiệu lực trong khi cửa hàng cố gắng thu tiền từ người dùng. Nếu thanh toán không thành công trước khi thời gian ân hạn kết thúc, gói đăng ký sẽ chuyển sang trạng thái sự cố thanh toán. ## Cách tính \{#calculation\} Chỉ số thời gian ân hạn đếm các gói đăng ký đã bước vào thời gian ân hạn trong khoảng thời gian được chọn. Thời gian ân hạn bắt đầu khi thanh toán gia hạn của gói đăng ký thất bại và kéo dài tối đa 6 ngày đối với gói đăng ký hàng tuần hoặc 16 ngày đối với các chu kỳ thanh toán khác. Nếu thanh toán thành công trong khoảng thời gian này, gói đăng ký tiếp tục bình thường; nếu không, gói đăng ký sẽ chuyển sang trạng thái [sự cố thanh toán](billing-issue). ## Bộ lọc và nhóm khả dụng \{#available-filters-and-grouping\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Paywall, A/B test, Placement, Khoảng thời gian, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Sản phẩm, Quốc gia, Cửa hàng, Paywall, Đối tượng, Placement, Thời hạn, Phân khúc và Attribution. ## Các chỉ số tương tự \{#similar-metrics\} Để so sánh các chỉ số này cùng nhau, xem [Bảng so sánh chỉ số](metric-comparison-table#billing-issues-and-revenue-recovery). - [Tiền hoàn trả](refund-money) - [Sự kiện hoàn trả](refund-events) - [Sự cố thanh toán](billing-issue) --- # File: grace-period-converted --- --- title: "Thời gian ân hạn đã chuyển đổi" description: "Theo dõi số lượng gói đăng ký đã bước vào thời gian ân hạn và được gia hạn trước khi thời gian đó kết thúc." --- Biểu đồ **Grace period converted** hiển thị số lượng gói đăng ký đã bước vào trạng thái [thời gian ân hạn](grace-period) và được gia hạn thành công trước khi thời gian đó kết thúc. ### Cách tính \{#calculation\} Biểu đồ Grace period converted hiển thị số lượng gia hạn gói đăng ký hàng ngày của những người dùng đang trong thời gian ân hạn. Thời gian ân hạn bắt đầu khi gói đăng ký bước vào trạng thái lỗi thanh toán do giao dịch thất bại và kết thúc sau một khoảng thời gian nhất định (6 ngày đối với gói đăng ký hàng tuần, 16 ngày đối với tất cả các gói còn lại) hoặc khi thanh toán được thực hiện thành công. Biểu đồ này cung cấp thông tin về hiệu quả của tính năng thời gian ân hạn và có thể giúp phát hiện các vấn đề tiềm ẩn trong quá trình xử lý thanh toán hoặc quản lý gói đăng ký. ### Bộ lọc khả dụng \{#available-filters\} :::link Bài viết chính: [Kiểm soát phân tích](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Refund Reason, Country, Offer Type, Offer ID, Offer Discount Type, Paywall, A/B tests, Placement, Period, Segment, Store, Product và Duration. - ✅ Nhóm theo: Product, Country, Store, Paywall, Audience, Placement, Duration, Segment và Attribution. ### Cách sử dụng biểu đồ Grace period converted \{#grace-period-converted-chart-usage\} Sử dụng biểu đồ này để theo dõi mức độ hiệu quả của tính năng thời gian ân hạn trong việc khôi phục các gói đăng ký gặp sự cố thanh toán. Bằng cách theo dõi xu hướng chuyển đổi theo thời gian, bạn có thể nhận diện các mẫu trong quá trình xử lý thanh toán và đánh giá tác động của các thay đổi đối với flow cập nhật thanh toán hoặc chiến lược giao tiếp trong thời gian ân hạn. ### Các chỉ số liên quan \{#similar-metrics\} - [Lỗi thanh toán](billing-issue) - [Lỗi thanh toán đã chuyển đổi](billing-issue-converted) - [Doanh thu từ lỗi thanh toán đã chuyển đổi](billing-issue-converted-revenue) - [Thời gian ân hạn](grace-period) - [Doanh thu từ thời gian ân hạn đã chuyển đổi](grace-period-converted-revenue) - [Hoàn tiền](refund-money) - [Sự kiện hoàn tiền](refund-events) --- # File: grace-period-converted-revenue --- --- title: "Doanh thu chuyển đổi trong thời gian ân hạn" description: "Theo dõi tổng doanh thu từ các chuyển đổi trong thời gian ân hạn." --- Biểu đồ **Grace period converted revenue** hiển thị doanh thu được tạo ra từ [các chuyển đổi trong thời gian ân hạn](grace-period-converted): các gói đăng ký đã vào trạng thái [thời gian ân hạn](grace-period) và được gia hạn thành công trước khi thời gian đó kết thúc. ### Cách tính \{#calculation\} Biểu đồ Grace period converted revenue hiển thị doanh thu hàng ngày được tạo ra từ việc gia hạn gói đăng ký của những người dùng đang trong thời gian ân hạn. Thời gian ân hạn bắt đầu khi gói đăng ký chuyển sang trạng thái lỗi thanh toán do thanh toán thất bại và kết thúc sau một khoảng thời gian nhất định (6 ngày đối với gói đăng ký hàng tuần, 16 ngày đối với tất cả các gói đăng ký còn lại) hoặc khi thanh toán được thực hiện thành công. Biểu đồ cung cấp thông tin về hiệu quả của tính năng thời gian ân hạn và có thể giúp xác định các vấn đề tiềm ẩn trong quá trình xử lý thanh toán hoặc quản lý gói đăng ký. ### Bộ lọc khả dụng \{#available-filters\} :::link Bài viết chính: [Công cụ điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Refund Reason, Country, Offer Type, Offer ID, Offer Discount Type, Paywall, A/B tests, Placement, Period, Segment, Store, Product và Duration. - ✅ Nhóm theo: Product, Country, Store, Paywall, Audience, Placement, Duration, Segment và Attribution. ### Cách sử dụng biểu đồ Grace period converted revenue \{#grace-period-converted-revenue-chart-usage\} Dùng biểu đồ này để đo lường tác động tài chính của tính năng thời gian ân hạn bằng cách theo dõi doanh thu thu hồi được từ các gói đăng ký gặp vấn đề thanh toán. Điều này giúp bạn đánh giá hiệu quả của chiến lược thời gian ân hạn và xem xét mức độ sinh lời khi triển khai các tính năng hoặc chiến dịch truyền thông liên quan đến thời gian ân hạn. ### Các chỉ số liên quan \{#similar-metrics\} - [Billing issue](billing-issue) - [Billing issue converted](billing-issue-converted) - [Billing issue converted revenue](billing-issue-converted-revenue) - [Grace period](grace-period) - [Grace period converted](grace-period-converted) - [Refund money](refund-money) - [Refund events](refund-events) --- # File: billing-issue --- --- title: "Vấn đề thanh toán" description: "Giải quyết các vấn đề thanh toán gói đăng ký bằng các công cụ hỗ trợ của Adapty." --- Biểu đồ Vấn đề thanh toán hiển thị số lượng gói đăng ký đã chuyển sang trạng thái Billing Issue. Trạng thái này thường xảy ra khi cửa hàng, chẳng hạn như Apple hoặc Google, không thể thu tiền từ người dùng vì một lý do nào đó — ví dụ như thẻ tín dụng hết hạn hoặc không đủ số dư. ## Cách tính \{#calculation\} Chỉ số vấn đề thanh toán đếm số gói đăng ký đã chuyển sang trạng thái billing issue trong khoảng thời gian đó. Một gói đăng ký chuyển sang trạng thái này khi cửa hàng (Apple hoặc Google) không thể xử lý khoản thanh toán gia hạn — thường do thẻ tín dụng hết hạn hoặc không đủ số dư. Trong trạng thái billing issue, gói đăng ký không còn hoạt động. Nếu tính năng [thời gian ân hạn](grace-period) được bật, gói đăng ký chỉ chuyển sang trạng thái billing issue sau khi thời gian ân hạn kết thúc mà vẫn chưa thanh toán được. ## Bộ lọc và nhóm có sẵn \{#available-filters-and-grouping\} :::link Bài viết chính: [Kiểm soát Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Quốc gia, Paywall, A/B test, Placement, Khoảng thời gian, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Sản phẩm, Quốc gia, Cửa hàng, Paywall, Audience, Placement, Thời hạn, Phân khúc và Attribution. ## Các chỉ số liên quan \{#similar-metrics\} Để so sánh các chỉ số này theo từng cột, xem [Bảng so sánh chỉ số](metric-comparison-table#billing-issues-and-revenue-recovery). - [Billing issue converted](billing-issue-converted) - [Billing issue converted revenue](billing-issue-converted-revenue) - [Hoàn tiền](refund-money) - [Sự kiện hoàn tiền](refund-events) - [Thời gian ân hạn](grace-period) - [Grace period converted](grace-period-converted) - [Grace period converted revenue](grace-period-converted-revenue) --- # File: billing-issue-converted --- --- title: "Billing issue đã chuyển đổi" description: "Theo dõi số lượng vấn đề thanh toán được giải quyết trước khi kết thúc chu kỳ thanh toán." --- Biểu đồ Billing issue converted hiển thị số lượng gói đăng ký theo ngày đã rơi vào trạng thái [Billing Issue](billing-issue) và được gia hạn trước khi kết thúc chu kỳ thanh toán. ### Cách tính \{#calculation\} Biểu đồ Billing issue converted hiển thị số lượng gói đăng ký đã rơi vào trạng thái [Billing Issue](billing-issue) trong chu kỳ thanh toán hiện tại và được gia hạn trong ngày đó. Một gói đăng ký rơi vào trạng thái Billing Issue khi cửa hàng (ví dụ: Apple, Google) không thể xử lý thanh toán từ người dùng vì một lý do nào đó, chẳng hạn như thẻ tín dụng hết hạn hoặc không đủ số dư. Trong trạng thái Billing Issue, gói đăng ký không được coi là đang hoạt động. Nếu tính năng Grace Period được bật trong cài đặt cửa hàng, gói đăng ký chỉ chuyển sang trạng thái Billing Issue sau khi thời gian ân hạn kết thúc. ### Bộ lọc khả dụng \{#available-filters\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Refund Reason, Country, Offer Type, Offer ID, Offer Discount Type, Paywall, A/B tests, Placement, Period, Segment, Store, Product và Duration. - ✅ Nhóm theo: Product, Country, Store, Paywall, Audience, Placement, Duration, Segment và Attribution. ### Cách sử dụng biểu đồ Billing issue converted \{#billing-issue-converted-chart-usage\} Sử dụng biểu đồ này để theo dõi mức độ hiệu quả trong việc giải quyết các vấn đề thanh toán trong chu kỳ thanh toán sau khi thời gian ân hạn kết thúc. Bằng cách theo dõi xu hướng giải quyết theo thời gian, bạn có thể nhận ra các mẫu trong việc thu hồi thanh toán và đánh giá tác động của các thay đổi đối với logic thử lại thanh toán hoặc các chiến lược giao tiếp trong quá trình xử lý vấn đề thanh toán. ### Các chỉ số tương tự \{#similar-metrics\} - [Billing issue](billing-issue) - [Billing issue converted revenue](billing-issue-converted-revenue) - [Refund money](refund-money) - [Refund events](refund-events) - [Grace period](grace-period) - [Grace period converted](grace-period-converted) - [Grace period converted revenue](grace-period-converted-revenue) --- # File: billing-issue-converted-revenue --- --- title: "Doanh thu từ billing issue đã chuyển đổi" description: "Giải quyết các vấn đề thanh toán gói đăng ký bằng các công cụ hỗ trợ của Adapty." --- Biểu đồ **Billing issue converted revenue** hiển thị doanh thu từ các [billing issue đã chuyển đổi](billing-issue-converted): các gói đăng ký đã vào trạng thái [Billing Issue](billing-issue) và được gia hạn trước khi kết thúc chu kỳ thanh toán. ### Cách tính \{#calculation\} Biểu đồ **Billing issue converted revenue** hiển thị doanh thu hàng ngày từ các gói đăng ký đã vào trạng thái [Billing Issue](billing-issue) trong chu kỳ thanh toán hiện tại và được gia hạn trong ngày đó. Một gói đăng ký vào trạng thái Billing Issue khi cửa hàng (ví dụ: Apple, Google) không thể xử lý thanh toán từ người dùng vì một lý do nào đó, chẳng hạn như thẻ tín dụng hết hạn hoặc không đủ số dư. Trong trạng thái Billing Issue, gói đăng ký không được coi là đang hoạt động. Nếu tính năng Grace Period được bật trong cài đặt cửa hàng, gói đăng ký sẽ chỉ chuyển sang trạng thái Billing Issue sau khi thời gian ân hạn đã hết. ### Bộ lọc khả dụng \{#available-filters\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: - ✅ Lọc theo: Attribution, Audience, Refund Reason, Country, Offer Type, Offer ID, Offer Discount Type, Paywall, A/B tests, Placement, Period, Segment, Store, Product, và Duration. - ✅ Nhóm theo: Product, Country, Store, Paywall, Audience, Placement, Duration, Segment, và Attribution. ### Cách sử dụng biểu đồ Billing issue converted revenue \{#billing-issue-converted-revenue-chart-usage\} Sử dụng biểu đồ này để đo lường tác động tài chính của các billing issue đã được xử lý bằng cách theo dõi doanh thu thu hồi từ các gói đăng ký sau khi thời gian ân hạn hết. Điều này giúp bạn định lượng hiệu quả của chiến lược khôi phục billing issue và đánh giá mức độ sinh lời khi triển khai cơ chế thử lại thanh toán hoặc các hình thức liên lạc có mục tiêu với người dùng. ### Các chỉ số liên quan \{#similar-metrics\} - [Billing issue](billing-issue) - [Billing issue converted](billing-issue-converted) - [Refund money](refund-money) - [Refund events](refund-events) - [Grace period](grace-period) - [Grace period converted](grace-period-converted) - [Grace period converted revenue](grace-period-converted-revenue) --- # File: ltv --- --- title: "Lifetime Value (LTV)" description: "Tìm hiểu cách tính toán và tối ưu hóa Lifetime Value (LTV) trong Adapty." --- Realized LTV (Lifetime Value) trên mỗi khách hàng trả tiền hiển thị doanh thu mà một cohort khách hàng trả tiền thực sự tạo ra sau khi đã trừ hoàn tiền, chia cho số lượng khách hàng trả tiền trong cohort đó. Vì vậy, biểu đồ này cho bạn biết trung bình bạn tạo ra bao nhiêu doanh thu từ mỗi khách hàng trả tiền. Adapty thiết kế biểu đồ LTV để trả lời một số câu hỏi quan trọng về doanh thu và hành vi khách hàng của ứng dụng, chẳng hạn như: 1. Mỗi cohort mang lại bao nhiêu tiền trong suốt vòng đời sử dụng ứng dụng của họ? 2. Tại thời điểm nào thì một cohort bắt đầu có lãi? 3. Làm thế nào để tối ưu hóa chi phí marketing và mua lại khách hàng nhằm thu hút những khách hàng có giá trị cao, LTV cao? 4. Mất bao lâu để thu hồi chi phí đầu tư vào việc thu hút khách hàng mới? Biểu đồ LTV hoạt động với dữ liệu ứng dụng mà chúng tôi thu thập thông qua SDK và các sự kiện trong ứng dụng. Với thông tin này, bạn có thể hiểu rõ hơn về hiệu suất của các gói đăng ký và doanh thu được tạo ra từ người đăng ký trong một khoảng thời gian nhất định. Bạn có thể dùng thông tin này để đưa ra quyết định sáng suốt về các gói đăng ký, chi phí quảng cáo và chiến lược thu hút khách hàng. Ngoài ra, các bộ lọc cho phép bạn phân khúc dữ liệu theo quốc gia, attribution và các biến số khác, giúp bạn hiểu sâu hơn về tệp khách hàng của mình. ### LTV theo lần gia hạn \{#ltv-by-renewals\} Chế độ xem **LTV by renewals** trình bày dữ liệu liên quan đến chu kỳ đăng ký (P), cụ thể là ghi lại lần đầu tiên khách hàng thực hiện thanh toán. Đối với gói đăng ký hàng tuần, điều này tương ứng với chu kỳ đăng ký hàng tuần tiếp theo. ### LTV theo ngày \{#ltv-by-days\} Chế độ xem **LTV by days** tổ chức và lọc dữ liệu theo các khoảng thời gian hàng ngày, hàng tuần hoặc hàng tháng. Nó cung cấp thông tin về tổng doanh thu được tạo ra bởi tất cả người dùng đã cài đặt ứng dụng vào một ngày, tuần hoặc tháng cụ thể, chia cho số lượng người dùng trả tiền trong cùng khoảng thời gian đó. Chế độ xem này cung cấp thông tin chi tiết có giá trị về theo dõi doanh thu và giúp hiểu toàn diện hành vi người dùng theo thời gian. ### Độ dài cohort và khung thời gian \{#cohort-length-and-time-frame\} Hai cài đặt thời gian kiểm soát những gì bảng hiển thị: - **Time frame** — phạm vi ngày. Đặt trong lịch phía trên bảng. - **Cohort length** — kích thước mỗi hàng: ngày, tuần, tháng, quý, hoặc năm. Với độ dài theo tháng, mỗi hàng bao gồm một tháng cài đặt. Hai cài đặt này hoạt động độc lập. Ví dụ: time frame 6 tháng cộng với cohort length theo tháng cho bạn một bảng có 6 hàng. Time frame 1 năm cộng với cohort length theo tuần cho bạn 52 hàng. ### Cách tính \{#calculation\} Realized LTV được tính bằng tổng doanh thu tạo ra từ mỗi cohort khách hàng, trừ đi hoàn tiền. _LTV cho ngày/tuần/tháng = Doanh thu thu được từ tất cả người dùng trả tiền đã cài đặt ứng dụng vào ngày/tuần/tháng này / số lượng người dùng trả tiền đã cài đặt ứng dụng vào ngày/tuần/tháng này_ Việc tính LTV bao gồm nâng cấp, hạ cấp và tái kích hoạt, chẳng hạn như khi người dùng thay đổi gói đăng ký hoặc mức giá. Nó tính đến doanh thu tạo ra từ gói đăng ký ban đầu và các lần gia hạn tiếp theo dựa trên gói đã cập nhật. ### Nhóm và lọc khả dụng \{#available-grouping-and-filtering\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: Cả bộ lọc và nhóm đều có thể áp dụng cho cả chế độ xem theo lần gia hạn và theo ngày của biểu đồ LTV, cho phép bạn đi sâu vào các cohort cụ thể và hiểu hành vi của họ theo thời gian - ✅ Lọc theo: Attribution, Đối tượng, Quốc gia, Paywall, A/B test, Placement, Phân khúc, Cửa hàng, Sản phẩm và Thời hạn. - ✅ Nhóm theo: Sản phẩm, Quốc gia, Cửa hàng, Thời hạn, Phân khúc và Cohort (Ngày, Tuần, Tháng hoặc Năm). Biểu đồ Realized LTV trong Adapty giúp bạn có được những hiểu biết có giá trị về hành vi khách hàng, tối ưu hóa chiến lược marketing, theo dõi hiệu suất doanh thu và đưa ra các quyết định dựa trên dữ liệu để tối đa hóa giá trị lâu dài của khách hàng. --- # File: analytics-cohorts --- --- title: "Phân tích cohort" description: "Sử dụng cohort phân tích trong Adapty để theo dõi mức độ tương tác của người dùng và xu hướng đăng ký." --- Cohort trong Adapty được thiết kế để trả lời một số câu hỏi quan trọng: 1. Vào ngày nào một cohort bắt đầu có lãi? 2. Ứng dụng kiếm được bao nhiêu tiền từ một cohort cụ thể? 3. Tôi có thể chi tối đa bao nhiêu tiền để thu hút một khách hàng trả phí? 4. Mất bao lâu để thu hồi chi phí quảng cáo? Cohort hoạt động dựa trên dữ liệu ứng dụng mà chúng tôi thu thập qua SDK và thông báo từ cửa hàng, không yêu cầu bất kỳ cấu hình bổ sung nào từ phía bạn. ## Cohort theo số lần gia hạn hoặc theo ngày \{#cohorts-by-renewals-or-by-days\} Bạn có thể phân tích cohort theo số lần gia hạn hoặc theo ngày. Tùy chọn này thay đổi tiêu đề các cột, và theo đó, cách tiếp cận phân tích cũng thay đổi. Theo dõi **theo ngày** cung cấp thông tin hữu ích cho việc lập ngân sách và hiểu rõ lịch thanh toán. Điều này đặc biệt hữu ích khi theo dõi các sản phẩm không phải gói đăng ký, chẳng hạn như consumable hoặc sản phẩm mua một lần. Ở chế độ này, màu xanh trong các ô bảng thường tập trung ở giữa các dòng do hai yếu tố chính. Thứ nhất, xem cohort theo ngày giúp nhìn thấy sớm các khoản thanh toán liên quan đến sản phẩm có thời hạn ngắn, trong khi ở chế độ xem gia hạn, chúng được gộp chung với các lần gia hạn hàng tháng và hàng năm. Thứ hai, các khoản thanh toán bị trễ góp phần tạo ra mô hình phân bổ này, do một số người dùng thanh toán muộn hơn dự kiến. Trong khi đó, theo dõi **theo lần gia hạn** hiển thị mức độ giữ chân và tỷ lệ rời bỏ của các cohort từ lần thanh toán này sang lần thanh toán khác mà không tính đến ngày cụ thể. Vì vậy, những người dùng thanh toán trễ (có thể là nhiều tháng) vẫn được tính vào số lần gia hạn tương ứng của họ. Cách tiếp cận này không phản ánh doanh thu theo lịch thực tế, nhưng lại rất thuận tiện để phân tích mức độ giữ chân và tỷ lệ rời bỏ của các cohort, cũng như rút ra insight từ hành vi của họ. Hãy chọn chế độ phù hợp với bạn hoặc dùng cả hai để có thêm nhiều kết luận và ý tưởng hơn. ## Cách Adapty xây dựng cohort \{#how-adapty-builds-cohorts\} Hãy xem ví dụ về cohort theo số lần gia hạn để hiểu cách bảng được hình thành. Để xây dựng cohort, chúng tôi sử dụng hai chỉ số: lượt cài đặt ứng dụng và giao dịch (mua hàng). Mỗi hàng trong cohort đại diện cho một khoảng thời gian cụ thể: từ một ngày đến một năm. Mỗi hàng bắt đầu bằng số người dùng đã cài đặt ứng dụng trong khoảng thời gian đó và đã kích hoạt gói đăng ký hoặc mua sản phẩm có quyền truy cập trọn đời/sản phẩm mua một lần. Mỗi cột tiếp theo trong hàng hiển thị số lượng người dùng đã gia hạn gói đăng ký trong kỳ đó. M3 là tháng 3, có nghĩa là người đăng ký đã gia hạn liên tiếp 3 lần tính đến thời điểm này, W7 là tuần 7, và Y2 là năm 2. Đôi khi bạn có thể thấy P2 trong các cohort. P là viết tắt của Period (Kỳ) của gói đăng ký. Adapty hiển thị P thay cho W/M/Y khi có nhiều sản phẩm với các kỳ gia hạn khác nhau cùng xuất hiện trong một cohort. Chúng tôi sử dụng màu gradient để làm nổi bật sự khác biệt giữa các giá trị trong cohort. Các số lớn hơn sẽ có màu đậm hơn. Trong hình bên dưới, bạn có thể thấy một cohort điển hình. 1. Cohort này chỉ hiển thị dữ liệu cho các sản phẩm theo tuần (ký hiệu #1). 2. Cohort không loại trừ doanh thu và hiển thị doanh thu dưới dạng giá trị tuyệt đối (ký hiệu #2). 3. Khoảng thời gian đang xem xét là 6 tháng gần nhất, và độ dài cohort là 1 tháng (ký hiệu #3). 4. Hàng **Total** (ký hiệu #4) hiển thị giá trị tích lũy cho từng kỳ. $442K trong ô đầu tiên của hàng **Total** tích lũy doanh thu kỳ đầu tiên (kích hoạt gói đăng ký) từ tất cả các tháng (tháng 11, tháng 12, v.v.) cho đến cuối khung thời gian. Ô Total hiển thị số lượng khách hàng đã cài đặt ứng dụng trong toàn bộ khoảng thời gian. 5. Cột đầu tiên của hàng tháng 11 năm 2023 (ký hiệu #5) hiển thị doanh thu kỳ đầu tiên (kích hoạt gói đăng ký) là $37,7K từ những khách hàng đã cài đặt ứng dụng vào tháng 11 năm 2023. Số lượng khách hàng đã cài đặt ứng dụng trong tháng 11 năm 2023 là 95.129, được hiển thị ở cột tiêu đề. Cột thứ hai của hàng tháng 11 năm 2023 hiển thị doanh thu tuần 2 (các gói đăng ký đã gia hạn đến tuần thứ 2) là $8,77K từ những khách hàng đã cài đặt ứng dụng vào tháng 11 năm 2023. 6. Trên bảng, bạn có thể thấy Total revenue, ARPU, ARPPU và ARPAS (ký hiệu #6). Bạn có thể đọc thêm về các chỉ số này ở phần sau trong bài viết này. 7. Bạn có thể cấu hình các cột ở phần bên phải của bảng bằng cách sử dụng trường dropdown **Columns** (ký hiệu #7). 8. Phía trên bảng ở góc phải (ký hiệu #8), còn có một trường dropdown để tính phí hoa hồng của cửa hàng và tính thuế cho các phân tích cohort cụ thể. Bạn có thể tìm hiểu cách Adapty tính phí hoa hồng cửa hàng và thuế tại [bài viết này](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue). Sau khi chọn tùy chọn tương ứng từ dropdown, dữ liệu doanh thu sẽ được tính lại dựa trên lựa chọn đó. 9. Ở phía bên phải của bảng, bạn có thể thấy doanh thu dự đoán (Predicted Revenue) và giá trị trọn đời dự đoán (Predicted LTV) (ký hiệu #9). Trường **Predicted Revenue** ước tính tổng doanh thu được tạo ra bởi một cohort người đăng ký trong một khung thời gian cụ thể, trong khi trường **Predicted LTV** đại diện cho giá trị dự kiến của mỗi người dùng trong cohort. Bạn có thể di chuột vào bất kỳ ô nào trong cohort để xem các chỉ số chi tiết cho khoảng thời gian đó. Các ô có đường gạch chéo ở nền là những khoảng thời gian chưa kết thúc, vì vậy giá trị trong đó có thể còn tăng thêm. ## Độ dài cohort và khung thời gian \{#cohort-length-and-time-frame\} Hai cài đặt thời gian kiểm soát những gì bảng hiển thị: - **Time frame** — phạm vi ngày. Đặt trong lịch phía trên bảng. - **Cohort length** — kích thước mỗi hàng: ngày, tuần, tháng, quý, hoặc năm. Với độ dài theo tháng, mỗi hàng bao gồm một tháng cài đặt. Hai cài đặt này hoạt động độc lập. Ví dụ: time frame 6 tháng cộng với cohort length theo tháng cho bạn một bảng có 6 hàng. Time frame 1 năm cộng với cohort length theo tuần cho bạn 52 hàng. ## Bộ lọc, chỉ số, phân khúc cohort và xuất CSV \{#filters-metrics-cohort-segments-and-export-in-csv\} :::link Bài viết chính: [Điều khiển Analytics](controls-filters-grouping-compare-proceeds) ::: Theo mặc định, Adapty xây dựng các cohort dựa trên dữ liệu từ tất cả các lần mua. Bạn có thể lọc theo thời hạn sản phẩm, sản phẩm cụ thể, quốc gia, cửa hàng, paywall, phân khúc và dữ liệu attribution. Ở bên phải bảng điều khiển, có một nút để xuất dữ liệu cohort ra CSV. Sau đó bạn có thể mở file trong Excel, Google Sheets, hoặc import vào hệ thống phân tích của riêng mình. Có 6 chỉ số có thể hiển thị trong cohort: Subscriptions, Payers, Revenue, ARPU, ARPPU và ARPAS. Bạn có thể hiển thị chúng dưới dạng giá trị tuyệt đối hoặc dưới dạng phần trăm thay đổi so với thời điểm bắt đầu cohort. ## Gói đăng ký, người thanh toán, tổng doanh thu, ARPU, ARPPU và ARPAS \{#subscriptions-payers-total-revenue-arpu-arppu-and-arpas\} **Subscriptions** là tổng số gói đăng ký đang hoạt động, sản phẩm mua trọn đời và sản phẩm mua một lần mà một cohort thực hiện trong khoảng thời gian được chọn. Theo dõi chỉ số này giúp bạn hiểu hành vi của khách hàng cũng như hiệu quả của các sản phẩm đang cung cấp. Từ đó, bạn có thể tinh chỉnh chiến lược sản phẩm, điều chỉnh hoạt động marketing và tối ưu hóa nguồn doanh thu. **Payers** là tổng số người dùng đã thực hiện mua hàng trong một cohort. Chỉ số này giúp bạn hiểu có bao nhiêu người dùng duy nhất đóng góp vào doanh thu của bạn. Đối với các ứng dụng có lượng lớn in-app purchase không phải gói đăng ký, chỉ số này có thể làm nổi bật phạm vi tiếp cận thực sự của các sản phẩm, cho thấy liệu một lượng lớn người dùng đang mua hàng hay doanh thu được thúc đẩy bởi một nhóm nhỏ người mua lặp lại. Hiểu được số lượng payers giúp đánh giá mức độ gắn kết của khách hàng, lên kế hoạch marketing có mục tiêu và tối ưu hóa chiến lược doanh thu. **Tổng doanh thu** được tích lũy cho một cohort trong khoảng thời gian đã chọn (25/11/2022 — 24/05/2023). Chỉ số này giúp bạn hiểu được tổng số tiền thu được từ người dùng trong một cohort cụ thể và tính toán ROAS. Ví dụ: nếu chi tiêu quảng cáo cho tháng 9 năm 2022 là $10.000 và tổng doanh thu từ cohort tháng 9 năm 2022 là $30.000, thì ROAS = 3:1. **ARPU** là doanh thu trung bình trên mỗi người dùng. Được tính bằng tổng doanh thu / số người dùng duy nhất. $60.000 doanh thu / 5.000 người dùng = $12 ARPU. Chỉ số này hữu ích khi so sánh với chi phí cài đặt (CPI) để đánh giá hiệu quả các chiến dịch marketing của bạn. **ARPPU** là doanh thu trung bình trên mỗi người dùng trả phí. Được tính bằng tổng doanh thu / số người dùng trả phí duy nhất. $60.000 doanh thu / 1.000 người dùng trả phí = $60 ARPPU. Chỉ số này giúp bạn hiểu trung bình mỗi khách hàng trả phí mang lại bao nhiêu doanh thu. **ARPAS** là doanh thu trung bình trên mỗi người dùng đang hoạt động (active subscriber). Chỉ số này được tính bằng tổng doanh thu / số lượng người dùng đang hoạt động. Người dùng ở đây là những người đã kích hoạt thời gian dùng thử hoặc gói đăng ký. $60.000 doanh thu / 1.500 người dùng = $40 ARPAS. ## Phí hoa hồng và thuế \{#commission-fees-and-taxes\} Một khía cạnh quan trọng trong việc tính doanh thu của cohort là việc tính phí hoa hồng cửa hàng và thuế (có thể thay đổi tùy theo quốc gia tài khoản cửa hàng của người dùng). Adapty hiện hỗ trợ tính phí hoa hồng và thuế cho cả App Store và Play Store trong phân tích cohort. Để biết thêm chi tiết về cách Adapty tính thuế và hoa hồng trong analytics, hãy tham khảo [tài liệu](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue) của chúng tôi. ## Doanh thu vs Tiền thu được \{#revenue-vs-proceeds\} Cả Doanh thu (Revenue) và Tiền thu được (Proceeds) đều là các chỉ số tiền tệ. Bạn có thể hiểu Doanh thu là doanh thu gộp và Tiền thu được là doanh thu ròng. Doanh thu không tính đến phí của App Store / Play Store, trong khi Tiền thu được thì có. Do đó, Tiền thu được luôn thấp hơn Doanh thu. Mức hoa hồng thực tế bị khấu trừ thay đổi tùy theo nhiều yếu tố, bao gồm điều kiện tham gia các chương trình như [Chương trình Doanh nghiệp Nhỏ](app-store-small-business-program) (15%), mức giảm cho các gói đăng ký dài hạn (15% sau một năm gia hạn), mức phí theo từng quốc gia, và mức phí tiêu chuẩn (lên đến 30%). Adapty tự động xác định mức phí hoa hồng áp dụng cho mọi giao dịch của khách hàng và tính toán Doanh thu dựa trên đó. Để biết thêm thông tin về cách xác định mức phí hoa hồng, xem tài liệu [Phí hoa hồng cửa hàng và thuế](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue). ## Xử lý hoàn tiền \{#refund-handling\} Hai quy tắc sau áp dụng chung cho mọi chỉ số cohort: - Hoàn tiền được tính theo ngày phát sinh, không phải ngày mua ban đầu. Hoàn tiền chỉ ảnh hưởng đến một cohort khi ngày hoàn tiền đó nằm trong khoảng thời gian đã chọn. - Hoàn tiền không bao giờ chuyển người dùng ra khỏi cohort và không thay đổi số lượt cài đặt. Thành phần cohort được xác định cố định ngay khi người dùng cài đặt ứng dụng. Ngoài các quy tắc trên, tác động của hoàn tiền còn phụ thuộc vào chỉ số và [chế độ xem](#cohorts-by-renewals-or-by-days). Hãy đọc theo cột tương ứng với chế độ bạn đang dùng. | Chỉ số | Theo lần gia hạn | Theo ngày | | --- | --- | --- | | Lượt cài đặt (kích thước cohort) | Không bị ảnh hưởng. Người dùng vẫn ở trong cohort của họ. | Giống như "Theo lần gia hạn". | | Gói đăng ký | Các gói đăng ký đã hoàn tiền vẫn được tính. | Hoàn tiền sẽ loại gói đăng ký đó khỏi số đếm. | | Người thanh toán | Người dùng đã thanh toán nhưng được hoàn tiền vẫn được tính. | Hoàn tiền sẽ loại người dùng đó khỏi số đếm, kể cả khi họ đã thực hiện các thanh toán thành công khác. | | Doanh thu | Số tiền hoàn trả được trừ khỏi cột kỳ gia hạn nơi khoản thanh toán ban đầu được ghi nhận. | Số tiền hoàn trả được trừ từ ngày hoàn tiền trở đi. | | ARPU | Doanh thu / lượt cài đặt. Hoàn tiền làm giảm doanh thu; số lượt cài đặt không bao giờ thay đổi. | Giống như "Theo lần gia hạn". | | ARPPU | Doanh thu / người dùng đã thanh toán. Hoàn tiền có thể làm giảm cả doanh thu lẫn số người dùng đã thanh toán, do đó ARPPU có thể biến động mạnh hơn so với doanh thu đơn thuần. | Giống như "Theo lần gia hạn". | | ARPAS | Doanh thu / người đăng ký đang hoạt động. Hoàn tiền làm giảm doanh thu; số lượng người đăng ký không thay đổi. | Giống như "Theo lần gia hạn". | | Retention | Không bị ảnh hưởng. Chỉ tính các sự kiện dùng thử và mua hàng, không tính hoàn tiền. | Giống như "Theo lần gia hạn". | Việc hoàn tiền sẽ trừ số tiền hoàn lại khỏi doanh thu bất kể chế độ hạch toán doanh thu đang được áp dụng là gì. ### Tỷ lệ chuyển đổi và ARPPU \{#conversion-rate-and-arppu\} Hoàn tiền không ảnh hưởng đến tỷ lệ chuyển đổi tính theo lượt cài đặt, vì số lượt cài đặt không bao giờ thay đổi. Tỷ lệ chuyển đổi tính theo người dùng trả tiền thì khác. Hoàn tiền làm giảm tỷ lệ này trong chế độ xem **by days**, nhưng không ảnh hưởng trong chế độ xem **by renewals**. Trong chế độ xem **theo số lần gia hạn**, các cột Revenue và Payers mỗi cột chỉ hiển thị một khoảng thời gian riêng lẻ. Cột ARPPU thì không. Mỗi ô ARPPU cộng dồn tất cả từ khoảng thời gian đầu tiên của cohort cho đến khoảng thời gian của cột đó. Vì vậy, nó luôn bao gồm nhiều khoảng thời gian cùng lúc, và không tính những người dùng đã được hoàn tiền. Do đó, việc chia Revenue của một khoảng thời gian đơn lẻ cho số người dùng trả phí của khoảng thời gian đó sẽ không cho ra kết quả ARPPU được hiển thị. **Ví dụ.** Một người dùng cài đặt vào tháng Hai và mua một gói đăng ký, sau đó được hoàn tiền toàn bộ vào tháng Tư. Xem cohort tháng Hai **theo ngày**: - Khung thời gian tháng 2–3, trước khi khoản hoàn tiền nằm trong cửa sổ: người dùng được tính là 1 người dùng trả phí, và doanh thu của họ được tính đầy đủ. - Khung thời gian tháng 2–6, sau khi khoản hoàn tiền nằm trong cửa sổ: người dùng được tính là 0 người dùng trả phí, và doanh thu giảm về 0. Ở cả hai khung thời gian, số lượt cài đặt tháng 2 và Retention vẫn giữ nguyên. [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds) so sánh các quy tắc tương tự giữa MRR, các biểu đồ doanh thu và xuất dữ liệu. ## Dự đoán: Doanh thu và LTV \{#prediction-revenue-and-ltv\} **Doanh thu dự đoán** là tổng doanh thu ước tính mà một cohort người dùng trả phí sẽ tạo ra trong khoảng thời gian đã chọn kể từ khi cohort được tạo. Chỉ số này được tính bằng cách nhân LTV dự đoán của cohort với số lượng người dùng trả phí dự đoán trong cohort đó. Ví dụ: nếu LTV dự đoán là $50 và cohort có 100 người dùng trả phí, thì Doanh thu dự đoán sẽ là $5.000. **Predicted LTV** là giá trị vòng đời ước tính trên mỗi người đăng ký có trả phí, thể hiện doanh thu trung bình mà mỗi người đăng ký có trả phí được kỳ vọng tạo ra trong khoảng thời gian đã chọn sau khi cohort được tạo. Các dự đoán này dựa trên các mô hình giữ chân cohort lịch sử, sử dụng dữ liệu riêng của ứng dụng khi có đủ lịch sử và dữ liệu trung bình từ nhiều ứng dụng khác trong trường hợp ngược lại. Để biết thêm chi tiết về mô hình dự đoán của Adapty, vui lòng tham khảo [Tài liệu về Dự đoán](predicted-ltv-and-revenue). Adapty cohorts cung cấp thông tin chi tiết về hành vi người dùng và hiệu quả tài chính trong ứng dụng của bạn. Bằng cách phân tích các cohort dựa trên lượt gia hạn hoặc số ngày, bạn có thể xác định thời điểm các cohort bắt đầu có lợi nhuận, theo dõi doanh thu, tính toán doanh thu trung bình trên mỗi người dùng, và hiểu rõ thời gian cần thiết để thu hồi chi phí quảng cáo. Với các bộ lọc, chỉ số và tùy chọn xuất dữ liệu linh hoạt, Adapty giúp bạn đưa ra quyết định dựa trên dữ liệu và tối ưu hóa chiến lược thu hút người dùng cũng như kiếm tiền từ ứng dụng để đạt hiệu quả tối đa. --- # File: analytics-funnels --- --- title: "Phân tích funnel" description: "Hiểu cách funnel analytics hoạt động trong Adapty để theo dõi hành vi người dùng và cải thiện tỷ lệ chuyển đổi." --- Funnel trong Adapty được thiết kế để giúp bạn trả lời những câu hỏi như: 1. Bao nhiêu phần trăm lượt cài đặt chuyển thành khách hàng trả tiền? 2. Bao nhiêu người dùng thử sản phẩm trở thành người dùng trung thành? 3. Bước nào có tỷ lệ thoát cao và cần được chú ý hơn? 4. Tại sao khách hàng ngừng thanh toán? Với biểu đồ funnel, bạn có thể tìm thêm nhiều thông tin về hành vi người dùng bằng cách thiết lập các bộ lọc và nhóm. Funnel hoạt động với dữ liệu thu thập qua SDK và thông báo từ cửa hàng, không yêu cầu bất kỳ cấu hình bổ sung nào từ phía bạn. :::note Funnel phản ánh dữ liệu cài đặt theo định nghĩa lượt cài đặt của bạn trong [App Settings](general#4-installs-definition-for-analytics). ::: <img src="/assets/shared/img/funnels-tab.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Đọc biểu đồ funnel từng bước \{#funnel-chart-step-by-step\} Hãy cùng đi qua từng thành phần của funnel để hiểu cách đọc hành trình người dùng trên biểu đồ. <img src="/assets/shared/img/ed5bf5d-CleanShot_2022-06-23_at_09.36.49.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Lượt cài đặt \{#installs\} Cột thứ 1 (1) là số lượt cài đặt. Nó được hiển thị dưới dạng giá trị tuyệt đối (2) của tổng số lần cài đặt (không phải người dùng duy nhất) và cũng là 100% — số đầu vào lớn nhất để tính toán tỷ lệ chuyển đổi tương đối cho các bước tiếp theo. Nếu người dùng xóa ứng dụng rồi cài lại, sẽ được tính là hai lượt cài đặt riêng biệt. Vùng màu xám bên cạnh thể hiện các thông số chuyển tiếp giữa các bước. Tỷ lệ chuyển đổi sang bước tiếp theo (Paywall hiển thị) được hiển thị trên một nhãn (3). Tỷ lệ thoát và giá trị tuyệt đối của lượt rời bỏ được hiển thị bên dưới (4). <img src="/assets/shared/img/00416f9-CleanShot_2022-06-23_at_14.02.06.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Paywall hiển thị \{#paywall-displayed\} Cột thứ 2 (5) hiển thị số người dùng đã xem paywall ít nhất một lần (6). Chỉ tính những người dùng có lượt cài đặt trong khoảng thời gian được chọn. Nếu người dùng xem paywall trong khoảng thời gian đã chọn nhưng ngày cài đặt nằm ngoài phạm vi thì lượt xem đó không được tính. Cũng có tỷ lệ phần trăm của các lượt xem đó so với bước thứ 1 (7). Bạn có thể nhận thấy phần trăm này bằng với nhãn màu xám (3) của bước 1. Sự bằng nhau này chỉ xảy ra ở hai bước đầu tiên. Chúng tôi thu thập dữ liệu cho bước này từ tất cả các paywall sử dụng phương thức `logShowFlow()` (iOS SDK v4+) / `logShowPaywall()`. Vì vậy, hãy đảm bảo gửi mọi lượt xem paywall đến Adapty bằng phương thức này như mô tả trong [tài liệu](present-remote-config-paywalls#track-paywall-view-events). Vùng màu xám bên cạnh cột thứ 2 thể hiện quá trình chuyển tiếp. Tỷ lệ chuyển đổi sang bước tiếp theo (Dùng thử) được hiển thị trên nhãn (8). Tỷ lệ thoát và giá trị tuyệt đối của khách hàng rời bỏ sau paywall được hiển thị bên dưới (9). <img src="/assets/shared/img/fb11650-CleanShot_2022-06-23_at_15.54.32.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Dùng thử \{#trials\} Cột thứ 3 (10) hiển thị số lượt dùng thử được kích hoạt trên các paywall bởi những khách hàng đã cài đặt ứng dụng trong khoảng thời gian đã chọn (11). Nếu bộ lọc được đặt cho sản phẩm không có dùng thử, giá trị này bằng 0 và cột sẽ trống. Cũng xem tỷ lệ phần trăm của lượt dùng thử tính từ bước thứ 1, thể hiện tỷ lệ chuyển đổi từ lượt cài đặt sang dùng thử (12). Bạn có thể nhận thấy phần trăm này không bằng với nhãn màu xám (8) của bước chuyển đổi trước. Đó là vì chúng tôi so sánh giá trị hiện tại với bước thứ 1 ở đầu biểu đồ và với bước trước đó trên các nhãn màu xám. Vì vậy, vùng màu xám bên cạnh cột thứ 3 hiển thị tỷ lệ chuyển đổi sang bước tiếp theo (Đã trả tiền) được thể hiện trên nhãn (13). Tỷ lệ thoát và giá trị tuyệt đối của khách hàng rời bỏ trong thời gian dùng thử được hiển thị bên dưới (14). <img src="/assets/shared/img/7b88909-CleanShot_2022-06-23_at_15.54.32_-_2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Gói đăng ký và gia hạn \{#subscriptions-and-renewals\} Cột thứ 4 hiển thị số gói đăng ký đã được kích hoạt (15). Đối với các sản phẩm không có dùng thử, con số này bao gồm các gói đăng ký trực tiếp từ paywall. Đối với các sản phẩm có dùng thử, nó chứa số lượt dùng thử chuyển đổi thành gói đăng ký trả tiền. Nếu bạn có cả hai loại sản phẩm, có và không có dùng thử, đây sẽ là tổng của cả hai. Tỷ lệ phần trăm ở trên cùng cho thấy tỷ lệ chuyển đổi từ lượt cài đặt (16). Tỷ lệ phần trăm trên nhãn màu xám cho thấy tỷ lệ chuyển đổi sang bước tiếp theo (gia hạn sang kỳ thứ 2) (17). Tỷ lệ phần trăm và giá trị tuyệt đối của lượt thoát trước khi gia hạn sang kỳ thứ 2 được hiển thị bên dưới tỷ lệ chuyển đổi (18). <img src="/assets/shared/img/d13bf9b-CleanShot_2022-06-23_at_15.54.32-3.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bước này bắt đầu một chuỗi các bước có cấu trúc tương tự. Sau lần gia hạn thứ 2 là lần thứ 3, rồi thứ 4, v.v. Nếu có đủ dữ liệu trong lịch sử ứng dụng của bạn, bạn có thể xem hàng chục kỳ bằng cách cuộn ngang. Logic cho các bước này vẫn giống nhau: - tỷ lệ phần trăm từ lượt cài đặt ở trên cùng, - tỷ lệ phần trăm từ bước trước ở dưới cùng, - số lượng gia hạn tuyệt đối ở trên cùng, - số lượng thoát tuyệt đối ở dưới cùng, - hover để xem popup lý do thoát. ### Lý do thoát \{#churn-reasons\} Adapty cung cấp thống kê chi tiết về *tỷ lệ thoát* từ giai đoạn Dùng thử trở đi. Mỗi người dùng bước vào một giai đoạn nhưng không tiến đến giai đoạn tiếp theo đều được tính là một trường hợp thoát. * Nếu một sự kiện cụ thể (ví dụ: hết hạn dùng thử hoặc vấn đề thanh toán) là nguyên nhân khiến không có chuyển đổi, Adapty sẽ hiển thị lý do đó. * Trạng thái **unknown** là trạng thái tạm thời. Nó cho biết người dùng chưa gặp phải sự kiện cho phép họ tiến sang giai đoạn tiếp theo. Ở giai đoạn Dùng thử, điều này thường có nghĩa là thời gian dùng thử chưa kết thúc. Điều này thường xảy ra khi xem Funnel cho các khoảng thời gian ngắn hoặc trong một ngày, vì dùng thử cần thời gian để xử lý. Adapty sẽ cập nhật thông tin khi người dùng chuyển đổi hoặc hủy dùng thử. <img src="/assets/shared/img/churn-reasons.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Chế độ xem bảng, bộ lọc và xuất CSV \{#table-view-filters-and-csv-export\} Biểu đồ funnel được bổ sung thêm dữ liệu dạng bảng để cung cấp tài liệu tiện lợi cho công việc với các con số. <img src="/assets/shared/img/4787aff-CleanShot_2022-06-23_at_21.01.44.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bảng này lặp lại cách tiếp cận của funnel với một số điều chỉnh. Có các cột hiển thị dữ liệu cho tất cả các bước ngoại trừ bước gói đăng ký trả tiền đầu tiên. Thay vào đó, có hai cột riêng biệt: Cài đặt -> Trả tiền và Dùng thử -> Trả tiền. Chúng hiển thị điểm cốt lõi của quá trình chuyển đổi khi người dùng miễn phí trở thành người dùng trả tiền. Có thể trông như thể có sự phân chia theo loại sản phẩm: cột Cài đặt -> Trả tiền chỉ hiển thị các sản phẩm không có dùng thử trong khi cột Dùng thử -> Trả tiền chứa các sản phẩm có dùng thử. Nhưng đó không hoàn toàn là cách hoạt động. Vì chúng tôi cũng xem xét những người dùng đã hết hạn dùng thử và mua sản phẩm có dùng thử như thể nó không có dùng thử. <img src="/assets/shared/img/a9bcbc7-CleanShot_2022-06-23_at_21.29.12.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đi sâu hơn vào các con số, bạn sẽ thấy các công cụ lọc mạnh mẽ để đưa ra các giả thuyết mới. Thoải mái đặt điều kiện theo nhiều chiều khác nhau. Thu thập thông tin chính xác dựa trên dữ liệu. Thử nghiệm với: 1. Loại sản phẩm — kinh tế, thời hạn, v.v. 2. Khoảng thời gian. 3. Phân khúc theo quốc gia. 4. Attribution lưu lượng truy cập. 5. Cửa hàng. Chọn Số tuyệt đối #, Tỷ lệ % hoặc cả hai để chỉ xem dữ liệu cần thiết. <img src="/assets/shared/img/1475e42-CleanShot_2022-06-23_at_21.50.33_-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Cuối cùng, ở bên phải thanh điều khiển, có nút để xuất dữ liệu funnel sang CSV. Bạn có thể mở nó trong Excel, Google Sheets hoặc nhập vào hệ thống phân tích của riêng bạn. :::important Hãy thông báo cho Adapty nếu ứng dụng của bạn tham gia chương trình hoa hồng giảm. Để đảm bảo tính toán chính xác, hãy chỉ định trạng thái [Small Business Program](app-store-small-business-program) và [Reduced Service Fee program](google-reduced-service-fee) trong [cài đặt ứng dụng](general) của bạn. ::: <img src="/assets/shared/img/ff23846-CleanShot_2022-06-23_at_22.15.49.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: analytics-retention --- --- title: "Phân tích retention" description: "Hiểu phân tích retention người dùng và tối ưu hóa chiến lược gói đăng ký của bạn." --- Các biểu đồ retention có thể giúp bạn trả lời những câu hỏi sau: 1. Ứng dụng của bạn giữ chân người dùng như thế nào qua từng giai đoạn? 2. Sản phẩm nào hấp dẫn hơn và giữ chân người dùng tốt hơn? 3. Nhóm người dùng nào trung thành hơn? 4. Mức retention nào có thể dùng làm chuẩn tham chiếu để tăng trưởng? 5. Và tất nhiên, bạn có thể tiết kiệm tiền bằng cách đầu tư vào đối tượng đã thu hút thay vì tìm kiếm người dùng mới như thế nào? Bạn sẽ tìm thấy những thông tin hữu ích về hành vi người dùng khi thiết lập các bộ lọc và nhóm. Retention được tính toán dựa trên dữ liệu chúng tôi thu thập qua SDK và thông báo từ cửa hàng, không cần bất kỳ cấu hình bổ sung nào từ phía bạn. ### Chúng tôi tính retention như thế nào? \{#how-do-we-calculate-retention\} Khi xem biểu đồ retention, bạn thấy số lượng người dùng thay đổi theo từng bước họ thực hiện: dùng thử (nếu checkbox "show trials" được chọn), lần thanh toán thứ 1, thứ 2, v.v. Hãy cùng làm rõ người dùng nào được tính khi bạn chọn khoảng thời gian cho biểu đồ retention. Ví dụ: bạn chọn 3 tháng gần nhất trong lịch và checkbox "show trials" không được chọn. Điều này có nghĩa là chúng tôi chỉ tính những người có gói đăng ký đầu tiên trong 3 tháng gần nhất. Nếu checkbox "show trials" được chọn và 3 tháng gần nhất được chọn trong lịch, chúng tôi tính tất cả những người đã dùng thử trong 3 tháng gần nhất. Đối với những người đăng ký này, chúng tôi hiển thị retention tuyệt đối cho bước thứ N là số người đã thực hiện lần thanh toán thứ N. Và chúng tôi tính giá trị retention tương đối cho bước thứ N là tỷ lệ giữa số lượng tuyệt đối của lần thanh toán thứ N so với tổng số gói đăng ký (hoặc lượt dùng thử) trong khoảng thời gian đã chọn. :::info Retention thay đổi hồi tố Bất kể bạn xem biểu đồ vào thời điểm nào, con số cơ sở (100%) vẫn giữ nguyên cho khoảng thời gian đã chọn. Trong khi đó, retention sang kỳ tiếp theo có thể tăng theo thời gian. Ví dụ: với gói đăng ký hàng tháng, nếu có 20 lần mua đầu tiên trong khoảng thời gian từ ngày 1 đến ngày 31 tháng 12, thì retention sang kỳ thứ hai dự kiến sẽ tăng trong suốt tháng 1 (và có thể cả sau đó) khi người dùng bước vào kỳ đăng ký tiếp theo đúng hạn hoặc muộn hơn vì một số lý do (ví dụ: thời gian ân hạn). ::: ### Xử lý hoàn tiền \{#refund-handling\} Các khoản hoàn tiền **không** bị loại trừ khỏi retention. Người dùng được hoàn tiền vẫn được tính trên đường cong retention, điều này có thể khiến Retention trông cao hơn so với [Active subscriptions](active-subscriptions) hoặc [Revenue](revenue) cho cùng một cohort. Để so sánh đầy đủ giữa các chỉ số, xem [Cách các chỉ số xử lý hoàn tiền](refund-events#how-metrics-handle-refunds). ### Cơ hội từ retention \{#retention-opportunities\} Hãy xem cách khai thác tối đa tính năng retention của Adapty. Không chỉ đơn thuần là đam mê với con số mà còn muốn thấy giá trị kinh doanh thực sự sau khi triển khai kết quả phân tích — hãy nghĩ về mục đích trước. Khi khám phá sâu hơn các tính năng của biểu đồ, sẽ rất hay nếu làm rõ tác động mà dữ liệu này có thể mang lại. Vì vậy, hãy cùng nhìn vào TẠI SAO và NHƯ THẾ NÀO. 1 - làm việc với đối tượng. Trước hết, retention liên quan đến đối tượng mục tiêu, sở thích của họ, và liệu sản phẩm của bạn có đáp ứng được kỳ vọng của họ trong suốt vòng đời sử dụng hay không. Nếu bạn muốn đo lường mối quan hệ cốt lõi của doanh nghiệp tạo ra doanh thu — retention chính là công cụ dành cho bạn. Việc đo lường này mang lại lợi ích vì thường bán hàng cho khách hiện tại rẻ hơn bán cho người lạ. Chi phí thấp hơn vì hai lý do: ít nỗ lực bán hàng hơn và giá trị đơn hàng trung bình cao hơn. Vì vậy, đầu tư vào sự trung thành của người đăng ký khi retention giảm là một ý tưởng hay. 2 - làm việc với sản phẩm. Lý do thứ hai của TẠI SAO là biểu đồ retention cho thấy vòng đời sử dụng thực tế của sản phẩm và cho phép bạn dự báo dài hạn. Và nếu bạn muốn cải thiện, hãy điều chỉnh quy trình phân phối sản phẩm để thay đổi vòng đời của nó, rồi dự báo lại để tiến gần hơn đến mục tiêu kinh doanh. Những cập nhật như vậy có thể là một phần của tầm nhìn chiến lược kết hợp với quy trình dự báo thường xuyên. Và đúng vậy, quá trình này không bao giờ kết thúc vì chúng ta đều phải chạy nhanh chỉ để đứng yên trong môi trường không ngừng thay đổi. 3 - làm việc với thị trường. Di chuyển nhanh hơn các đối thủ chính là tốt, nhưng đôi khi thoát ra khỏi cuộc đua thông thường lại mang lại nhiều lợi ích hơn. Khi bạn phân tích hành vi người dùng ở các quốc gia và cửa hàng khác nhau, một số đặc thù địa phương có thể mở ra những insights nổi bật và cơ hội mới cho doanh nghiệp. Bối cảnh văn hóa và thị trường có thể được phân tích từ góc độ retention để sau đó sử dụng cho phân khúc và phát triển tiếp theo. Ví dụ, bạn có thể tìm thấy vùng nước xanh ở một số khu vực và phát triển nhanh hơn ở đó. Tất nhiên, việc sử dụng dữ liệu retention không chỉ giới hạn ở cách diễn giải cơ bản này, nhưng đây có thể là điểm khởi đầu tốt nếu bạn muốn nhanh chóng tạo ra giá trị thực. ### Đường cong, chế độ xem bảng, bộ lọc và xuất CSV \{#curves-table-view-filters-and-csv-export\} Giờ khi chúng ta đã hiểu về mục đích retention và các cách diễn giải cơ bản, hãy xem qua các công cụ giúp mọi thứ trở nên tiện lợi. Cốt lõi của tính năng retention trong Adapty là biểu đồ. Nó cho thấy mức retention phụ thuộc vào các bước trong vòng đời của khách hàng như thế nào. Các bước được hiển thị trên trục ngang: Trial, Paid (gói đăng ký thứ 1), P2 (gói đăng ký thứ 2), P3, P4, v.v. Lưu ý rằng trục bắt đầu bằng bước Trial chỉ khi checkbox "Show trials" được chọn. Đối với tính toán dữ liệu, checkbox này hoạt động như sau. Khi "Show trials" được chọn và trục bắt đầu từ bước Trial, bạn chỉ thấy các tình huống có dùng thử — không có giao dịch trực tiếp từ lần cài đặt nào được hiển thị và bước Paid chỉ chứa các giao dịch đến từ dùng thử. Khi "Show trials" không được chọn và trục bắt đầu từ bước Paid, bước đầu tiên này chứa tất cả các giao dịch đầu tiên bao gồm cả từ dùng thử lẫn trực tiếp từ lần cài đặt. Khi bạn di chuột qua biểu đồ, một cửa sổ bật lên hiển thị tóm tắt dữ liệu. Và nếu bạn di chuột qua một cột trong bảng bên dưới, bạn cũng thấy cửa sổ bật lên tóm tắt với dữ liệu liên quan trên biểu đồ. Bảng chứa cùng nhóm và bộ lọc được chọn cho biểu đồ. Hãy thoải mái kết hợp các bộ lọc và nhóm để phân tích nâng cao. Thu thập insights thực sự dựa trên dữ liệu. Tùy chỉnh: 1. Loại sản phẩm. 2. Thời hạn. 3. Khoảng thời gian. 4. Quốc gia. 5. Attribution lưu lượng truy cập. 6. Cửa hàng. Sử dụng tùy chọn #Absolute và %Relative để xem dữ liệu cần thiết. Cuối cùng, ở phía bên phải của bảng điều khiển, có nút để xuất dữ liệu funnel ra CSV. Sau đó bạn có thể mở trong Excel, Google Sheets, hoặc import vào hệ thống phân tích riêng của mình để tiếp tục phân tích và dự báo trong môi trường bạn ưa thích. :::warning Hãy đảm bảo rằng bạn đã khai báo ứng dụng của mình tham gia Small Business Program trong [Adapty General Settings](https://app.adapty.io/settings/general). ::: --- # File: analytics-conversion --- --- title: "Phân tích chuyển đổi" description: "Đo lường tỷ lệ chuyển đổi gói đăng ký bằng các công cụ phân tích của Adapty." --- Trong khi funnel cho bạn cái nhìn tổng quan và retention tập trung vào sự trung thành của người dùng, phân tích chuyển đổi được thiết kế để giúp bạn đánh giá hiệu quả ở mọi bước quan trọng trong hành trình người dùng—theo thời gian. Chuyển đổi giúp trả lời các câu hỏi sau: 1. Tỷ lệ chuyển đổi của ứng dụng thay đổi như thế nào theo thời gian? Có xu hướng theo mùa nào không? 2. Chuyển đổi thay đổi như thế nào tại thời điểm diễn ra các hoạt động marketing hoặc một số tình huống mới khác? 3. Người dùng ở các khu vực khác nhau phản ứng như thế nào với các bản cập nhật ứng dụng của bạn? 4. Loại sản phẩm nào có tỷ lệ chuyển đổi tốt hơn theo thời gian? Chuyển đổi được thực hiện với dữ liệu chúng tôi thu thập qua Adapty SDK và thông báo từ cửa hàng, và không yêu cầu bất kỳ cấu hình bổ sung nào từ phía bạn. ## Điều khiển chính và biểu đồ \{#main-controls-and-charts\} Mặc dù doanh thu thường là chỉ số chính để đo lường thành công, nhưng đó chỉ là một phần của bức tranh lớn hơn. Hiểu cách doanh nghiệp của bạn hoạt động theo thời gian—trên các hành vi người dùng và giai đoạn vòng đời khác nhau—cũng quan trọng không kém. Đó là lúc phân tích chuyển đổi phát huy tác dụng. Bạn có thể tìm thấy thêm thông tin chi tiết về hành vi người dùng bằng cách đặt bộ lọc và nhóm. Để xác định và phân tích xu hướng, hãy theo dõi cách chuyển đổi của bạn thay đổi hàng ngày, hàng tháng hoặc hàng năm. Ở phía bên trái của biểu đồ, bạn sẽ thấy điều khiển các bước chuyển đổi. Điều này cho phép bạn chọn các chuyển đổi cụ thể cần theo dõi—chẳng hạn như Cài đặt → Dùng thử, Dùng thử → Trả phí, hoặc Trả phí → Gia hạn. Mỗi chỉ số chuyển đổi tuân theo logic sau: - Đặt **X** là số người dùng đã bước vào trạng thái ban đầu vào một ngày được chọn (ví dụ: lượt cài đặt). - Đặt **Y** là số người dùng trong nhóm đó cuối cùng đạt đến trạng thái mục tiêu (ví dụ: bắt đầu dùng thử). - Tỷ lệ chuyển đổi được tính như sau: **Chuyển đổi = (Y / X) × 100%** :::note Ngày hiển thị trên biểu đồ tương ứng với thời điểm người dùng bước vào trạng thái ban đầu (X)—thời điểm họ đủ điều kiện để chuyển đổi. ::: Vui lòng xem bên dưới để hiểu từng chuyển đổi, kèm theo ví dụ tham khảo. ### Cài đặt -> Trả phí \{#install---paid\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng đã cài đặt ứng dụng vào một ngày cụ thể và cuối cùng mua gói đăng ký đầu tiên của họ. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt cài đặt vào một ngày được chọn (giống nhau cho tất cả sản phẩm, vì không có sản phẩm nào được chọn tại thời điểm cài đặt). - **Y** = số người dùng trong nhóm đó cuối cùng mua gói đăng ký đầu tiên (dùng thử hoặc không dùng thử). **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt cài đặt. - Đến ngày 8 tháng 1, 20 người dùng trong nhóm đó đã đăng ký. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa từ nhóm cài đặt ngày 1 tháng 1 đã mua gói đăng ký. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã cài đặt ứng dụng vào ngày 1 tháng 1 cuối cùng đã chuyển đổi sang gói đăng ký trả phí, tính đến thời điểm hiện tại. </details> ### Cài đặt -> Dùng thử \{#install---trial\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng đã cài đặt ứng dụng vào một ngày cụ thể và cuối cùng bắt đầu dùng thử. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt cài đặt vào một ngày được chọn (giống nhau cho tất cả sản phẩm, vì không có sản phẩm nào được chọn tại thời điểm cài đặt). - **Y** = số người dùng trong nhóm đó cuối cùng đã kích hoạt dùng thử, vào bất kỳ thời điểm nào. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt cài đặt. - Đến ngày 8 tháng 1, 20 người dùng trong nhóm đó đã bắt đầu dùng thử. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa từ nhóm cài đặt ngày 1 tháng 1 đã bắt đầu dùng thử. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã cài đặt ứng dụng vào ngày 1 tháng 1 cuối cùng đã bắt đầu dùng thử, tính đến thời điểm hiện tại. </details> ### Xem paywall -> Dùng thử \{#paywall-view---trial\} Chỉ số này theo dõi số người dùng đã bắt đầu dùng thử sau khi xem paywall. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số người dùng đã xem paywall vào một ngày được chọn. - **Y** = số người dùng đã bắt đầu dùng thử vào bất kỳ thời điểm nào sau đó. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt xem paywall. - Đến ngày 8 tháng 1, 20 người dùng trong nhóm đó đã bắt đầu dùng thử. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa đã bắt đầu dùng thử. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này cho thấy 50% người dùng đã xem paywall vào ngày 1 tháng 1 đã bắt đầu dùng thử, tính đến thời điểm hiện tại. </details> ### Xem paywall -> Trả phí \{#paywall-view---paid\} Chỉ số này theo dõi số người dùng đã mua hàng sau khi xem paywall. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số người dùng đã xem paywall vào một ngày được chọn. - **Y** = số người dùng đã mua hàng vào bất kỳ thời điểm nào sau đó. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt xem paywall. - Đến ngày 8 tháng 1, 20 người dùng trong nhóm đó đã mua hàng. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa đã mua hàng. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này cho thấy 50% người dùng đã xem paywall vào ngày 1 tháng 1 đã mua hàng, tính đến thời điểm hiện tại. </details> ### Dùng thử -> Trả phí \{#trial---paid\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng đã bắt đầu dùng thử vào một ngày cụ thể và sau đó mua gói đăng ký đầu tiên của họ. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt bắt đầu dùng thử vào một ngày được chọn. - **Y** = số người dùng trong nhóm đó cuối cùng đã mua gói đăng ký sau khi dùng thử. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt bắt đầu dùng thử. - Đến ngày 8 tháng 1, 20 người dùng trong nhóm đó đã đăng ký. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa từ nhóm dùng thử ngày 1 tháng 1 đã đăng ký. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã bắt đầu dùng thử vào ngày 1 tháng 1 cuối cùng đã chuyển đổi sang gói đăng ký trả phí, tính đến thời điểm hiện tại. </details> ### Trả phí -> Kỳ thứ 2 \{#paid---2nd-period\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng đã gia hạn gói đăng ký sau lần thanh toán đầu tiên. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt đăng ký lần đầu vào một ngày được chọn. - **Y** = số người dùng đã gia hạn sang kỳ thứ hai, vào bất kỳ thời điểm nào sau đó (thường là sau một chu kỳ đăng ký; bao gồm cả gia hạn trong thời gian ân hạn). - **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt đăng ký lần đầu. - Đến ngày 8 tháng 1, 20 người trong nhóm đó đã gia hạn. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa từ nhóm đó đã gia hạn. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này cho thấy 50% người dùng đã thanh toán gói đăng ký lần đầu vào ngày 1 tháng 1 đã gia hạn sang kỳ thứ hai, tính đến thời điểm hiện tại. </details> ### Kỳ thứ 2 -> Kỳ thứ 3 \{#2nd-period---3rd-period\} Chỉ số này theo dõi số người dùng đã gia hạn lần nữa sau kỳ đăng ký thứ hai của họ. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt đăng ký kỳ thứ hai vào một ngày được chọn. - **Y** = số người dùng đã gia hạn sang kỳ thứ ba, vào bất kỳ thời điểm nào sau đó (thường là sau thêm một chu kỳ thanh toán; bao gồm cả gia hạn trong thời gian ân hạn). **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt đăng ký kỳ thứ hai. - Đến ngày 8 tháng 1, 20 người dùng trong nhóm đó đã gia hạn. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa đã gia hạn. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này cho thấy 50% người dùng đã bước vào kỳ đăng ký thứ hai vào ngày 1 tháng 1 đã gia hạn sang kỳ thứ ba, tính đến thời điểm hiện tại. </details> ### Kỳ thứ 3 -> Kỳ thứ 4 \{#3rd-period---4th-period\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng đã gia hạn sau kỳ đăng ký thứ ba của họ. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt đăng ký kỳ thứ ba vào một ngày được chọn. - **Y** = số người dùng đã gia hạn sang kỳ thứ tư vào bất kỳ thời điểm nào sau đó (thường là sau một chu kỳ thanh toán; bao gồm cả gia hạn trong thời gian ân hạn). **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt đăng ký kỳ thứ ba. - Đến ngày 8 tháng 1, 20 người dùng đã gia hạn. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa đã gia hạn. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã bước vào kỳ đăng ký thứ ba vào ngày 1 tháng 1 đã gia hạn sang kỳ thứ tư, tính đến thời điểm hiện tại. </details> ### Kỳ thứ 4 -> Kỳ thứ 5 \{#4th-period---5th-period\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng đã gia hạn sau kỳ đăng ký thứ tư của họ. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt đăng ký kỳ thứ tư vào một ngày được chọn. - **Y** = số người dùng đã gia hạn sang kỳ thứ năm vào bất kỳ thời điểm nào sau đó (thường là sau một chu kỳ thanh toán; bao gồm cả gia hạn trong thời gian ân hạn). **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt đăng ký kỳ thứ tư. - Đến ngày 8 tháng 1, 20 người dùng đã gia hạn. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2, 30 người dùng nữa đã gia hạn. - Vào ngày 1 tháng 2, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã bước vào kỳ đăng ký thứ tư vào ngày 1 tháng 1 đã gia hạn sang kỳ thứ năm, tính đến thời điểm hiện tại. </details> ### 6 tháng + \{#6-months-\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng vẫn duy trì đăng ký hơn 6 tháng kể từ lần đăng ký đầu tiên. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt đăng ký lần đầu vào một ngày được chọn. - **Y** = số người dùng trong nhóm đó đã gia hạn ít nhất một lần sau 6 tháng kể từ ngày đăng ký ban đầu. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, có 100 lượt đăng ký lần đầu. - Đến tuần đầu tháng 7, 20 người trong nhóm đó đã gia hạn (ví dụ: vào lần đăng ký hàng tuần thứ 25 của họ). - Vào ngày 8 tháng 7, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 8, 30 người nữa đã gia hạn sau 6 tháng. - Vào ngày 1 tháng 8, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã đăng ký vào ngày 1 tháng 1 vẫn duy trì đăng ký sau 6 tháng tính đến ngày 1 tháng 8. </details> ### 1 năm + \{#1-year-\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng vẫn duy trì đăng ký hơn 12 tháng kể từ lần đăng ký đầu tiên. <details> <summary>Cách hoạt động</summary> **Đặt**: - **X** = số lượt đăng ký lần đầu vào một ngày được chọn. - **Y** = số người dùng trong nhóm đó đã gia hạn ít nhất một lần sau 12 tháng kể từ ngày đăng ký ban đầu. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1 năm 2021, có 100 lượt đăng ký lần đầu. - Đến tuần đầu tháng 1 năm 2022, 20 người đã gia hạn. - Vào ngày 8 tháng 1 năm 2022, tỷ lệ chuyển đổi = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2 năm 2022, 30 người nữa đã gia hạn sau 12 tháng. - Vào ngày 1 tháng 2 năm 2022, tỷ lệ chuyển đổi = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã đăng ký vào ngày 1 tháng 1 năm 2021 đã duy trì hoạt động hơn một năm. </details> ### 2 năm + \{#2-years-\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng vẫn duy trì đăng ký hơn 24 tháng kể từ ngày thanh toán đầu tiên. <details> <summary>Cách hoạt động</summary> **Đặt**: - X = số lượt đăng ký lần đầu vào một ngày được chọn. - Y = số người dùng trong nhóm đó đã gia hạn ít nhất một lần sau 24 tháng kể từ ngày đăng ký ban đầu. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1 năm 2020, có 100 lượt đăng ký lần đầu. - Đến tuần đầu tháng 1 năm 2022, 20 người trong nhóm đó đã gia hạn. - Vào ngày 8 tháng 1 năm 2022, tỷ lệ chuyển đổi = (20 / 100) × 100% = 20% - Đến ngày 1 tháng 2 năm 2022, 30 người nữa đã gia hạn sau 2 năm. - Vào ngày 1 tháng 2 năm 2022, tỷ lệ chuyển đổi = ((20 + 30) / 100) × 100% = 50% Điều này có nghĩa là 50% người dùng đã đăng ký vào ngày 1 tháng 1 năm 2020 vẫn còn hoạt động sau 2 năm, tính đến ngày 1 tháng 2 năm 2022. </details> ### Thời gian ân hạn -> Trả phí \{#grace-period---paid\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng đã bước vào [thời gian ân hạn gói đăng ký](grace-period) và giải quyết vấn đề *trước khi* kết thúc thời gian ân hạn. <details> <summary>Cách hoạt động</summary> **Đặt**: - X = số người đăng ký đã bước vào thời gian ân hạn. - Y = số người dùng trong nhóm đó đã gia hạn gói đăng ký trước khi thời gian ân hạn kết thúc. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1 năm 2025, gói đăng ký của 100 người không thể được gia hạn tự động. Họ bước vào thời gian ân hạn 16 ngày, dự kiến kết thúc vào ngày 17 tháng 1. - 50 người đã cập nhật thông tin thanh toán của họ trong khoảng từ ngày 1 đến ngày 17 tháng 1, và gói đăng ký của họ đã được gia hạn thành công. - Vào ngày 17 tháng 1 năm 2025, tỷ lệ chuyển đổi = (50 / 100) × 100% = 50% </details> ### Vấn đề thanh toán -> Trả phí \{#billing-issue---paid\} Chỉ số này cho thấy tỷ lệ phần trăm người dùng gặp phải [vấn đề thanh toán](/billing-issue) và tiếp tục thanh toán trước khi kết thúc chu kỳ thanh toán. <details> <summary>Cách hoạt động</summary> **Đặt**: - X = số người đăng ký gặp vấn đề thanh toán. - Y = số người dùng trong nhóm đó đã gia hạn gói đăng ký trong khoảng thời gian giữa lúc gặp vấn đề thanh toán và khi kết thúc chu kỳ thanh toán. **Công thức**: Chuyển đổi = (Y / X) × 100% **Ví dụ**: - Vào ngày 1 tháng 1, 100 người đăng ký gặp vấn đề thanh toán khi gói đăng ký của họ không thể được gia hạn tự động. - Lưu ý: Nếu thời gian ân hạn được bật, trạng thái vấn đề thanh toán chỉ bắt đầu sau khi thời gian ân hạn kết thúc. Trong ví dụ này, giả sử thời gian ân hạn đã kết thúc vào ngày 1 tháng 1. - Đến ngày 8 tháng 1, 10 người dùng trong nhóm đó đã giải quyết vấn đề thanh toán và gia hạn. - Vào ngày 8 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = (10 / 100) × 100% = 10% - Đến ngày 31 tháng 1 (kết thúc chu kỳ thanh toán), 10 người dùng nữa đã gia hạn. - Vào ngày 31 tháng 1, tỷ lệ chuyển đổi cho ngày 1 tháng 1 = ((10 + 10) / 100) × 100% = 20% Điều này cho thấy 20% người dùng đã bước vào trạng thái vấn đề thanh toán vào ngày 1 tháng 1 đã giải quyết vấn đề và gia hạn trước khi kết thúc chu kỳ thanh toán của họ. </details> ## Nhóm và khoảng thời gian \{#grouping-and-time-ranges\} Đối tượng phân tích khi chọn chuyển đổi là biểu đồ. Biểu đồ thể hiện cách tỷ lệ chuyển đổi thay đổi theo thời gian. Sử dụng bộ chọn ngày để chọn các tùy chọn nhanh cho khoảng thời gian. Biểu đồ thường chứa nhiều đường cong. Mặc định có tối đa năm đường được chọn trong danh sách nhóm và bạn có thể thay đổi lựa chọn bằng cách chọn các hộp kiểm ở khu vực bên phải của biểu đồ. Khi bạn mở trang lần đầu tiên, thời lượng sản phẩm được chọn làm nhóm mặc định. Sau đó, cài đặt của bạn được lưu trong bộ nhớ cache và lần tiếp theo bạn sẽ thấy nhóm bạn đã chọn gần đây nhất. Các nhóm sau đây khả dụng: - Sản phẩm - Quốc gia - Cửa hàng - Paywall - Thời lượng - Attribution marketing Nếu khoảng ngày được chọn không đủ để hiển thị kết quả, bạn có thể thấy thông báo đề xuất một ngày phù hợp và tùy chọn tự động điều chỉnh khoảng ngày để bạn có thể thực hiện chỉ với một cú nhấp. ## Dạng bảng, bộ lọc và xuất CSV \{#table-view-filters-and-csv-export\} So sánh các đường cong cho bức tranh rõ ràng, và để khai thác thêm thông tin hãy sử dụng dạng bảng bên dưới biểu đồ. Bảng được đồng bộ với biểu đồ, khi di chuột qua một cột bạn sẽ thấy cửa sổ bật lên liên quan trên các đường cong. Nhóm được đề cập ở trên thay đổi cả biểu đồ lẫn bảng. Đặt bộ lọc nhanh theo sản phẩm hoặc sử dụng các bộ lọc nâng cao khác, bao gồm Sản phẩm, Quốc gia, Cửa hàng, Thời lượng, Attribution. Chúng tôi biết rằng việc có tùy chọn làm việc với số liệu theo cách bạn muốn là quan trọng. Vì vậy, ở bên phải bảng điều khiển, có một nút để xuất dữ liệu funnel sang CSV. Sau đó bạn có thể mở trong Excel, Google Sheets, hoặc nhập vào hệ thống phân tích của riêng bạn để tiếp tục phân tích và dự đoán trong môi trường bạn ưa thích. :::important Hãy thông báo cho Adapty nếu ứng dụng của bạn tham gia chương trình hoa hồng giảm. Để đảm bảo tính toán chính xác, hãy chỉ định trạng thái [Small Business Program](app-store-small-business-program) và [Reduced Service Fee program](google-reduced-service-fee) của bạn trong [cài đặt ứng dụng](general). ::: --- # File: reports --- --- title: "Báo cáo" description: "Tạo báo cáo gói đăng ký chi tiết trong Adapty để phân tích doanh thu ứng dụng và hành vi người dùng." --- Nhận thông tin kịp thời và liên quan trực tiếp vào hộp thư của bạn, bao gồm doanh thu, tỷ lệ rời bỏ, số người đăng ký đang hoạt động, số lượt dùng thử đang hoạt động và nhiều hơn nữa – những chỉ số tương tự có trong [Charts](charts). Các báo cáo này có thể được gửi hàng ngày, hàng tuần hoặc hàng tháng, thể hiện xu hướng so sánh kỳ gần nhất với kỳ trước đó. Dữ liệu chúng tôi gửi trong báo cáo dựa trên cấu hình trang [**Overview**](https://app.adapty.io/overview) của bạn, bao gồm các chỉ số, thứ tự của chúng, múi giờ báo cáo và loại doanh thu. Bạn có thể linh hoạt chọn mức độ chi tiết mong muốn cho báo cáo: tổng hợp hoặc theo từng ứng dụng. Báo cáo tổng hợp là một email duy nhất chứa thông tin tổng hợp của tất cả ứng dụng (hoặc tập con mà bạn đã chọn). Ngược lại, báo cáo theo từng ứng dụng chỉ chứa dữ liệu của một ứng dụng cụ thể. Chúng tôi khuyến nghị bật báo cáo tổng hợp cho tất cả ứng dụng và báo cáo theo từng ứng dụng cho các ứng dụng mới ra mắt hoặc có mức độ ưu tiên cao, cũng như những ứng dụng bạn trực tiếp phụ trách. Dù chọn mức độ chi tiết nào, báo cáo qua email đều được gửi vào hộp thư của bạn lúc 9 giờ sáng theo múi giờ địa phương: báo cáo hàng ngày đến mỗi ngày, báo cáo hàng tuần đến vào thứ Hai, và báo cáo hàng tháng đến vào ngày đầu tiên của tháng. Mỗi báo cáo bao gồm dữ liệu hiện tại cùng với so sánh với kỳ trước (ví dụ: báo cáo hàng ngày hôm nay so sánh dữ liệu của hôm qua và hôm kia; báo cáo hàng tuần hôm nay so sánh dữ liệu tuần trước và tuần trước đó, v.v.). Dù bạn chọn báo cáo nào, bạn sẽ luôn nhận được thông tin mới nhất và chính xác nhất trực tiếp trong hộp thư của mình. ## Bật báo cáo \{#enable-reports\} 1. Mở mục [**Account**](https://app.adapty.io/account) trên menu trên cùng của Adapty. 2. Trong phần **Email reports**, chọn loại báo cáo bạn muốn nhận – hàng ngày, hàng tuần và/hoặc hàng tháng. 2. Tùy chỉnh từng loại báo cáo bằng cách chọn các ứng dụng liên quan. Để thực hiện, nhấp vào nút **Edit**. 3. Trong cửa sổ báo cáo, chọn các ứng dụng bạn muốn đưa vào. 4. Cuối cùng, nhấp vào nút **Save changes** để áp dụng các lựa chọn của bạn. ## Đặt múi giờ của bạn \{#set-your-time-zone\} 1. Mở mục [**Overview**](https://app.adapty.io/overview) trong menu chính của Adapty. 2. Nhấp vào nút **Edit** và chọn múi giờ của bạn. 3. Nhấp vào nút **Done** để lưu. --- # File: discrepancies-and-troubleshooting --- --- title: "Khắc phục sự khác biệt dữ liệu" description: "Tìm nguyên nhân gây ra sự khác biệt trong dữ liệu" --- Người dùng Adapty đôi khi gặp phải **sự khác biệt** khi so sánh các tập dữ liệu tương tự từ các nguồn khác nhau. Điều này có thể xảy ra khi bạn so sánh: * Biểu đồ Adapty với báo cáo của cửa hàng * Biểu đồ Adapty với biểu đồ của bên thứ ba * Các biểu đồ khác nhau trong Adapty ## Quy trình khắc phục sự cố \{#troubleshooting-algorithm\} Hầu hết các sự khác biệt giữa Adapty và các nền tảng khác đều là điều bình thường và có thể dự đoán được. Nguyên nhân là do **các nguồn khác nhau xử lý cùng một dữ liệu theo những cách khác nhau**. Đôi khi, chúng lại cho thấy **vấn đề với cấu hình Adapty của bạn**. Nếu bạn nghi ngờ dữ liệu của mình khác nhau giữa các nền tảng, cách tốt nhất là [xuất dữ liệu thô](export-analytics-api-requests) và **so sánh các file**. * Ngay cả các cửa hàng cũng có thể gặp vấn đề liên quan đến xử lý và hiển thị dữ liệu. Hãy truy cập **dữ liệu giao dịch thô** của cửa hàng để so sánh chính xác nhất. * Khi so sánh Adapty với nền tảng analytics khác, hãy dùng báo cáo giao dịch của cửa hàng làm nguồn dữ liệu tham chiếu. * Việc xác định sự khác biệt sẽ dễ hơn với tập dữ liệu nhỏ. Hãy so sánh lượng dữ liệu nhỏ — tập trung vào một sản phẩm cụ thể trong một ngày duy nhất. * Xác định xem sự khác biệt của bạn xuất phát từ **giá cả** hay **số lượng sự kiện**. Vấn đề về giá có thể được khắc phục bằng cách [cập nhật sản phẩm](#product-pricing). Vấn đề về sự kiện có thể chỉ ra [sự cố phía máy chủ](#issues-with-server-notifications-and-rtdn). * Xem [event feed](event-feed) để theo dõi các sự kiện đến — bạn có thể nhận thấy những hành vi bất thường. Sau khi xác định được điểm dữ liệu bắt đầu phân kỳ, bạn có thể tìm hiểu các nguyên nhân phổ biến sau đây: ## Sự cố với thông báo máy chủ và RTDN \{#issues-with-server-notifications-and-rtdn\} Adapty sẽ không nhận được dữ liệu sự kiện cần thiết nếu bạn chưa cấu hình đúng kết nối cửa hàng. Điều này đặc biệt ảnh hưởng đến các sự kiện xảy ra mà không có sự tham gia trực tiếp của người dùng — gia hạn gói đăng ký, vấn đề thanh toán, v.v. Hoàn tất cấu hình server-to-server càng sớm càng tốt ([App Store](enable-app-store-server-notifications) | [Play Store](enable-real-time-developer-notifications-rtdn)) và [chờ](#data-delays) để các cửa hàng thiết lập kết nối. Bạn có thể [tải lên thủ công](importing-historical-data-to-adapty) dữ liệu App Store Connect còn thiếu vào Adapty. ## Dữ liệu bị thiếu \{#missing-data\} ### Người dùng có phiên bản ứng dụng cũ \{#users-with-out-of-date-app-versions\} Nếu một số người dùng đang chạy phiên bản ứng dụng cũ hơn không có Adapty SDK, Adapty sẽ không nhận được dữ liệu của họ. Vì lý do này, các con số từ Adapty và các nguồn khác sẽ không khớp nhau. ### Vấn đề tích hợp \{#integration-issues\} Một số tích hợp Adapty (ví dụ: Adjust hoặc AppsFlyer) yêu cầu thêm code trong ứng dụng để hoạt động. Nếu bạn cấu hình Adapty dashboard nhưng không cập nhật ứng dụng, dữ liệu cần thiết sẽ không xuất hiện trong Adapty. ### Thiếu dữ liệu lịch sử \{#missing-historical-data\} Adapty không có quyền truy cập vào dữ liệu lịch sử của ứng dụng, trừ khi bạn [tự import](importing-historical-data-to-adapty). Nếu [khoảng thời gian](controls-filters-grouping-compare-proceeds#set-the-date-range) của biểu đồ bắt đầu trước khi bạn tích hợp Adapty và bạn chưa import dữ liệu lịch sử, các giá trị sẽ khác với các nguồn khác. ## Độ trễ dữ liệu \{#data-delays\} Adapty hướng đến việc phân tích gần như theo thời gian thực nền kinh tế ứng dụng của bạn. Các giới hạn và ngoại lệ sau đây áp dụng: * Khi bạn tích hợp Adapty lần đầu, dữ liệu có thể không xuất hiện ngay lập tức. * Khi bạn bật tích hợp với nền tảng bên thứ ba, có thể có độ trễ trước khi dữ liệu được đồng bộ hoàn toàn. * Sau khi Adapty nhận được dữ liệu cửa hàng, cần thêm **15-30 phút** để xử lý và hiển thị trên trang Analytics. * Trao đổi dữ liệu giữa Adapty và bên thứ ba **không phải lúc nào cũng tức thì** do số lượng các biến số liên quan. * Các tính toán cho một số chỉ số nâng cao (chẳng hạn như [dự đoán cohort](predicted-ltv-and-revenue)) yêu cầu một lượng dữ liệu nhất định. Adapty sẽ chỉ thực hiện các tính toán này khi thu thập đủ dữ liệu. ## Thời gian và lịch \{#time-and-calendar\} #### Ngày và múi giờ \{#dates-and-timezones\} Một trong những lý do phổ biến nhất gây ra sự khác biệt dữ liệu là sự khác nhau trong cài đặt múi giờ. Adapty tính ngày theo múi giờ `UTC`. Nếu nền tảng khác sử dụng múi giờ khác, các tính toán sẽ không giống nhau. Sự khác biệt sẽ giảm dần khi bạn mở rộng phạm vi thời gian. Bạn có thể [thay đổi cài đặt múi giờ](general#3-reporting-timezone) cho từng ứng dụng. #### Lịch tài chính của Apple \{#the-apple-fiscal-calendar\} Apple sử dụng [lịch kế toán](https://adapty.io/apple-fiscal-calendar/) riêng để xác định kỳ bán hàng và ngày thanh toán. Mỗi "tháng" trong lịch này gồm **4 hoặc 5 tuần**, và **có thể bao gồm các ngày từ các tháng lịch kề nhau**. Các khoản thanh toán thường được thực hiện trong vòng 30–45 ngày sau khi kỳ bán hàng kết thúc. Ví dụ: kỳ bán hàng "tháng 1 năm 2026" bắt đầu vào ngày 28 tháng 12 năm 2025 — 4 ngày trước khi tháng lịch bắt đầu. Ngày thanh toán ước tính cho kỳ này là ngày 5 tháng 3. Đừng so sánh dữ liệu từ báo cáo thanh toán của Apple với các tháng lịch. Thay vào đó, hãy chọn [khoảng ngày tùy chỉnh](controls-filters-grouping-compare-proceeds#set-the-date-range) tương ứng với kỳ bán hàng cần thiết. #### Ngày giao dịch \{#transaction-dates\} Một số dịch vụ (ví dụ: AppsFlyer) có thể áp dụng quy tắc [cohort](analytics-cohorts) khi hiển thị giao dịch, và gán chúng cho ngày cài đặt ứng dụng thay vì ngày giao dịch thực tế xảy ra. ## Tính toán doanh thu \{#revenue-calculation\} ### Phí và thuế \{#fees-and-taxes\} Tùy thuộc vào [cài đặt](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue), biểu đồ Adapty có thể hiển thị **doanh thu gộp**, **doanh thu sau khi trừ hoa hồng cửa hàng**, hoặc **doanh thu sau khi trừ hoa hồng cửa hàng và thuế**. Một số cửa hàng và nền tảng bên thứ ba có thể không có khả năng hiển thị doanh thu gộp hoặc tự động khấu trừ thuế. Nếu bạn thấy sự khác biệt giữa hai biểu đồ doanh thu khác nhau, hãy đảm bảo rằng việc so sánh là hợp lệ. ### Hủy và hoàn tiền \{#cancellations-and-refunds\} Các nền tảng khác nhau hiển thị dữ liệu hoàn tiền theo những cách khác nhau. Adapty xử lý hoàn tiền như doanh thu âm. Nếu người dùng đăng ký và yêu cầu hoàn tiền vào ngày hôm sau, cả hai sự kiện đều sẽ được phản ánh trong biểu đồ Adapty — mỗi sự kiện vào ngày riêng của nó. Các nền tảng khác có thể trừ giá trị hoàn tiền khỏi giao dịch gốc. ## Giao dịch sandbox \{#sandbox-purchases\} [Event feed](event-feed) hiển thị các giao dịch được thực hiện bởi tài khoản sandbox. Các biểu đồ analytics thì không. Tuy nhiên, nếu dữ liệu import lịch sử của bạn có chứa giao dịch sandbox, Adapty sẽ không thể phân biệt chúng, và các biểu đồ sẽ phản ánh các giao dịch sandbox trong lịch sử. ## Lượt cài đặt và tải xuống \{#installs-and-downloads\} Các cửa hàng (đặc biệt là Apple App Store) có thể theo dõi trực tiếp lượt tải xuống của người dùng. Số liệu thống kê của họ có thể bao gồm các trường hợp ứng dụng đã được cài đặt nhưng chưa bao giờ được khởi chạy. Adapty chỉ có thể ghi nhận một lượt cài đặt khi người dùng khởi chạy ứng dụng, bất kể [định nghĩa cài đặt](general#4-installs-definition-for-analytics) của bạn là gì. ## Quốc gia và cửa hàng \{#country-and-store\} Để đảm bảo báo cáo chính xác, Adapty [có thể suy luận](controls-filters-grouping-compare-proceeds#filter-and-group-data) quốc gia của người dùng từ IP của họ. Các cửa hàng luôn gán lượt tải xuống và giao dịch cho một app store cụ thể. Nếu bạn cần phân biệt rõ ràng giữa hai điều này, bạn có thể [tạo phân khúc người dùng mới](segments) với thuộc tính `Country by store account` và [lọc analytics theo phân khúc](controls-filters-grouping-compare-proceeds#filter-and-group-data). ## Giá sản phẩm \{#product-pricing\} Nếu giá sản phẩm không chính xác gây ra sự khác biệt doanh thu, việc thay đổi giá sẽ không sửa lại các giao dịch cũ. Để thay đổi giá của các giao dịch hiện có, bạn cần ghi đè bắt buộc bằng cách import dữ liệu chính xác. Khi người dùng khôi phục một giao dịch cũ sau khi giá thay đổi, Apple có thể báo cáo sai giá trị của giao dịch đó. Bạn cần import dữ liệu lịch sử để Adapty phản ánh đúng giá trị. ## Xung đột attribution \{#attribution-conflicts\} Adapty chỉ có thể sử dụng [một nguồn attribution duy nhất](attribution-integration#prevent-data-issues) cho mỗi giao dịch. Bạn không thể ghi đè dữ liệu này sau đó. Nếu thiết lập của bạn bao gồm nhiều nhà cung cấp attribution không đồng nhất với nhau, cùng một giao dịch trên hai nền tảng khác nhau có thể xuất hiện với hai nguồn traffic khác nhau. ## Sự khác biệt trong thuật ngữ \{#differences-in-terminology\} Các nền tảng khác nhau có thể có tên gọi khác nhau cho cùng một khái niệm. Các chỉ số liên quan đến [doanh thu](#fees-and-taxes) có tên gọi khác nhau tùy nền tảng: | Adapty | App Store Connect | Google Play Console | |--------|-------------------|----------------------| | **Gross revenue** | Sales | Gross Revenue | | **Proceeds after store commission** | N/A | N/A | | **Proceeds after store commission and taxes** | Proceeds | Earnings | | **ARPPU** | Proceeds per paying user | ARPPU | Các chỉ số khác cũng có thể khác nhau về định nghĩa: - **Gói đăng ký**: - Adapty không tính các lượt dùng thử mới là gói đăng ký. Một [gói đăng ký mới](reactivated-subscriptions) luôn bắt đầu bằng một giao dịch tài chính. - Các nền tảng khác, chẳng hạn như Google Play Console, có thể tính **mỗi lượt dùng thử là một gói đăng ký mới**, ngay cả trước khi thanh toán đầu tiên được thực hiện. - **Tỷ lệ giữ chân**: - Adapty đo tỷ lệ giữ chân dựa trên số lần gia hạn gói đăng ký. - App Store Connect coi người dùng là được giữ lại nếu họ mở ứng dụng vào ngày được chỉ định. Người dùng không có gói đăng ký vẫn được tính, nhưng người dùng có gói đăng ký mà không mở ứng dụng vào ngày đó sẽ không được tính. - Chỉ số "Retained Installers" của Google Play Console đo tỷ lệ giữ chân dựa trên số ngày ứng dụng vẫn còn được cài đặt trên thiết bị của người dùng. Người dùng không mở ứng dụng vẫn được tính vào chỉ số này. ## Chỉ số gói đăng ký mới so với sự kiện `subscription_started` \{#new-subscriptions-metric-vs-the-subscription_started-event\} Chỉ số [Gói đăng ký mới](reactivated-subscriptions) và [sự kiện tích hợp](events) `subscription_started` đếm những thứ khác nhau, vì vậy tổng số của chúng không khớp nhau. Chỉ số này đếm cả các giao dịch mua đầu tiên không qua dùng thử lẫn các lượt chuyển đổi từ dùng thử sang trả phí. Sự kiện `subscription_started` chỉ kích hoạt cho các giao dịch mua đầu tiên không qua dùng thử — khi dùng thử chuyển sang trả phí, Adapty gửi `trial_converted` thay thế. Kết quả là, số lượng gói đăng ký mới sẽ cao hơn số sự kiện `subscription_started` bất cứ khi nào ứng dụng của bạn có lượt chuyển đổi từ dùng thử. --- # File: predicted-ltv-and-revenue --- --- title: "Dự đoán trong cohort" description: "Sử dụng phân tích dự đoán của Adapty để dự báo LTV và doanh thu." --- Tính năng Dự đoán của Adapty được thiết kế để giúp bạn trả lời những câu hỏi sau: 1. Giá trị vòng đời (LTV) dự đoán của các cohort người dùng là bao nhiêu? 2. Cohort nào có khả năng tạo ra doanh thu cao nhất trong tương lai? 3. Bạn có thể đầu tư bao nhiêu dựa trên mức lợi nhuận dự đoán? Với Adapty Predictions, bạn có thể đưa ra các quyết định dựa trên dữ liệu về doanh thu và tăng trưởng. Mô hình dự đoán của Adapty ước tính tiềm năng doanh thu dài hạn của các cohort người dùng trong ứng dụng của bạn. Với mỗi cohort, mô hình dự báo doanh thu, số lượng người đăng ký đang trả phí và LTV trung bình sẽ thay đổi như thế nào theo thời gian. Điều này giúp bạn đưa ra các quyết định sáng suốt về thu hút người dùng, chiến lược marketing và phát triển sản phẩm. Adapty cung cấp dự đoán giá trị vòng đời (LTV) và doanh thu dự đoán cho các cohort người dùng trả phí. Các dự đoán được hiển thị trên trang phân tích cohort cho các mốc 3, 6, 9, 12, 18 và 24 tháng sau khi tạo cohort. Đối với các ứng dụng có lịch sử dữ liệu rất hạn chế, mô hình sẽ sử dụng mức trung bình từ nhiều ứng dụng khác, do đó dự đoán cho các ứng dụng mới có thể chưa phản ánh đầy đủ hành vi cụ thể của người dùng. ## Mô hình hoạt động như thế nào \{#how-the-model-works\} Mô hình dự đoán của Adapty sử dụng các mẫu giữ chân người dùng từ dữ liệu cohort lịch sử để dự báo doanh thu và LTV trong tương lai. Với mỗi tổ hợp ứng dụng và loại gói đăng ký, mô hình đo lường mức thay đổi của số lượng người đăng ký có trả phí và tổng doanh thu từ kỳ gia hạn này sang kỳ gia hạn tiếp theo. Mô hình tính hai tỷ lệ giữ chân — một cho người đăng ký, một cho doanh thu — dựa trên các cohort quá khứ của ứng dụng. Các tỷ lệ này sau đó được áp dụng cho các cohort mới để dự báo tăng trưởng trong vòng 3, 6, 9, 12, 18 và 24 tháng sau khi cohort được tạo. Dữ liệu sử dụng được ẩn danh hoàn toàn. Model tạo ra hai giá trị cho mỗi cohort: - **Predicted revenue**: Tổng doanh thu mà một cohort được dự báo sẽ tạo ra trong khoảng thời gian đã chọn. - **Predicted LTV**: Doanh thu dự đoán chia cho số lượng người đăng ký có trả phí dự kiến trong cohort. ### Trọng số theo ứng dụng và trọng số chung \{#app-specific-and-cross-app-weights\} Theo mặc định, các dự đoán cho một cohort sẽ sử dụng trọng số retention được học từ chính các cohort lịch sử của ứng dụng đó, phản ánh hành vi người dùng đặc thù của ứng dụng. Khi một ứng dụng chưa có đủ dữ liệu lịch sử cho một khoảng thời gian dự đoán cụ thể, Adapty sẽ sử dụng trọng số giữ chân người dùng được tính trung bình từ tất cả các ứng dụng có cùng loại gói đăng ký làm phương án dự phòng. Ví dụ, một dự đoán 12 tháng cho ứng dụng mới chỉ hoạt động được sáu tháng sẽ sử dụng dữ liệu dự phòng tổng hợp từ nhiều ứng dụng. Phương án dự phòng này được áp dụng độc lập cho từng khoảng thời gian, vì vậy cùng một cohort có thể dùng trọng số riêng của ứng dụng cho dự đoán 3 tháng và dùng trọng số tổng hợp cho dự đoán 12 tháng. ### Tính khả dụng và cập nhật \{#availability-and-updates\} Dự đoán sẽ khả dụng sau khi một cohort hoàn thành kỳ gia hạn đầu tiên — thường là một tuần sau khi tạo đối với gói đăng ký hàng tuần và khoảng bốn tuần đối với gói đăng ký hàng tháng. Sau đó, dự đoán được cập nhật hàng ngày bằng dữ liệu giao dịch mới nhất, giúp chúng luôn phản ánh hành vi hiện tại của cohort. ### Hạn chế \{#limitations\} - **Chất lượng dữ liệu**: Hành vi cohort bất thường hoặc cohort có quá ít người dùng trả phí làm giảm độ chính xác. Các cohort có ít hơn 100 người dùng trả phí sẽ bị loại khỏi dữ liệu huấn luyện của mô hình. - **Ứng dụng mới**: Các ứng dụng không có đủ lịch sử sẽ sử dụng trọng số dự phòng chung, vốn có thể không phản ánh đúng hành vi người dùng của ứng dụng. - **Tuổi cohort**: Dự đoán cho một khoảng thời gian nhất định sẽ bị ẩn khi cohort vượt quá khoảng đó. Ví dụ: dự đoán 3 tháng sẽ ngừng hiển thị sau ba tháng, và không có dự đoán nào được hiển thị cho các cohort cũ hơn 24 tháng. ## Trên Dashboard \{#in-the-dashboard\} Để xem các dự đoán, hãy điều hướng đến trang Cohort analysis trong Adapty dashboard của bạn. Để biết thêm chi tiết về cohort, xem [Phân tích Cohort](analytics-cohorts). <img src="/assets/shared/img/4d808b4-Export-1691486610612.gif" alt="Trang Cohort Analysis hiển thị các cột Predicted Revenue và Predicted LTV" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Cột **Predicted revenue** hiển thị tổng doanh thu ước tính mà một cohort người dùng đăng ký dự kiến tạo ra trong khoảng thời gian đã chọn kể từ khi cohort được tạo. Giá trị này được tính toán bằng mô hình dự đoán của Adapty, dựa trên các mẫu giữ chân cohort lịch sử của ứng dụng. Cột **Predicted LTV** hiển thị giá trị vòng đời ước tính của mỗi người dùng trong cohort đã chọn. Giá trị này được tính bằng cách chia doanh thu dự đoán cho số lượng người dùng trả phí dự đoán trong cohort. ### Chọn khoảng thời gian dự đoán \{#select-the-horizon\} Để thay đổi khoảng thời gian dự đoán, hãy chọn một giá trị từ menu thả xuống **Predictions**. Các tùy chọn có sẵn là 3, 6, 9, 12, 18 và 24 tháng sau khi cohort được tạo. ### Lọc theo sản phẩm \{#filter-by-product\} Bạn có thể lọc doanh thu dự đoán và LTV theo sản phẩm. Theo mặc định, dự đoán được xây dựng từ tất cả dữ liệu mua hàng — lọc theo sản phẩm cho thấy từng sản phẩm đóng góp như thế nào. <img src="/assets/shared/img/66a9c61-Export-1691486288948.gif" alt="Phân tích Cohort được lọc theo sản phẩm" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Khi nào dự đoán không khả dụng \{#when-predictions-are-unavailable\} Khi không thể tạo dự đoán cho một cohort, các cột Predicted Revenue và Predicted LTV sẽ hiển thị dấu gạch ngang (—) thay vì giá trị. Điều này có thể xảy ra vì một số lý do: - **Chưa đủ thời gian kể từ khi tạo cohort**: Dự đoán chỉ khả dụng sau khi cohort hoàn thành kỳ gia hạn đầu tiên — khoảng một tuần đối với gói đăng ký hàng tuần và khoảng bốn tuần đối với gói đăng ký hàng tháng. - **Cohort quá nhỏ**: Số lượng người dùng trả phí quá ít để tạo ra dự đoán đáng tin cậy. - **Hành vi cohort bất thường**: Cohort có sự khác biệt đáng kể so với các mẫu mà mô hình dự kiến. Chờ thêm vài tuần có thể giải quyết được vấn đề này khi dữ liệu tích lũy thêm. - **Vượt quá horizon**: Cohort cũ hơn horizon dự đoán đã chọn. Ví dụ: dự đoán 3 tháng sẽ bị ẩn sau ba tháng, dự đoán 12 tháng bị ẩn sau mười hai tháng, và không hiển thị dự đoán nào cho các cohort cũ hơn 24 tháng. :::warning Khi bật tính năng dự đoán, cần lưu ý rằng có thể mất tối đa 24 giờ trước khi dữ liệu dự đoán về Doanh thu và LTV xuất hiện trên Adapty Dashboard của bạn. ::: --- # File: predictions-in-ab-tests --- --- title: "Dự đoán trong A/B test" description: "Tìm hiểu cách dự đoán trong A/B test giúp tinh chỉnh chiến lược định giá gói đăng ký." --- Chào mừng bạn đến với tài liệu Phân tích dự đoán của Adapty dành cho tính năng A/B test. Công cụ này sẽ cung cấp thông tin chi tiết về kết quả tương lai của các A/B test đang chạy và giúp bạn đưa ra quyết định dựa trên dữ liệu nhanh hơn 🚀 với các dự đoán được hỗ trợ bởi ML của Adapty. ### Dự đoán A/B test là gì? \{#what-are-ab-test-predictions\} Tính năng Dự đoán A/B test của Adapty sử dụng các kỹ thuật machine learning tiên tiến (cụ thể là mô hình gradient boosting) để dự báo tiềm năng doanh thu dài hạn của các paywall được so sánh trong một A/B test. Mô hình dự đoán này cho phép bạn chọn paywall hiệu quả nhất dựa trên doanh thu dự kiến sau một năm, thay vì chỉ dựa vào các chỉ số bạn quan sát được trong khi test đang chạy. Điều này giúp bạn xác định người chiến thắng một cách đáng tin cậy hơn và nhanh hơn, mà không cần phải chờ hàng tuần để dữ liệu tích lũy đủ. ### Mô hình hoạt động như thế nào? \{#how-does-the-model-work\} Mô hình được huấn luyện trên dữ liệu lịch sử A/B test phong phú từ nhiều ứng dụng thuộc các danh mục khác nhau. Nó tích hợp nhiều tính năng để dự đoán doanh thu mà một paywall có khả năng tạo ra trong một năm sau khi thử nghiệm bắt đầu. Các tính năng này bao gồm: - Giao dịch và tỷ lệ chuyển đổi của người dùng theo các khoảng thời gian khác nhau - Phân bố địa lý của người dùng - Nền tảng sử dụng (iOS hoặc Android) - Tỷ lệ hủy đăng ký và hoàn tiền - Các sản phẩm gói đăng ký và độ dài chu kỳ của chúng (hàng ngày, hàng tháng, hàng năm, v.v.) - Dữ liệu liên quan đến giao dịch khác Mô hình cũng tính đến các thời gian dùng thử trong paywall, sử dụng tỷ lệ chuyển đổi lịch sử để dự đoán doanh thu như thể người dùng đã chuyển đổi. Điều này đảm bảo so sánh công bằng giữa các paywall có và không có ưu đãi dùng thử, vì chúng tôi cũng sẽ tính đến các lượt dùng thử đang hoạt động có khả năng mang lại doanh thu trong tương lai. ### Điều gì khác biệt giữa Predicted P2BB và P2BB thông thường? \{#how-is-predicted-p2bb-different-from-just-the-p2bb\} Các A/B test của chúng tôi sử dụng phương pháp Bayesian: về cơ bản, chúng tôi mô hình hóa phân phối doanh thu trên mỗi người dùng (hay cụ thể hơn là "Doanh thu trên 1K người dùng") rồi tính toán xác suất một phân phối "thực sự" tốt hơn phân phối kia chứ không phải do ngẫu nhiên — và đó là điều chúng tôi gọi là Probability-to-be-the-best hay P2BB (bạn có thể tìm hiểu thêm về cách tiếp cận của chúng tôi [tại đây](maths-behind-it)). Điều quan trọng cần lưu ý là khi làm như vậy, chúng tôi chỉ dựa vào doanh thu đã tích lũy trong thời gian test chạy. Vì vậy, nếu bạn chạy một test so sánh gói đăng ký hàng năm với gói hàng tuần, bạn sẽ phải chờ rất lâu để thực sự hiểu cái nào hiệu quả hơn. Điều tương tự cũng xảy ra khi bạn so sánh gói đăng ký có dùng thử với gói không có dùng thử trong A/B test — vì các lượt dùng thử đang hoạt động có thể làm thay đổi kết quả người chiến thắng luôn không được tính vào doanh thu. Đây là lúc mô hình dự đoán của chúng tôi phát huy tác dụng. Dựa trên phân phối doanh thu hiện tại trong A/B test và được huấn luyện trên bộ dữ liệu lớn, mô hình có khả năng dự đoán phiên bản tương lai của phân phối doanh thu (cụ thể là sau 1 năm). Và sau đó, nó tạo ra predicted P2BB — giá trị bạn sẽ đạt được nếu chạy test trong toàn bộ một năm. Lưu ý rằng đôi khi predicted P2BB có thể mâu thuẫn với P2BB hiện tại. Khi điều đó xảy ra, chúng tôi tô màu vàng các hàng biến thể như sau: <img src="/assets/shared/img/74577c6-CleanShot_2024-02-15_at_13.08.452x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chúng tôi xem đó là dấu hiệu cho thấy bạn nên tích lũy thêm dữ liệu để xác nhận người chiến thắng hoặc tìm hiểu sâu hơn về A/B test để tìm ra nguyên nhân đằng sau. Nhìn chung, chúng tôi khuyên bạn nên tin tưởng predicted P2BB hơn P2BB hiện tại vì nó đơn giản là tính đến nhiều dữ liệu hơn, nhưng quyết định cuối cùng tất nhiên là tùy bạn. ### Độ chính xác và độ tin cậy của mô hình \{#model-accuracy-and-certainty\} Mô hình đạt mức độ chính xác cao, với Mean Absolute Percentage Error (MAPE) ở mức dưới 10% một chút. Mức độ chính xác này cho phép các doanh nghiệp tự tin dựa vào các dự đoán của mô hình khi đưa ra quyết định dựa trên dữ liệu. Để đảm bảo tính ổn định hơn nữa, mô hình sử dụng tiêu chí "độ tin cậy" dựa trên ba yếu tố: - Khoảng dự đoán hẹp — mô hình tự tin vào kết quả của mình - Lượng gói đăng ký và doanh thu đủ trong test - Ít nhất 2 tuần kể từ khi test bắt đầu Một dự đoán được coi là đáng tin cậy khi ít nhất hai trong ba tiêu chí này được đáp ứng. Khi một A/B test mới bắt đầu, mô hình cung cấp dự đoán doanh thu trên 1K người dùng trong một năm tới (chỉ số A/B test chính của chúng tôi) cho mỗi paywall. Các dự đoán chỉ được hiển thị khi chúng đáp ứng tiêu chí độ tin cậy. Nếu dữ liệu không đủ, mô hình sẽ hiển thị "insufficient data for prediction". ### Hạn chế và những điều cần lưu ý \{#limitations-and-considerations\} Mặc dù mô hình dự đoán của chúng tôi là một công cụ mạnh mẽ, nhưng điều quan trọng là phải xem xét các hạn chế của nó. Hiệu suất của mô hình phụ thuộc vào chất lượng và tính đại diện của dữ liệu có sẵn. Hành vi cohort bất thường hoặc các ứng dụng mới chưa có trong tập huấn luyện có thể ảnh hưởng đến độ chính xác của dự đoán. Tuy nhiên, các dự đoán được cập nhật hàng ngày để phản ánh dữ liệu và hành vi người dùng mới nhất. Điều này đảm bảo rằng thông tin bạn nhận được luôn dựa trên thông tin cập nhật nhất. 🚧 Lưu ý: Công cụ này là một phần bổ sung, không phải là sự thay thế cho đánh giá chuyên môn và sự hiểu biết của bạn về những đặc thù riêng của ứng dụng. Hãy sử dụng các dự đoán này như một hướng dẫn cùng với các chỉ số khác và kiến thức thị trường để đưa ra quyết định sáng suốt. --- # File: adapty-ads-manager --- --- title: "Adapty Ads Manager" description: "Nhận phân tích thời gian thực từ Apple Ads và quản lý, tối ưu hóa các chiến dịch của bạn" --- **Adapty Ads Manager** là nền tảng tất cả trong một được thiết kế để giúp bạn quản lý, tối ưu hóa và mở rộng các chiến dịch Apple Ads một cách hiệu quả hơn. Nền tảng này kết nối hiệu suất Apple Search Ads với các chỉ số doanh thu quan trọng như lượt cài đặt, dùng thử, gói đăng ký và giá trị vòng đời — mà không cần MMP. Với phân tích thời gian thực, dự đoán dựa trên AI và tự động hóa thông minh, Adapty Ads Manager loại bỏ việc điều chỉnh giá thầu thủ công tẻ nhạt, bảng tính và các phỏng đoán, thay thế chúng bằng những thông tin rõ ràng và công cụ giúp bạn hành động nhanh hơn. Với Adapty Ads Manager, bạn sẽ có: - **[Tổng quan](ads-manager-overview)**: Tất cả các chỉ số quan trọng trong một màn hình — chi tiêu, doanh thu, ROAS, CPA và nhiều hơn nữa — mỗi chỉ số đều có biểu đồ xu hướng theo ngày - **[AI Agent](ads-manager-ai-agent)**: Đặt câu hỏi bằng ngôn ngữ tự nhiên và nhận câu trả lời cùng khuyến nghị toàn phễu - **Dữ liệu hiệu suất theo thời gian thực**: Trên các chiến dịch, nhóm quảng cáo và từ khóa - **Theo dõi doanh thu đầu cuối**: Từ tìm kiếm → cài đặt → dùng thử → gói đăng ký → LTV - **Dự đoán & khuyến nghị từ AI**: Để mở rộng quy mô có lợi nhuận - **Quản lý hàng loạt**: Giá thầu, ngân sách, trạng thái và cấu trúc - **[Tự động hóa dựa trên quy tắc](ads-manager-automations)**: Quản lý toàn bộ vòng đời từ khóa - **[Thông tin thị trường](ads-manager-market-intelligence)**: Chiến lược từ khóa của đối thủ trên 50+ quốc gia - **[CPP A/B Tests](ads-manager-cpp-ab-tests)**: So sánh các trang sản phẩm tùy chỉnh và tìm ra phương án hiệu quả nhất <CustomDocCardList ids={['adapty-ads-manager-get-started', 'ads-manager-overview', 'ads-manager-ai-agent', 'adapty-ads-manager-analytics', 'ads-manager-create-campaign', 'ads-manager-create-ad-group', 'ads-manager-manage-keywords', 'ads-manager-automations', 'ads-manager-market-intelligence', 'ads-manager-cpp-ab-tests']} /> ## Tại sao chọn Adapty Ads Manager? \{#why-choose-adapty-ads-manager\} Vì chúng tôi cung cấp cho bạn **dữ liệu chính xác nhất trên thị trường.** Không giống như Apple Ads console gốc hay các MMP, dữ liệu của chúng tôi **theo thời gian thực, không mất mát và kết nối đầy đủ** với các trial, gói đăng ký và LTV — không có độ trễ hay khoảng trống attribution. Với cách triển khai đơn giản và trải nghiệm người dùng mượt mà, bạn có thể quản lý mọi thứ ở một nơi mà không cần phải chuyển đổi qua lại giữa nhiều công cụ. ## Bắt đầu \{#get-started\} Để bắt đầu với Adapty Ads Manager, hãy làm theo [hướng dẫn](adapty-ads-manager-get-started) và bạn đã sẵn sàng khám phá. --- # File: adapty-ads-manager-get-started --- --- title: "Bắt đầu với Adapty Ads Manager" description: "Nhập dữ liệu lịch sử từ Apple Ads và bắt đầu nhận cập nhật theo thời gian thực trên dashboard" --- [Adapty Ads Manager](adapty-ads-manager) là nền tảng tối ưu hóa và phân tích dành cho Apple Ads. Trong hướng dẫn này, bạn sẽ học cách bắt đầu sử dụng Adapty Ads Manager qua hai bước: 1. Cài đặt Adapty SDK và để nó theo dõi dữ liệu mua hàng của bạn. 2. Kết nối Adapty Ads Manager với tài khoản Apple Ads của bạn để nhập dữ liệu lịch sử và bắt đầu theo dõi các cập nhật theo thời gian thực. :::note Adapty Ads Manager không sử dụng [tích hợp Apple Ads](apple-search-ads) từ **App settings**. Để sử dụng Adapty Ads Manager, bạn chỉ cần hoàn thành các bước thiết lập được mô tả trong hướng dẫn này. ::: ## 1. Cài đặt Adapty SDK \{#1-install-the-adapty-sdk\} :::important Adapty Ads Manager là một **sản phẩm độc lập**. Bạn có thể sử dụng nó ngay cả khi paywall, gói đăng ký hay analytics của bạn không được quản lý bởi Adapty — bạn không cần phải migrate toàn bộ hệ thống sang Adapty. Để có dữ liệu doanh thu chính xác, bạn chỉ cần cài đặt tối thiểu là: cài SDK ở chế độ observer và bật thông báo server App Store trong Adapty. ::: Để kết nối dữ liệu doanh thu với hiệu suất chiến dịch, hãy để Adapty theo dõi các giao dịch mua hàng của bạn: 1. Bước đầu tiên phụ thuộc vào việc bạn đã có in-app purchase được triển khai hay chưa: - Nếu bạn **đã triển khai in-app purchase với Adapty**, bạn không cần làm gì thêm ở bước này. - Nếu bạn **đã triển khai in-app purchase mà không dùng Adapty** và không có kế hoạch migrate sang Adapty, hãy cài đặt SDK cho nền tảng của bạn ở chế độ observer. Ở bước này, bạn chỉ cần thêm SDK vào dự án, kích hoạt nó với chế độ observer được bật, và báo cáo các giao dịch: - [iOS](implement-observer-mode) - [Android](implement-observer-mode-android) - [React Native](implement-observer-mode-react-native) - [Flutter](implement-observer-mode-flutter) - [Unity](implement-observer-mode-unity) - [Kotlin Multiplatform](implement-observer-mode-kmp) - [Capacitor](implement-observer-mode-capacitor) - Nếu bạn **chưa triển khai in-app purchase và muốn sử dụng Adapty**, hãy hoàn thành các bước trong [hướng dẫn quickstart](quickstart) để ủy quyền xử lý giao dịch cho Adapty. 2. Để nhận các cập nhật liên quan đến doanh thu trực tiếp từ App Store, hãy [bật thông báo server App Store trong Adapty](enable-app-store-server-notifications). ## 2. Kết nối Apple Ads \{#2-connect-apple-ads\} :::important Bạn cần có vai trò **Account Admin** trong Apple Ads để kết nối Apple Ads với Adapty. ::: Tiếp theo, bạn cần kết nối tài khoản Adapty Ads Manager với tài khoản Apple Ads của mình: 1. Nhấp vào logo Adapty trên thanh tiêu đề và chọn **Search Ads**. 2. Nhấp vào **Continue with Apple**. 3. Đăng nhập vào tài khoản Apple của bạn. 4. Chọn quyền truy cập bạn muốn cấp cho Adapty Ads Manager: - **Read and Write**: Cấp quyền truy cập vào tất cả các nhóm chiến dịch. - **Limited access**: Chọn các nhóm chiến dịch cụ thể và gán vai trò **Read & Write** để chỉ cấp quyền truy cập vào những nhóm đó. 5. Nhấp vào **Grant access**. Sau đó, Adapty sẽ bắt đầu đồng bộ dữ liệu lịch sử của bạn từ Apple Ads. Bạn có thể bắt đầu khám phá Adapty Ads Manager ngay bây giờ, nhưng sẽ mất một khoảng thời gian để toàn bộ dữ liệu lịch sử được nhập xong. ## Tiếp theo là gì \{#whats-next\} Sau khi đồng bộ thành công dữ liệu giao dịch, hãy tiếp tục tìm hiểu cách: - [Quản lý chiến dịch, nhóm quảng cáo và từ khóa của bạn](ads-manager) - [Thiết lập quy tắc tự động để điều chỉnh giá thầu dựa trên hiệu suất chiến dịch](ads-manager-automations) --- # File: ads-manager-overview --- --- title: "Tổng quan trong Adapty Ads Manager" description: "Xem tất cả các chỉ số Apple Ads quan trọng trong một trang, mỗi chỉ số đi kèm biểu đồ xu hướng." --- Trang **Overview** hiển thị tất cả các chỉ số Apple Ads quan trọng trong một nơi, mỗi chỉ số đi kèm một biểu đồ xu hướng. Theo mặc định, trang hiển thị dữ liệu cho tất cả các ứng dụng đã kết nối. Để xem từng ứng dụng riêng lẻ, chọn ứng dụng từ menu thả xuống ở phần header. Để mở trang này, vào **Overview** ở thanh bên trái của Adapty Ads Manager. :::tip Để xem tóm tắt những gì cần chú ý thay vì phải dò từng chỉ số, hãy hỏi [AI Agent](ads-manager-ai-agent). ::: ## Chỉ số \{#metrics\} Mỗi chỉ số hiển thị dưới dạng thẻ kèm biểu đồ xu hướng cho khoảng thời gian đã chọn. Để xem định nghĩa và công thức tính từng chỉ số, xem [Chỉ số trong Apple Ads Manager](adapty-ads-manager-metrics). ## Cấu hình các chỉ số hiển thị \{#configure-displayed-metrics\} Để thay đổi các chỉ số xuất hiện trên trang **Overview**, nhấn **Edit metrics**. Tại đây bạn có thể: - **Thêm chỉ số**: Nhấn **Add metric** và chọn các checkbox cho những chỉ số bạn muốn. - **Xóa chỉ số**: Bỏ chọn checkbox trong bảng **Add metric**, hoặc nhấn **×** bên cạnh chỉ số đó. ## Điều khiển \{#controls\} Sử dụng các điều khiển ở trên cùng để điều chỉnh nội dung hiển thị trên trang **Overview**: - **Date range**: Chọn khoảng thời gian có sẵn (**Last 7 days**, **Last 30 days**, **Last 90 days**) hoặc nhập khoảng tùy chỉnh. Tất cả các biểu đồ và giá trị tổng hợp sẽ cập nhật theo khoảng thời gian đã chọn. - **Chart type**: Chuyển đổi giữa các dạng biểu đồ cột xếp chồng, đường, và tròn. - **Revenue display**: Chọn cách tính các chỉ số doanh thu: - **Gross revenue**: Tổng doanh thu trước khi khấu trừ bất kỳ khoản nào. - **Proceeds after store commission**: Doanh thu sau khi trừ hoa hồng của Apple. - **Proceeds after store commissions and taxes**: Doanh thu ròng sau khi trừ cả hoa hồng của Apple và các khoản thuế áp dụng. --- # File: ads-manager-ai-agent --- --- title: "AI Agent trong Adapty Ads Manager" description: "Đặt câu hỏi bằng ngôn ngữ tự nhiên về tài khoản Apple Ads của bạn và nhận câu trả lời cùng các đề xuất cho toàn bộ phễu." --- AI Agent là một trợ lý chat trong Adapty Ads Manager, giúp trả lời các câu hỏi về tài khoản Apple Ads của bạn bằng ngôn ngữ tự nhiên. Nó bao quát toàn bộ phễu — impression, cài đặt, dùng thử, gói đăng ký và doanh thu — nên có thể phân tích doanh thu và ROAS, không chỉ dừng lại ở lượt nhấp. Attribution tích hợp sẵn của Adapty cung cấp dữ liệu phễu này cho agent gần như theo thời gian thực. Trợ lý chỉ có vai trò tư vấn: nó phân tích tài khoản của bạn và đề xuất những việc cần làm, nhưng không tự thay đổi chiến dịch, giá thầu hay ngân sách thay bạn. ## Những gì bạn có thể hỏi \{#what-you-can-ask\} Hỏi agent về bất kỳ phần nào trong tài khoản Apple Ads của bạn. Agent có thể: - **Tổng quan tài khoản**: Tóm tắt những gì đang xảy ra trong tài khoản và những vấn đề cần ưu tiên giải quyết trước. - **Đối tượng hoạt động kém**: Tìm các chiến dịch không mang lại hiệu quả và từ khóa không tạo ra chuyển đổi. - **Ngân sách**: Xác định các chiến dịch đã chạm giới hạn ngân sách hàng ngày và tư vấn có nên tăng lên hay không. - **Quyết định giá thầu và tạm dừng**: Đề xuất có nên tăng giá thầu hay giữ nguyên, và có nên tạm dừng chiến dịch hay tiếp tục. - **Hiệu suất theo địa lý**: Cho thấy quốc gia nào hoạt động tốt và quốc gia nào kém, từ đó gợi ý phân bổ lại ngân sách. ## Ví dụ về các câu hỏi \{#example-questions\} Agent sẽ cho câu trả lời hữu ích nhất khi bạn nêu rõ một chỉ số, một khoảng thời gian, và quyết định bạn đang cân nhắc. Ví dụ: - Những từ khóa nào có ROAS cao nhất, và tôi nên phân bổ lại ngân sách như thế nào? - Những từ khóa nào có chi tiêu cao nhưng không có trial hay gói đăng ký nào trong 30 ngày qua, và nên tạm dừng từ khóa nào? - Những chiến dịch nào đạt giới hạn ngân sách hàng ngày nhưng vẫn có lợi nhuận, và nên tăng thêm bao nhiêu cho mỗi chiến dịch? - So sánh các quốc gia theo ROAS và chi phí mỗi gói đăng ký — nên chuyển ngân sách về đâu? - Tôi có nên giảm bid cho từ khóa này, tạm dừng nó, hay cần thêm thời gian? Hãy cho tôi xem dữ liệu funnel đằng sau quyết định đó. - Những từ khóa nào mang lại lượt cài đặt rẻ nhưng ít khi chuyển đổi thành gói đăng ký trả phí? ## Mở AI Agent \{#open-the-ai-agent\} Để mở agent, nhấn **Ask AI Agent** trong header của tài khoản. Trước khi đặt câu hỏi, hãy chọn một ứng dụng để xác định phạm vi hoạt động của agent. Sau đó nhập câu hỏi và gửi đi. Agent chạy các tác vụ ở chế độ nền, vì vậy bạn có thể tiếp tục làm việc trong Adapty Ads Manager trong khi nó chuẩn bị câu trả lời. Để thay đổi mô hình trả lời câu hỏi của bạn, hãy dùng bộ chọn bên cạnh ô nhập tin nhắn. ## Xem lại các cuộc trò chuyện trước \{#access-previous-chats\} Bảng thu gọn chỉ hiển thị cuộc trò chuyện hiện tại của bạn. Để xem lại các cuộc trò chuyện trước, hãy nhấp vào nút mở rộng ở đầu bảng để vào chế độ toàn màn hình. Danh sách **Chats** sẽ mở ra ở bên trái, nơi bạn có thể tìm kiếm các cuộc trò chuyện cũ hoặc bắt đầu cuộc trò chuyện mới bằng **New chat**. ## Hạn chế \{#limitations\} AI Agent chỉ mang tính tư vấn. Nó đề xuất các hành động nhưng không tự áp dụng thay bạn. Hãy xem xét các đề xuất, sau đó tự thực hiện thay đổi khi [quản lý chiến dịch](ads-manager-create-campaign) và [từ khóa](ads-manager-manage-keywords). --- # File: adapty-ads-manager-metrics --- --- title: "Các chỉ số trong Apple Ads Manager" description: "Xem phân tích ứng dụng trong Apple Ads Manager." --- Apple Ads Manager cung cấp các chỉ số toàn diện để đo lường hiệu suất chiến dịch và hành vi người dùng. ## Hiệu suất \{#performance\} | Chỉ số | Mô tả | |--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Spend | Tổng chi phí cho mỗi lần người dùng nhấn vào quảng cáo của bạn. | | Impressions | Số lần quảng cáo được tài trợ của bạn xuất hiện trong kết quả tìm kiếm App Store trong khoảng thời gian báo cáo. | | CPM | Số tiền trung bình bạn trả cho mỗi nghìn lần hiển thị quảng cáo. CPM trung bình = Spend / (Impressions / 1000) Lưu ý: đối với chiến dịch App Store Search Results sử dụng mô hình giá CPT, đây là CPM hiệu quả. | | Taps | Số lần người dùng nhấn vào quảng cáo trong khoảng thời gian báo cáo. | | CPT | Số tiền trung bình bạn trả cho mỗi lần nhấn vào quảng cáo. CPT trung bình = Spend / Taps | | TTR | Số lần quảng cáo được nhấn chia cho tổng số lần hiển thị. TTR = Taps / Impressions * 100% | | Downloads (Total) | Tổng số lượt tải mới và tải lại từ quảng cáo thông qua tap-through và view-through trong khoảng thời gian báo cáo. | | Downloads (View-Through) | Số lượt tải và tải lại từ người dùng đã xem quảng cáo trong khoảng thời gian 24 giờ nhưng không nhấn vào. | | Downloads (Tap-Through) | Tổng số lượt tải mới và tải lại từ người dùng đã nhấn vào quảng cáo trong khoảng thời gian 30 ngày. | | Avg CPA (Total) | Chi phí trung bình trên mỗi lượt mua (CPA) tổng là tổng chi tiêu chiến dịch chia cho tổng số lượt tải xuất phát từ việc xem hoặc nhấn vào quảng cáo trong khoảng thời gian báo cáo. | | Avg CPA (Tap-Through) | Chi phí trung bình trên mỗi lượt mua (CPA) tap-through là tổng chi tiêu chiến dịch chia cho số lượt tải tap-through trong khoảng thời gian báo cáo. | | Download Rate (Total) | Tổng số lượt tải xuất phát từ việc xem hoặc nhấn vào quảng cáo chia cho tổng số lần nhấn trong khoảng thời gian báo cáo. Công thức: (Total Downloads / Taps) × 100% nếu Taps > 0, ngược lại là 0% | | Download Rate (Tap-Through) | Tổng số lượt tải xuất phát từ việc nhấn vào quảng cáo chia cho tổng số lần nhấn trong khoảng thời gian báo cáo. Công thức: (Tap-Through Downloads / Taps) × 100% nếu Taps > 0, ngược lại là 0% | | DPM (Total) | Downloads per Mille (DPM) là số lượt tải trên mỗi nghìn lần hiển thị. Công thức: (Total Downloads / Impressions) × 1000 nếu Impressions > 0, ngược lại là 0 | ## Chuyển đổi \{#conversions\} :::note Doanh thu, ARPU, ARPPU, ARPAS, ROAS và ROI cũng có sẵn dưới dạng chỉ số cohort để phân tích theo thời gian của các nhóm người dùng. ::: | Chỉ số | Mô tả | |--------|--------| | Conversions | Conversions là tổng số sự kiện chuyển đổi trong kỳ báo cáo. Công thức: Trials started + Subscriptions started + Non-subscriptions | | Conversion CR | Conversion CR (Tỷ lệ chuyển đổi) là tỷ lệ phần trăm tổng số lượt tải xuống dẫn đến chuyển đổi. Công thức: (Conversions / Total Downloads) × 100% nếu Total Downloads > 0, ngược lại là 0% | | Cost per Conversion | Cost per Conversion là tổng chi tiêu của bạn chia cho số lượng chuyển đổi. Công thức: Spend / Conversions nếu Conversions > 0, ngược lại là 0 | | Revenue | Revenue là tổng số tiền thu được từ các giao dịch mua, gia hạn hoặc các chuyển đổi có thu phí khác trong ứng dụng của bạn trong khoảng thời gian đã chọn (trước khi trừ hoa hồng của cửa hàng). | | ROAS | ROAS (Return on Ad Spend) là doanh thu từ quảng cáo chia cho chi tiêu quảng cáo, được biểu thị bằng phần trăm. Công thức: (Revenue / Spend) × 100% nếu Spend > 0, ngược lại là 0% | | ROI | ROI (Return on Investment) đo lợi nhuận ròng so với chi tiêu. Công thức: ((Revenue − Spend) / Spend) × 100% nếu Spend > 0, ngược lại là 0% | | ARPU | ARPU (Average Revenue per User) là doanh thu trung bình trên mỗi người dùng. Được tính bằng tổng doanh thu chia cho số người dùng duy nhất. Ví dụ: doanh thu $60.000 / 5.000 người dùng = ARPU $12. Hữu ích khi so sánh giá trị này với chi phí mỗi lần cài đặt (CPI) để đánh giá hiệu quả của các chiến dịch marketing. | | ARPPU | ARPPU (Average Revenue per Paying User) là doanh thu trung bình trên mỗi người dùng trả tiền. Được tính bằng tổng doanh thu chia cho số người dùng trả tiền duy nhất. Ví dụ: doanh thu $60.000 / 1.000 người dùng trả tiền = ARPPU $60. Giúp bạn hiểu một khách hàng trả tiền tạo ra bao nhiêu doanh thu trung bình. | | ARPAS | ARPAS là doanh thu trung bình trên mỗi subscriber đang hoạt động. Được tính bằng tổng doanh thu / số subscriber đang hoạt động. Subscriber ở đây là những người đã kích hoạt giai đoạn dùng thử hoặc gói đăng ký. Ví dụ: doanh thu $60.000 / 1.500 subscriber = ARPAS $40. | | Installs | Installs là tổng số người dùng đã cài đặt ứng dụng lần đầu tiên, cũng như các lần cài đặt lại của người dùng hiện có. Bao gồm nhiều lần cài đặt bởi cùng một người dùng trên các thiết bị khác nhau. Lưu ý rằng các lần tải xuống chưa hoàn tất hoặc cài đặt bị hủy trước khi hoàn thành sẽ không được tính vào số lần cài đặt. | | Installs CR | Installs CR (Tỷ lệ chuyển đổi) là tỷ lệ phần trăm người dùng trong tổng số lượt tải xuống đã cài đặt ứng dụng. Công thức: (Installs / Total Downloads) × 100% nếu Total Downloads > 0, ngược lại là 0% | | CPI | CPI (Cost per Install) là chi phí mỗi lần cài đặt được ghi nhận bởi Adapty. Công thức: Spend / Installs nếu Installs > 0, ngược lại là 0 | | Trials | Trials là số lượng gói đăng ký dùng thử mới được khởi tạo trong kỳ báo cáo. | | Trial CR | Trial CR (Tỷ lệ chuyển đổi) là tỷ lệ phần trăm tổng số lượt tải xuống đã bắt đầu dùng thử. Công thức: (Trials / Total Downloads) × 100% nếu Total Downloads > 0, ngược lại là 0% | | Cost per Trial | Cost per Trial là tổng chi tiêu của bạn chia cho số lượng lần bắt đầu dùng thử mới. Công thức: Spend / Trials nếu Trials > 0, ngược lại là 0 | | Trials converted | Trials converted là số lượng gói đăng ký dùng thử đã chuyển đổi thành công thành gói đăng ký có trả phí trong kỳ báo cáo. | | Trial converted CR | Trial converted CR (Tỷ lệ chuyển đổi) là tỷ lệ phần trăm gói đăng ký dùng thử đã chuyển đổi thành gói đăng ký có trả phí. Công thức: (Trials converted / Trials) × 100% nếu Trials > 0, ngược lại là 0% | | Cost per Trial converted | Cost per Trial converted là tổng chi tiêu của bạn chia cho số lượng trial đã chuyển đổi trong cùng kỳ. Công thức: Spend / Trials converted nếu Trials converted > 0, ngược lại là 0 | | Subscriptions | Subscriptions là tổng số lượng đăng ký mới (không phải dùng thử) trong kỳ báo cáo. | | Subscription CR | Subscription CR (Tỷ lệ chuyển đổi) là tỷ lệ phần trăm tổng số lượt tải xuống đã đăng ký gói có trả phí (không có dùng thử miễn phí). Công thức: (Subscriptions / Total Downloads) × 100% nếu Total Downloads > 0, ngược lại là 0% | | Cost per Subscription | Cost per Subscription là tổng chi tiêu của bạn chia cho số lượng gói đăng ký mới được bắt đầu. Công thức: Spend / Subscriptions nếu Subscriptions > 0, ngược lại là 0 | | Non-subscriptions started | Non-subscriptions started là tổng số sản phẩm mua một lần trong ứng dụng không dựa trên gói đăng ký. | | Non-subscription CR | Non-subscription CR (Tỷ lệ chuyển đổi) là tỷ lệ phần trăm tổng số lượt tải xuống đã thực hiện sản phẩm mua một lần trong ứng dụng của bạn. Công thức: (Non-subscriptions started / Total Downloads) × 100% nếu Total Downloads > 0, ngược lại là 0% | | Cost per Non-subscription | Cost per Non-subscription là tổng chi tiêu của bạn chia cho số lượng sản phẩm mua một lần. Công thức: Spend / Non-subscriptions started nếu Non-subscriptions started > 0, ngược lại là 0 | ## Tải xuống nâng cao \{#advanced-downloads\} | Chỉ số | Mô tả | |--------|-------| | Lượt tải mới (Tổng cộng) | Tổng số lượt tải mới qua tap và qua view trong kỳ báo cáo. | | Lượt tải mới (Qua view) | Lượt tải mới từ người dùng đã xem quảng cáo nhưng không nhấn vào và trước đây chưa tải app của bạn, được tính trong cửa sổ 24 giờ. | | Lượt tải mới (Qua tap) | Lượt tải mới từ người dùng đã nhấn vào quảng cáo và trước đây chưa tải app của bạn. Được tính trong cửa sổ attribution 30 ngày. | | Tỷ lệ lượt tải mới (Qua tap) | Cho thấy tỷ lệ phần trăm tổng lượt tải từ tap là lượt tải mới (từ người dùng đã nhấn vào quảng cáo trong cửa sổ attribution 30 ngày). | | Lượt tải lại (Tổng cộng) | Tổng số lượt tải lại qua tap và qua view trong kỳ báo cáo. | | Lượt tải lại (Qua view) | Lượt tải lại từ người dùng đã xem quảng cáo nhưng không nhấn vào trong cửa sổ 24 giờ. Được tính khi người dùng tải app, xóa đi rồi tải lại trên cùng một thiết bị hoặc thiết bị khác sau khi xem quảng cáo. | | Lượt tải lại (Qua tap) | Lượt tải lại từ người dùng đã nhấn vào quảng cáo trong cửa sổ attribution 30 ngày. Được tính khi người dùng tải app, xóa đi rồi tải lại trên cùng một thiết bị hoặc thiết bị khác sau khi nhấn vào quảng cáo. | | Tỷ lệ lượt tải lại (Qua tap) | Cho thấy tỷ lệ phần trăm tổng lượt tải từ tap là lượt tải lại. | ## Thông tin chi tiết \{#insights\} | Chỉ số | Mô tả | |--------|-------| | Impression Share | Impression Share là tỷ lệ phần trăm lượt hiển thị mà quảng cáo của bạn nhận được so với tổng số lượt hiển thị cho cùng một từ khóa tìm kiếm. | | Rank | Rank (hiện tại) là vị trí hiện tại của ứng dụng theo thị phần hiển thị cho từ khóa tìm kiếm đã chọn tại một quốc gia hoặc khu vực cụ thể. | | Search Popularity | Search Popularity (hiện tại) là mức độ phổ biến của các từ khóa tìm kiếm theo quốc gia hoặc khu vực. Thang điểm từ 1–5, trong đó 5 là mức lượng tìm kiếm cao nhất. | --- # File: ads-manager-create-campaign --- --- title: "Quản lý chiến dịch trong Adapty Ads Manager" description: "Tạo và chỉnh sửa chiến dịch Apple Ads trong Adapty Ads Manager." --- Adapty Ads Manager tích hợp hai chiều với Apple Ads: bạn nhận được dữ liệu hiệu suất gần theo thời gian thực và có thể tạo, chỉnh sửa chiến dịch trực tiếp từ Adapty Dashboard theo cách tiện lợi hơn nhiều so với giao diện gốc của Apple. Nếu bạn tạo một chiến dịch trong Apple Ads dashboard gốc, chiến dịch đó sẽ tự động xuất hiện trong Adapty Ads Manager trong vòng 24 giờ. Ngoài việc [xem các chỉ số chiến dịch toàn diện](adapty-ads-manager-analytics), bạn có thể quản lý tất cả cài đặt chiến dịch: - Tạo chiến dịch - Chỉnh sửa chiến dịch hiện có - Khởi chạy & tạm dừng chiến dịch :::tip Để tìm các chiến dịch cần chú ý — chẳng hạn như chiến dịch đã đạt giới hạn ngân sách hàng ngày hoặc không hiệu quả — hãy hỏi [AI Agent](ads-manager-ai-agent). ::: ## Campaign là gì \{#what-is-a-campaign\} Campaign tập trung vào một ứng dụng duy nhất và chạy quảng cáo tại một placement trên App Store. Mỗi campaign bao gồm ngân sách hàng ngày và [nhóm quảng cáo](ads-manager-create-ad-group) tập trung vào một chiến lược cụ thể để quảng bá ứng dụng của bạn. Campaign sẽ tiếp tục chi tiêu dựa trên cài đặt ngân sách của nó. :::important Lưu ý rằng một chiến dịch không thể tự chạy được; nhóm quảng cáo là cấp độ mà bạn thiết lập giá thầu mặc định, đối tượng và từ khóa. Nếu không có nhóm quảng cáo, chiến dịch sẽ không có targeting hay giá thầu và sẽ không phân phối. Hãy tạo chiến dịch, sau đó [thêm ít nhất một nhóm quảng cáo](ads-manager-create-ad-group) để kích hoạt nó. ::: ## Tạo chiến dịch \{#create-campaigns\} Để tạo một chiến dịch: 1. Mở trang **Ads Manager** và nhấp **+**. Chọn **Create campaign** để khởi chạy trình hướng dẫn tạo chiến dịch. 2. Chọn **placement** cho quảng cáo của bạn, sau đó nhấp **Start**: | Placement | Vị trí hiển thị quảng cáo | | --- | --- | | **Search Results** | Ở đầu kết quả tìm kiếm trên App Store. | | **Search Tab** | Trong danh sách ứng dụng gợi ý trên tab Tìm kiếm, trước khi người dùng tìm kiếm. | | **Today Tab** | Trên trang Today của App Store. | | **Product Pages** | Trong danh sách **You Might Also Like** trên trang của các sản phẩm khác. Apple sẽ tự chọn các trang phù hợp cho bạn. | | **Duplicate a Campaign** | Tái sử dụng placement, nhóm quảng cáo và từ khóa từ một chiến dịch hiện có. | 3. Đối với các chiến dịch **Search Result**, hãy chọn **loại chiến dịch**. Việc nhóm các chiến dịch theo loại giúp tổ chức dữ liệu báo cáo và cho phép Adapty đề xuất chiến lược từ khóa phù hợp. :::note Chọn **Max Conversions** để bỏ qua việc chọn từ khóa thủ công và để tính năng đặt giá thầu tự động của Apple tối ưu hóa cho các lượt chuyển đổi. ::: | Loại chiến dịch | Mô tả | | --- | --- | | **Generic** | Các từ khóa không có thương hiệu mô tả chức năng ứng dụng của bạn. | | **Competitor** | Các từ khóa thương hiệu của đối thủ cạnh tranh. | | **Discovery** | Search Match tự động tìm ra các từ khóa mới, giúp bạn thu thập những từ khóa hiệu quả nhất. | | **Max Conversions** | Chiến lược đặt giá thầu tự động của Apple được tối ưu hóa cho chuyển đổi. Đặt **Target CPA** ở bước Settings. | | **Brand** | Các từ khóa gắn liền với thương hiệu ứng dụng của bạn. | | **Custom** | Không có chiến lược định sẵn. Tự xây dựng chiến dịch từ đầu. | 4. Chọn **app** (ứng dụng bạn muốn quảng bá) và **campaign group** (tài khoản Apple Ads chạy và thanh toán cho chiến dịch). 5. Chọn **countries or regions** để nhắm mục tiêu. Để dễ tối ưu hóa hơn, hãy sử dụng một chiến dịch cho mỗi quốc gia. 6. Cấu hình **basic settings** của chiến dịch. Nhắm mục tiêu đối tượng và từ khóa nằm trong [ad groups](ads-manager-create-ad-group) của chiến dịch, được thêm vào sau khi tạo chiến dịch. | Cài đặt | Mô tả | | --- | --- | | **Campaign name** | Tự động điền từ ứng dụng, placement và quốc gia. Có thể chỉnh sửa bất cứ lúc nào. | | **Daily budget** | Số tiền chiến dịch có thể chi tiêu mỗi ngày. | | **Target CPA** | Mục tiêu chi phí mỗi lần chuyển đổi cho hệ thống đặt giá thầu tự động. Chỉ xuất hiện với chiến dịch Max Conversions. | | **Ad scheduling** | Tùy chọn. Chiến dịch bắt đầu ngay lập tức trừ khi bạn đặt ngày bắt đầu muộn hơn; thêm ngày kết thúc để dừng chiến dịch vào thời điểm cụ thể. | Tài khoản theo hạn mức tín dụng cũng cần hoàn tất thông tin thanh toán ở bước này. 7. Xem lại phần tóm tắt, xác nhận thông tin, rồi nhấp **Create campaign**. 8. Tiếp tục [cấu hình nhóm quảng cáo](ads-manager-create-ad-group). Chiến dịch không thể chạy nếu không có nhóm quảng cáo — chúng xác định đối tượng và/hoặc từ khóa. ## Chỉnh sửa chiến dịch \{#edit-campaigns\} Để chỉnh sửa bất kỳ chiến dịch nào đã tạo: 1. Mở cài đặt chiến dịch bằng một trong hai cách sau: - Nhấp vào tên chiến dịch trong **Ads Manager > Campaigns**. Sau đó, nhấp vào **Edit campaign** ở góc trên bên phải. - Hoặc chọn hộp kiểm bên cạnh tên chiến dịch và nhấp vào **Actions > Edit campaign settings**. 2. Điều chỉnh cài đặt chiến dịch. Bạn có thể chỉnh sửa tên chiến dịch, loại chiến dịch (dành cho chiến dịch Search Results), quốc gia và ngân sách hàng ngày. Để thay đổi vị trí hiển thị quảng cáo, chiến lược đặt giá thầu hoặc lịch chạy, hãy tạo một chiến dịch mới thay thế. 3. Nhấp vào **Save changes**. Bạn cũng có thể thay đổi loại chiến dịch trực tiếp trong cột **Campaign type** của bảng chiến dịch. :::note Các chỉnh sửa thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ với Adapty Ads Manager, nhưng có thể mất một chút thời gian để hiển thị. ::: ## Xuất danh sách chiến dịch \{#export-campaigns\} Để xuất bảng chiến dịch dưới dạng CSV, nhấp vào biểu tượng tải xuống phía trên bảng và chọn **Export current page** hoặc **Export all pages**. **Export all pages** tải xuống tất cả các chiến dịch trên mọi trang vào một file duy nhất. Một cửa sổ tiến trình sẽ theo dõi quá trình tải xuống; bạn có thể hủy bất cứ lúc nào. Có hai bộ lọc tùy chọn: - **Enabled only**: Chỉ bao gồm các chiến dịch đang hoạt động. - **With spend ≥**: Chỉ bao gồm các chiến dịch có chi tiêu vượt ngưỡng được chỉ định. - **Group by country**: Phân tách từng dòng chiến dịch theo quốc gia. Bảng dữ liệu được xuất đúng như hiển thị trên dashboard của bạn, với các cột bạn đã chọn để hiển thị. ## Khởi chạy & tạm dừng chiến dịch \{#launch--pause-campaigns\} Để khởi chạy hoặc tạm dừng bất kỳ chiến dịch nào từ Adapty Ads Manager: 1. Vào **Ads Manager > Campaigns**. 2. Bật hoặc tắt công tắc bên cạnh tên chiến dịch trong cột **Status**. ## Xóa chiến dịch \{#delete-campaigns\} Adapty Ads Manager không thể xóa chiến dịch. Thay vào đó, bạn có thể [tạm dừng](#launch-pause-campaigns) chiến dịch để dừng chi tiêu nhưng vẫn giữ lại cài đặt và dữ liệu phân tích. --- # File: ads-manager-create-ad-group --- --- title: "Quản lý nhóm quảng cáo trong Adapty Ads Manager" description: "Tạo và chỉnh sửa nhóm quảng cáo Apple Ads trong Adapty Ads Manager." --- Adapty Ads Manager tích hợp hai chiều với Apple Ads: bạn nhận được dữ liệu hiệu suất gần theo thời gian thực, đồng thời có thể tạo và chỉnh sửa chiến dịch trực tiếp từ Adapty Dashboard theo cách tiện lợi hơn nhiều so với giao diện gốc. Nếu bạn tạo một nhóm quảng cáo trong dashboard Apple Ads gốc, nhóm đó sẽ tự động xuất hiện trong Adapty Ads Manager trong vòng 24 giờ. Ngoài việc [xem các chỉ số chiến dịch toàn diện](adapty-ads-manager-analytics), bạn có thể quản lý tất cả các cài đặt nhóm quảng cáo: - Tạo nhóm quảng cáo - Chỉnh sửa nhóm quảng cáo hiện có - Khởi chạy & tạm dừng nhóm quảng cáo ## Ad group là gì \{#what-is-ad-group\} Ad group thuộc một [campaign](ads-manager-create-campaign) và là nơi bạn cấu hình chiến lược nhắm mục tiêu và đặt giá thầu cho quảng cáo. Mỗi ad group bao gồm cài đặt giá thầu, nhắm mục tiêu đối tượng, và [từ khóa](ads-manager-manage-keywords) xác định thời điểm và đối tượng được xem quảng cáo của bạn. Ad group giúp bạn tổ chức chiến lược quảng cáo trong một campaign và thử nghiệm các cách tiếp cận nhắm mục tiêu khác nhau. :::important Lưu ý rằng một chiến dịch không thể chạy nếu không có nhóm quảng cáo. Nhóm quảng cáo là cấp độ nơi bạn thiết lập giá thầu mặc định, đối tượng và từ khóa, vì vậy nếu không có ít nhất một nhóm quảng cáo, chiến dịch sẽ không có mục tiêu hay giá thầu và sẽ không phân phát. Hãy tạo chiến dịch trước, sau đó [thêm ít nhất một nhóm quảng cáo](ads-manager-create-ad-group) để kích hoạt nó. ::: ## Tạo nhóm quảng cáo \{#create-ad-groups\} Để tạo nhóm quảng cáo Apple Ads mới: 1. Vào **Ads Manager** từ menu sidebar. Ở bất kỳ tab nào, nhấp **+** phía trên bảng, rồi chọn **Create ad group**. 2. Chọn ứng dụng bạn muốn thêm ad group vào. 3. Chọn chiến dịch bạn muốn thêm ad group vào. 4. Cấu hình các cài đặt cho ad group: - **Ad group name**: Nhãn bạn đặt để nhận dạng và tìm kiếm ad group của mình trên dashboard. - **Default max CPT bid**: Số tiền tối đa bạn sẵn sàng trả cho mỗi lượt nhấn vào quảng cáo. Mức bid này áp dụng cho tất cả các từ khóa trong ad group, trừ khi bạn thiết lập bid riêng cho từng từ khóa. - **CPA cap (limits impressions)** (Tùy chọn): Cài đặt này xác định số tiền tối đa bạn sẵn sàng chi cho mỗi lượt chuyển đổi qua nhấn (ví dụ: tải xuống hoặc một hành động mục tiêu khác). Nó đặt mức trần bid cho tất cả các từ khóa trong ad group của bạn. Giới hạn giá thầu được tính bằng cách nhân CPA cap bạn cung cấp với tỷ lệ chuyển đổi tap-through của bạn: `CPA Cap × CR (Tap-Through)`. Nếu mức giá thầu CPT tối đa cho từ khóa thấp hơn giá trị này, mức giá thầu CPT tối đa thấp hơn sẽ được áp dụng. Ví dụ: nếu CPA cap của bạn là $5 và tỷ lệ chuyển đổi tap-through là 65%, mức giá thầu tối đa được áp dụng cho tất cả các từ khóa trong nhóm quảng cáo sẽ là $3,25. Nếu CPT tối đa được đặt ở mức $4, mức giá thầu tối đa được áp dụng vẫn sẽ là $3,25. - **Search Match**: Bật để tự động khớp quảng cáo của bạn với các tìm kiếm liên quan mà không cần chỉ định từ khóa. Khi bật, Apple Ads có thể hiển thị quảng cáo của bạn cho các tìm kiếm liên quan đến metadata và danh mục ứng dụng của bạn. - **Audience**: Tiêu chí nhắm mục tiêu xác định người dùng nào sẽ thấy quảng cáo của bạn. - **All eligible users**: Hiển thị quảng cáo của bạn cho tất cả người dùng đủ điều kiện cho chiến dịch của bạn. - **Specific audiences**: Nhắm mục tiêu các phân khúc người dùng cụ thể bằng cách cấu hình: - **Devices**: Nhắm mục tiêu iPad, iPhone hoặc cả hai. - **Customer type**: Nhắm mục tiêu tất cả người dùng, người dùng mới hoặc người dùng quay lại. - **Gender**: Nhắm mục tiêu theo giới tính hoặc tất cả người dùng. - **Age range**: Nhắm mục tiêu các độ tuổi cụ thể hoặc tất cả người dùng. - **Ad scheduling** (Tùy chọn, khả dụng khi bạn chọn **Specific audiences**): Đặt thời điểm quảng cáo bắt đầu chạy: - **Start date and time**: Thời điểm nhóm quảng cáo của bạn bắt đầu phục vụ quảng cáo. - **End date** (Tùy chọn): Thời điểm nhóm quảng cáo của bạn ngừng phục vụ quảng cáo. 5. Nhấp vào **Create**. 6. Nếu loại vị trí chiến dịch của bạn là **Search results**, bạn có thể [thêm từ khóa](ads-manager-manage-keywords) để bắt đầu phục vụ quảng cáo. Đối với các loại vị trí khác, bạn đã hoàn tất. :::note Trong chiến dịch **Max Conversions**, nhóm quảng cáo có toggle **Bidding strategy**: **Standard** hoặc **Automated**. Không có trường **Default max CPT bid**. Với **Automated**, bạn đặt CPA mục tiêu và Apple sẽ tự động tối ưu hóa giá thầu cho bạn. ::: ## Chỉnh sửa nhóm quảng cáo \{#edit-ad-groups\} Để chỉnh sửa bất kỳ nhóm quảng cáo nào đã tạo: 1. Mở cài đặt chiến dịch bằng một trong các cách sau: - Nhấp vào tên chiến dịch trong **Ads Manager > Ad groups**. Sau đó, nhấp vào **Edit ad group** ở góc trên bên phải. - Hoặc chọn hộp kiểm bên cạnh tên nhóm quảng cáo và nhấp vào **Actions > Edit ad group settings**. 2. Điều chỉnh cài đặt nhóm quảng cáo. Không thể thay đổi ứng dụng, chiến dịch, loại đối tượng, ngày và giờ bắt đầu. Trong nhóm quảng cáo tự động, chỉ có thể chỉnh sửa tên. 3. Nhấp vào **Save changes**. Để nhân bản một nhóm quảng cáo, hãy chọn nó trong **Ads Manager > Ad groups** và nhấp vào **Actions > Duplicate ad group**. :::note Các chỉnh sửa đối với nhóm quảng cáo được thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ với Adapty Ads Manager, nhưng có thể mất một chút thời gian để hiển thị trong Adapty Ads Manager. ::: ## Xuất nhóm quảng cáo \{#export-ad-groups\} Để xuất bảng nhóm quảng cáo dưới dạng CSV, nhấp vào biểu tượng tải xuống phía trên bảng và chọn **Export current page** hoặc **Export all pages**. **Export all pages** tải xuống tất cả nhóm quảng cáo trên mọi trang vào một file duy nhất. Một cửa sổ tiến trình sẽ theo dõi quá trình tải xuống; bạn có thể hủy bất cứ lúc nào. Có hai bộ lọc tùy chọn: - **Enabled only**: Chỉ bao gồm các nhóm quảng cáo đang hoạt động. - **With spend ≥**: Chỉ bao gồm các nhóm quảng cáo có chi tiêu vượt ngưỡng chỉ định. - **Group by country**: Phân tách từng dòng nhóm quảng cáo theo quốc gia. Bảng được xuất theo đúng giao diện trên dashboard của bạn, với các cột bạn đã chọn để hiển thị. ## Khởi chạy & tạm dừng nhóm quảng cáo \{#launch--pause-ad-groups\} Để khởi chạy hoặc tạm dừng bất kỳ nhóm quảng cáo nào từ Apple Ads Manager: 1. Vào **Ads Manager > Ad groups**, hoặc điều hướng đến trang chiến dịch để xem các nhóm quảng cáo của nó. 2. Bật hoặc tắt nút chuyển bên cạnh tên nhóm quảng cáo trong cột **Status**. --- # File: ads-manager-manage-keywords --- --- title: "Quản lý từ khóa trong Adapty Ads Manager" description: "Thêm và quản lý từ khóa Apple Ads, từ khóa loại trừ và từ khóa SKAG trong Adapty Ads Manager." --- Adapty Ads Manager tích hợp hai chiều với Apple Ads: bạn nhận được dữ liệu hiệu suất gần thời gian thực và có thể tạo, chỉnh sửa từ khóa trực tiếp từ Adapty Dashboard — tiện lợi hơn nhiều so với giao diện gốc của Apple Ads. Nếu bạn tạo một từ khóa trong dashboard Apple Ads gốc, từ khóa đó sẽ tự động xuất hiện trong Adapty Ads Manager trong vòng 24 giờ. Ngoài việc [khám phá phân tích toàn diện](adapty-ads-manager-analytics), bạn có thể quản lý tất cả các cài đặt từ khóa của mình: - Thêm từ khóa vào nhóm quảng cáo - Thêm từ khóa phủ định - Thêm từ khóa dưới dạng SKAG (Single Keyword Ad Group) - Chỉnh sửa từ khóa trực tiếp trong bảng - Thực hiện hành động hàng loạt trên nhiều từ khóa - Bật & tắt từ khóa :::tip Để tìm những từ khóa hoạt động kém hiệu quả trong tài khoản của bạn — ví dụ: từ khóa có chi phí nhưng không có lượt chuyển đổi — hãy hỏi [AI Agent](ads-manager-ai-agent). ::: ## Keywords là gì \{#what-are-keywords\} Keywords là các cụm từ tìm kiếm khiến quảng cáo của bạn xuất hiện trong kết quả tìm kiếm App Store. Chúng được tổ chức trong các [ad group](ads-manager-create-ad-group), thuộc về các [chiến dịch](ads-manager-create-campaign). Cấu trúc phân cấp này giúp bạn tổ chức và quản lý chiến lược quảng cáo hiệu quả. :::important Keywords chỉ áp dụng cho các chiến dịch có loại placement **Search results**. Đối với các chiến dịch có loại placement khác (Search tab hoặc Product pages), keywords không được sử dụng. ::: ### Standard keywords \{#standard-keywords\} Standard keywords là các cụm từ chính bạn đặt giá thầu để kích hoạt quảng cáo. Khi người dùng tìm kiếm các cụm từ này trên App Store, quảng cáo của bạn có thể xuất hiện trong kết quả tìm kiếm. ### Negative keywords \{#negative-keywords\} Negative keywords ngăn quảng cáo của bạn xuất hiện trong các lượt tìm kiếm không liên quan đến ứng dụng. Bằng cách thêm negative keywords, bạn có thể giảm chi phí lãng phí cho các lượt tìm kiếm không phù hợp. Negative keywords có thể được thêm ở cấp ad group hoặc dưới dạng negative keywords xuyên nhóm áp dụng cho nhiều chiến dịch cùng lúc. ### Keywords dạng SKAG (Single Keyword Ad Group) \{#keywords-as-skag-single-keyword-ad-group\} SKAG (Single Keyword Ad Group) là chiến lược tạo các ad group riêng lẻ, mỗi ad group chứa một keyword duy nhất. Cách tiếp cận này cho phép bạn: - Kiểm soát chính xác giá thầu cho các keywords có giá trị cao - Phân tích hiệu suất tốt hơn ở cấp keyword SKAG đặc biệt hữu ích để xác định các keywords hoạt động tốt nhất và tối đa hóa tiềm năng của chúng thông qua các ad group chuyên biệt. ## Thêm từ khóa \{#add-keywords\} Để thêm từ khóa vào một nhóm quảng cáo: :::note Từ khóa trong các chiến dịch **Maximize Conversions** không sử dụng giá thầu — việc đặt giá thầu được quản lý tự động theo mục tiêu CPA của chiến dịch. Trường **CPT bid** không áp dụng cho các chiến dịch này. ::: 1. Vào **Ads Manager** từ menu sidebar. Ở bất kỳ tab nào, nhấp **+** phía trên bảng, sau đó chọn **Add keywords** từ menu thả xuống. 2. Trong cửa sổ modal, chọn các chiến dịch và nhóm quảng cáo mà bạn muốn thêm từ khóa. Sau khi chọn nhóm quảng cáo trong một chiến dịch, bạn có thể tiếp tục chọn chiến dịch khác và thêm nhiều nhóm quảng cáo hơn vào danh sách. 3. Nhấp vào **Select** để tiếp tục. 4. Trong hộp thoại **Add keywords**, nhập từ khóa vào trường **Keywords list**. Nếu bạn có file phân cách bằng dấu phẩy chứa từ khóa, bạn có thể dán nội dung của file đó để Adapty Ads Manager tải lên tất cả từ khóa cùng một lúc. 5. Với mỗi từ khóa trong bảng, hãy cấu hình: - **Match type**: Chọn kiểu khớp **Exact** hoặc **Broad** - **CPT bid**: Đặt mức giá thầu chi phí tối đa mỗi lần nhấp cho từ khóa này, hoặc để trống để sử dụng mức giá thầu CPT tối đa mặc định của nhóm quảng cáo 6. Xem lại các từ khóa của bạn và nhấp vào **Add X keywords** (trong đó X là số lượng từ khóa bạn đang thêm). :::important Sau khi lưu một keyword, bạn không thể thay đổi loại đối sánh của nó. Nếu cần thay đổi loại đối sánh, hãy xóa keyword đó và thêm lại với loại đối sánh mong muốn. ::: ## Thêm từ khóa phủ định \{#add-negative-keywords\} Để thêm từ khóa phủ định: 1. Vào **Ads Manager** từ menu thanh bên. Ở bất kỳ tab nào, nhấn **+** phía trên bảng, sau đó chọn **Add negative keywords** từ dropdown. 2. Trong modal **Add negative keywords to**, chọn cấp độ bạn muốn thêm từ khóa phủ định: - **Selected campaigns**: Thêm từ khóa phủ định ở cấp chiến dịch. - **Selected ad groups**: Thêm từ khóa phủ định ở cấp nhóm quảng cáo. - **All ad groups in selected campaigns**: Thêm từ khóa phủ định ở cấp nhóm quảng cáo cho tất cả các nhóm quảng cáo trong các chiến dịch đã chọn. :::note Lưu ý những điều sau: - Từ khóa phủ định ở cấp nhóm quảng cáo có độ ưu tiên cao hơn từ khóa phủ định ở cấp chiến dịch. - Nếu bạn thêm từ khóa phủ định cho tất cả các nhóm quảng cáo trong các chiến dịch đã chọn, bạn sẽ cần thêm thủ công nếu sau này quyết định thêm nhóm quảng cáo mới vào các chiến dịch đó. ::: 3. Nhập các từ khóa phủ định vào trường **Keywords list**. Nếu bạn có file chứa các từ khóa phân cách bằng dấu phẩy, bạn có thể dán nội dung đó vào để Adapty Ads Manager tải lên tất cả từ khóa cùng một lúc. 4. Với mỗi từ khóa trong bảng, chọn **Match type**: - **Exact**: Loại trừ chính xác từ khóa đó hoặc các biến thể rất gần. - **Broad**: Loại trừ từ khóa đó và các cụm tìm kiếm liên quan. Hoặc chọn checkbox bên cạnh các từ khóa và thay đổi match type theo nhóm. 5. Xem lại các từ khóa phủ định rồi nhấn **Add X keywords** (trong đó X là số lượng từ khóa bạn đang thêm). :::note Từ khóa phủ định xuyên nhóm đặc biệt hữu ích khi bạn muốn loại trừ một số cụm từ tìm kiếm trên nhiều chiến dịch cùng một lúc, tiết kiệm thời gian và đảm bảo tính nhất quán trong toàn bộ chiến lược quảng cáo của bạn. ::: ## Thêm từ khóa dưới dạng SKAG \{#add-keywords-as-skag\} Để thêm từ khóa dưới dạng SKAG (Single Keyword Ad Group): 1. Vào **Ads Manager** từ menu sidebar. Ở bất kỳ tab nào, nhấn **+** phía trên bảng, rồi chọn **Add keywords as SKAG** từ dropdown. 2. Chọn các chiến dịch mà bạn muốn tạo ad group SKAG. Bạn có thể chọn nhiều chiến dịch cùng lúc. 3. Mặc định, các ad group mới sẽ được tạo với cài đặt mặc định nhắm đến tất cả người dùng. Nếu muốn thay đổi, chọn **Copy settings from ad group** và chọn một ad group hiện có để sao chép cài đặt từ đó. 4. Cấu hình cài đặt cho các ad group mới: - **Ad group name prefix**: Tiền tố tùy chọn thêm vào tên mỗi ad group (ví dụ: "SKAG_" sẽ tạo ra "SKAG_keyword1", "SKAG_keyword2", v.v.). Bạn có thể nhấn **Tag** để tự động thêm từ khóa, tên chiến dịch và quốc gia vào tên nhóm. - **CPT bid** và **CPA cap**: Đặt giá thầu cho tất cả từ khóa cùng lúc, hoặc chọn **Set CPT bid and CPA cap for each word manually** để đặt riêng cho từng từ khóa. 5. Nhập từ khóa vào trường **Keywords list**. Nếu bạn có file phân cách bằng dấu phẩy chứa các từ khóa, bạn có thể dán nội dung của file đó để Adapty Ads Manager tải lên tất cả từ khóa cùng lúc. 6. Với mỗi từ khóa trong bảng, chọn **Match type**: - **Exact**: Chỉ khớp với đúng từ khóa đó hoặc các biến thể rất gần - **Broad**: Khớp với từ khóa và các cụm tìm kiếm liên quan Hoặc chọn checkbox bên cạnh chúng để thay đổi match type hàng loạt. 7. Chọn **Check for duplicates in target campaign** để đảm bảo không có từ khóa trùng lặp trong các chiến dịch mục tiêu. 8. Nhấn **Create** để tạo các ad group SKAG. Mỗi từ khóa sẽ được đặt vào nhóm quảng cáo riêng trong từng chiến dịch đã chọn, giúp bạn quản lý và tối ưu hóa chúng một cách độc lập. ## Chỉnh sửa từ khóa \{#edit-keywords\} Để chỉnh sửa từ khóa hiện có: 1. Vào **Ads Manager > Keywords** hoặc **Ads Manager > Negative keywords** và tìm từ khóa bạn muốn chỉnh sửa trong bảng, hoặc điều hướng đến trang chiến dịch, sau đó đến trang nhóm quảng cáo và tìm từ khóa. 2. Chỉnh sửa các giá trị trực tiếp trong bảng: - **CPT bid**: Nhấp vào giá trị bid và nhập mức chi phí tối đa mỗi lần nhấn mới - **Status**: Dùng nút chuyển đổi để tạm dừng hoặc kích hoạt từ khóa :::note Các chỉnh sửa từ khóa thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ sang Adapty Ads Manager, nhưng có thể mất một chút thời gian để hiển thị trong Adapty Ads Manager. ::: ## Hành động hàng loạt \{#bulk-actions\} Bạn có thể thực hiện hành động hàng loạt trên nhiều từ khóa cùng lúc để tiết kiệm thời gian và quản lý từ khóa hiệu quả hơn. Để thực hiện hành động hàng loạt: 1. Truy cập tab **Ads Manager > Keywords** hoặc **Ads Manager > Negative keywords**. 2. Chọn nhiều từ khóa bằng cách đánh dấu vào ô bên cạnh các từ khóa bạn muốn quản lý. 3. Nhấp vào dropdown **Actions** và chọn một trong các tùy chọn sau: - **Add as keywords**: Thêm các từ khóa đã chọn làm từ khóa tiêu chuẩn - **Add as negative keywords**: Thêm các từ khóa đã chọn làm từ khóa phủ định - **Add as SKAG**: Tạo Single Keyword Ad Groups cho các từ khóa đã chọn - **Activate**: Kích hoạt các từ khóa đã chọn - **Pause**: Tạm dừng các từ khóa đã chọn - **Create segment from keywords**: Tạo phân khúc đối tượng từ các từ khóa đã chọn - **Copy keywords**: Sao chép tên các từ khóa đã chọn vào clipboard - **Edit CPT bids**: Chỉnh sửa giá thầu CPT cho các từ khóa đã chọn. Bạn có thể chỉnh sửa theo các cách khác nhau: - **Set to**: Đặt nhiều giá thầu về một mức cụ thể. - **Increase by/decrease by**: Tăng hoặc giảm giá thầu theo một số tiền cụ thể tính bằng USD hoặc theo phần trăm giá thầu. Tùy chọn, đặt giới hạn giá thầu tối đa để tránh chi tiêu quá mức ngoài ý muốn. - **Set to average CPT**: Sử dụng chỉ số CPT (cost-per-tap) để điều chỉnh giá thầu phù hợp với nó. Đặt hệ số nhân. Ví dụ: đặt hệ số nhân là 0,9 khi hiệu suất thấp hơn kỳ vọng, hoặc 1,1 khi hiệu suất vượt kỳ vọng. - **Set to average CPA**: Sử dụng chỉ số CPA (cost-per-acquisition) để điều chỉnh giá thầu phù hợp với nó. Đặt hệ số nhân. Tab **Negative keywords** bao gồm một số hành động giới hạn: - **Add as keywords** - **Add as negative keywords** - **Add as SKAG** - **Delete keywords** đều khả dụng. :::tip Các hành động hàng loạt đặc biệt hữu ích cho: - Chuyển đổi từ khóa giữa các loại khác nhau (standard, negative, SKAG) - Nhanh chóng thêm từ khóa với các match type khác cho nhiều từ khóa - Lọc các từ khóa hoạt động tốt nhất và điều chỉnh giá thầu - Xác định các từ khóa hoạt động kém và tạm dừng chúng ::: ## Xuất từ khóa \{#export-keywords\} Để xuất bảng từ khóa dưới dạng CSV, nhấn vào biểu tượng tải xuống phía trên bảng và chọn **Export current page** hoặc **Export all pages**. **Export all pages** tải xuống tất cả từ khóa trên mọi trang trong một file duy nhất. Một cửa sổ tiến trình sẽ theo dõi quá trình tải xuống; bạn có thể hủy bất cứ lúc nào. Có hai bộ lọc tùy chọn: - **Enabled only**: Chỉ bao gồm các từ khóa đang hoạt động. - **With spend ≥**: Chỉ bao gồm các từ khóa có chi tiêu vượt ngưỡng đã chỉ định. - **Group by country**: Phân tách từng hàng từ khóa theo quốc gia. Bảng dữ liệu được xuất ra đúng như trên dashboard của bạn, với các cột bạn đã chọn hiển thị. ## Khám phá biểu đồ theo từ khóa \{#explore-keyword-level-charts\} Bạn có thể mở biểu đồ cho bất kỳ từ khóa nào trực tiếp từ bảng **Ads Manager > Keywords**. Tính năng này cho phép phân tích hiệu suất chi tiết theo từng ngày cho mỗi từ khóa riêng lẻ. Để hiển thị biểu đồ, nhấp vào biểu tượng biểu đồ bên cạnh từ khóa trong bảng. Theo mặc định, biểu đồ sẽ hiển thị chỉ số **Spend** cho từ khóa đã chọn. Bạn có thể hiển thị nhiều chỉ số cùng lúc để phát hiện các mối tương quan và thay đổi theo thời gian. Nhấp **+** để thêm chỉ số mới. Nhấn **Reset** để bắt đầu lại hoặc đơn giản là bỏ chọn các ô chỉ số để ẩn chúng đi. ## Lịch sử đặt giá thầu \{#bid-history\} Để xem lịch sử đặt giá thầu của một từ khóa, nhấp vào **biểu tượng biểu đồ** bên cạnh từ khóa đó trong bảng. Một bảng điều khiển sẽ mở ra với hai tab: **Metrics** và **Bid History**. - Tab **Metrics** hiển thị biểu đồ các chỉ số theo thời gian. Bạn có thể thêm hoặc bỏ chỉ số tương tự như trong biểu đồ ở cấp từ khóa — nhấn **+** để thêm, hoặc bỏ chọn checkbox để ẩn. Một điểm đánh dấu sẽ xuất hiện tại mỗi vị trí có thay đổi bid — di chuột qua điểm đó để xem mức bid cụ thể vào ngày đó. Dùng tính năng này để liên kết các lần thay đổi bid với sự biến động trong hiệu suất: nếu một chỉ số giảm hoặc tăng đột biến sau một lần thay đổi, điểm đánh dấu sẽ cho bạn biết chính xác thời điểm xảy ra. - Tab **Bid History** liệt kê từng lần thay đổi bid: ngày thay đổi, loại thay đổi, giá trị cũ và mới, cũng như nguyên nhân — thay đổi thủ công hay do quy tắc tự động (hiển thị kèm ID của quy tắc). --- # File: ads-manager-manage-ads --- --- title: "Quản lý quảng cáo trong Adapty Ads Manager" description: "Tạo và chỉnh sửa quảng cáo Apple Ads trong Adapty Ads Manager." --- Adapty Ads Manager tích hợp hai chiều với Apple Ads: bạn nhận được dữ liệu hiệu suất gần theo thời gian thực, đồng thời có thể tạo và chỉnh sửa quảng cáo trực tiếp từ Adapty Dashboard theo cách thuận tiện hơn nhiều so với giao diện gốc. Nếu bạn tạo quảng cáo trong dashboard Apple Ads gốc, quảng cáo đó sẽ tự động xuất hiện trong Adapty Ads Manager trong vòng 24 giờ. ## Quảng cáo là gì \{#what-are-ads\} Quảng cáo là một creative quảng cáo được gán cho một [nhóm quảng cáo](ads-manager-create-ad-group) bên trong một [chiến dịch](ads-manager-create-campaign). Bạn có thể gán một quảng cáo đang hoạt động cho mỗi nhóm quảng cáo. ## Tạo quảng cáo \{#create-ads\} Trước khi bắt đầu, hãy đảm bảo bạn đã tạo: - **Ad group**. Bạn có thể [tạo trực tiếp trong Adapty Ads Manager dashboard](ads-manager-create-ad-group). - **Custom product page**. Bạn cần [thiết lập trực tiếp trong Apple Ads](https://developer.apple.com/help/app-store-connect/create-custom-product-pages/configure-multiple-product-page-versions/). Trang này phải được App Store phê duyệt trước khi bạn có thể sử dụng trong quảng cáo. :::note Nếu bạn đã có quảng cáo đang chạy trong ad group đã chọn, quảng cáo đó sẽ bị tạm dừng để chạy quảng cáo mới. ::: Để tạo quảng cáo Apple Ads mới: 1. Vào **Ads Manager** từ menu thanh bên. Ở bất kỳ tab nào, nhấn **+** phía trên bảng và chọn **Create ad**. 2. Chọn ứng dụng bạn muốn chạy quảng cáo. 3. Chọn một hoặc nhiều nhóm quảng cáo. Adapty sẽ tạo quảng cáo trong từng nhóm đã chọn. 4. Nhập tên quảng cáo. 5. Đặt trạng thái quảng cáo. Tắt nút **Status** nếu bạn muốn chạy quảng cáo sau. 6. Nhấn **Select CPP**. Bạn sẽ thấy tất cả các trang sản phẩm tùy chỉnh của ứng dụng đã được App Store phê duyệt. Bạn chỉ có thể chọn một trang sản phẩm tùy chỉnh. 7. Nhấp vào **Create ad**. ## Chỉnh sửa quảng cáo \{#edit-ads\} :::note Sau khi tạo quảng cáo, bạn có thể chỉnh sửa tên và trạng thái của nó. Bạn không thể thay đổi CPP hoặc chuyển nó sang nhóm quảng cáo khác. ::: Để chỉnh sửa tên quảng cáo, sử dụng một trong các tùy chọn sau: - Nhấp vào tên quảng cáo trong **Ads Manager > Ads**. Chỉnh sửa tên quảng cáo và nhấp vào dấu kiểm tra bên cạnh. - Chọn hộp kiểm bên cạnh tên quảng cáo và nhấp vào **Actions > Edit ad**. Thay đổi tên hoặc trạng thái quảng cáo rồi nhấp **Save changes**. :::note Các chỉnh sửa đối với quảng cáo được thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ sang Adapty Ads Manager, nhưng có thể mất một khoảng thời gian để hiển thị trong Adapty Ads Manager. ::: ## Xuất dữ liệu quảng cáo \{#export-ads\} Để xuất bảng quảng cáo dưới dạng CSV, nhấp vào biểu tượng tải xuống phía trên bảng và chọn **Export current page** hoặc **Export all pages**. **Export all pages** tải xuống toàn bộ quảng cáo từ tất cả các trang vào một file duy nhất. Một cửa sổ tiến trình sẽ theo dõi quá trình tải; bạn có thể hủy bất cứ lúc nào. Có hai bộ lọc tùy chọn: - **Enabled only**: Chỉ bao gồm các quảng cáo đang hoạt động. - **With spend ≥**: Chỉ bao gồm các quảng cáo có chi tiêu vượt ngưỡng đã chỉ định. - **Group by country**: Phân tách từng hàng quảng cáo theo quốc gia. Bảng dữ liệu được xuất theo đúng giao diện trên dashboard của bạn, với các cột bạn đã chọn để hiển thị. ## Khởi chạy & tạm dừng quảng cáo \{#launch--pause-ads\} Để khởi chạy hoặc tạm dừng bất kỳ quảng cáo nào từ Adapty Ads Manager, sử dụng một trong các tùy chọn sau: - Bật hoặc tắt nút **Status** trong **Ads Manager > Ads**. - Chọn hộp kiểm cạnh tên quảng cáo, nhấn **Actions > Edit ad**, bật/tắt nút **Status**, rồi nhấn **Save changes**. --- # File: ads-manager-create-segments --- --- title: "Tạo phân khúc dựa trên attribution từ Apple Ads trong Adapty Ads Manager" description: "Tạo phân khúc từ các campaign, nhóm quảng cáo và từ khóa chỉ trong vài cú nhấp chuột trong Adapty Ads Manager." --- Bạn có thể tạo [phân khúc](segments) người dùng trực tiếp từ [Adapty Ads Manager](adapty-ads-manager) bằng cách chọn các chiến dịch, nhóm quảng cáo hoặc từ khóa rồi chuyển chúng thành phân khúc chỉ trong vài bước. Điều này giúp dễ dàng cá nhân hóa paywall và ưu đãi dựa trên nguồn acquisition mà không cần thiết lập thủ công các điều kiện phân khúc. Sau khi tạo phân khúc, bạn có thể dùng nó để gán các sản phẩm và mức giá khác nhau, chạy A/B test, và tùy chỉnh giao diện paywall. ## Các trường hợp sử dụng \{#use-cases\} Dưới đây là một số ví dụ về cách sử dụng các phân khúc được tạo từ dữ liệu Apple Ads trong thực tế: - **Paywall theo từ khóa**. Hiển thị paywall tập trung vào tính năng cho người dùng đến từ các từ khóa có ý định cao, và paywall tổng quát cho người dùng đến từ các từ khóa khám phá rộng hơn. - **Ưu đãi theo chiến dịch**. Cung cấp thời gian dùng thử dài hơn hoặc giá đặc biệt cho người dùng đến từ các chiến dịch Apple Ads được chọn, trong khi giữ ưu đãi tiêu chuẩn cho những người dùng khác. - **Nhất quán giữa quảng cáo và paywall**. Điều hướng người dùng đến từ các nhóm quảng cáo quảng bá tính năng cụ thể đến các paywall làm nổi bật những tính năng đó trước tiên. - **Tối ưu hóa chiến dịch ROI cao**. Hiển thị paywall cao cấp với giá đầy đủ cho những người dùng đến từ các chiến dịch thường xuyên mang lại giá trị trọn đời cao hơn. ## Tạo phân khúc \{#create-segments\} Để tạo phân khúc từ Adapty Ads Manager: 1. Vào **Ads Manager** và chuyển sang tab **Campaigns**, **Ad groups**, hoặc **Keywords**. Chọn hộp kiểm bên cạnh các mục bạn muốn sử dụng. Lưu ý rằng nếu bạn chọn nhiều mục, tất cả chúng sẽ được dùng để tạo một phân khúc duy nhất thay vì mỗi mục một phân khúc riêng. 2. Nhấn **Actions > Create segment from campaigns/ad groups/keywords**. 3. Nếu cần, cập nhật thông tin phân khúc trong cửa sổ **Create segment**: - **Adapty project**: App trong Adapty mà bạn muốn tạo phân khúc này. - **Build segment from campaign/ad group**: Khi tạo phân khúc từ campaign hoặc ad group, bạn có thể điều chỉnh các campaign hoặc ad group đã chọn ở bước này. - **Segment name** - **Segment description** 4. Nhấn **Create**. 5. Sau khi phân khúc được tạo, bạn có thể chuẩn bị để sử dụng nó: - Thêm vào [placement](placements) để dùng với paywall hoặc onboarding hiện có - Thiết kế [paywall](adapty-paywall-builder) hoặc [onboarding](onboardings) mới để hiển thị cho người dùng trong phân khúc - Chạy [A/B test](ab-tests) --- # File: ads-manager-automations-keyword-rules --- --- title: "Quy tắc từ khóa trong Adapty Ads Manager" description: "Tự động quản lý vòng đời từ khóa — điều chỉnh giá thầu, bật hoặc tạm dừng từ khóa, và di chuyển chúng giữa các nhóm quảng cáo — dựa trên hiệu suất chiến dịch." --- Quy tắc từ khóa tự động hành động trên các từ khóa của bạn dựa trên hiệu suất toàn phễu — từ lượt cài đặt đến trial, gói đăng ký và doanh thu. Xác định điều kiện bằng các chỉ số như chi tiêu, CPA, ROAS và dữ liệu cohort, sau đó chọn hành động mà quy tắc thực hiện khi các điều kiện đó được đáp ứng. Quy tắc chạy theo lịch bạn đặt. Chúng phản ứng với các thay đổi hiệu suất mà không cần can thiệp thủ công. ## Các hành động có sẵn \{#available-actions\} Mỗi quy tắc từ khóa thực hiện một hành động khi điều kiện của nó được đáp ứng: | Hành động | Mô tả | |--------|-------------| | **Change bid** | Tăng, giảm hoặc đặt giá thầu CPT | | **Enable keyword** | Kích hoạt lại từ khóa đang tạm dừng | | **Pause keyword** | Tạm dừng từ khóa đang hoạt động | | **Add as keyword to…** | Sao chép từ khóa sang nhóm quảng cáo khác với giá thầu và loại đối sánh được chỉ định | | **Add as negative keyword to…** | Thêm từ khóa dưới dạng từ khóa phủ định vào các nhóm quảng cáo hoặc chiến dịch được chỉ định | ## Tạo quy tắc từ khóa \{#create-a-keyword-rule\} Bạn có thể tạo quy tắc từ khóa từ mẫu có sẵn hoặc tạo thủ công từ đầu. ### Từ mẫu có sẵn \{#from-a-template\} Adapty cung cấp các mẫu có sẵn cho các tình huống tối ưu hóa phổ biến. Một số mẫu thường gặp bao gồm: - **Cắt giảm lãng phí trên các từ khóa không chuyển đổi**: Giảm giá thầu khi Chi tiêu > X và Lượt cài đặt hoặc Dùng thử = 0. - **Mở rộng quy mô các từ khóa hiệu quả**: Tăng giá thầu khi ROAS > mục tiêu hoặc CPA < mục tiêu. Để tạo một quy tắc từ mẫu có sẵn: 1. Trong thanh bên trái, vào **Automations** và nhấn **Templates**. 2. Chọn một template và nhấn **Next**. 3. Xem lại và điều chỉnh các cài đặt được điền sẵn: - **Rule name**: Tự động đặt theo tên template và ngày hiện tại (ví dụ: "Scale Winning Keywords - [2025-11-12]"). - **Apply to**: Chọn các nhóm chiến dịch, ứng dụng, chiến dịch hoặc nhóm quảng cáo mà quy tắc sẽ áp dụng. - **Conditions**: Chỉnh sửa các điều kiện được cấu hình sẵn nếu cần. - **Action**: Chỉnh sửa hành động được đặt sẵn nếu cần. - **Schedule**: Thiết lập tần suất chạy của quy tắc. 4. Nhấn **Save** để kích hoạt quy tắc. ### Thủ công \{#manually\} Để tạo quy tắc keyword tùy chỉnh từ đầu: 1. Trong thanh sidebar bên trái, đi đến **Automations**, nhấp vào **Create rule** và chọn **Keywords** làm loại quy tắc. 2. Nhập **Rule name** mô tả rõ ràng. 3. Trong phần **Apply to**, chọn các nhóm chiến dịch, ứng dụng, chiến dịch hoặc nhóm quảng cáo mà quy tắc sẽ áp dụng. 4. Nhấp vào **Add condition** và chọn một [chỉ số](adapty-ads-manager-metrics) từ danh sách. Các chỉ số được tính toán cho khoảng thời gian đã chọn theo đơn vị tiền tệ trong tài khoản của bạn. Dữ liệu được cập nhật gần theo thời gian thực, vì vậy các quy tắc luôn sử dụng dữ liệu hiệu suất mới nhất. 5. Đặt khoảng thời gian (ví dụ: 3 ngày trước hoặc 7 ngày trước), chọn toán tử so sánh và nhập giá trị ngưỡng. 6. Để thêm nhiều điều kiện hơn, nhấp vào **Add condition** và chọn toán tử **And** hoặc **Or** ở bên trái. 7. Trong phần **Action**, chọn hành động xảy ra khi các điều kiện được thỏa mãn: **Thay đổi bid** - **Loại hành động**: Chọn **Increase by**, **Decrease by**, hoặc **Set to**. - **Loại giá trị**: Chuyển đổi giữa **$** (giá trị tuyệt đối) và **%** (tương đối so với bid hiện tại tại thời điểm quy tắc chạy). - **Giới hạn bid tối đa** (tùy chọn): Mức cap bid tối đa để tránh overbid nếu quy tắc kích hoạt nhiều lần trên các tín hiệu mạnh. **Bật keyword** - Không cần cấu hình thêm. Quy tắc sẽ bật lại các keyword đang tạm dừng đáp ứng các điều kiện. **Tạm dừng keyword** - Không cần cấu hình thêm. Quy tắc sẽ tạm dừng các keyword đang hoạt động đáp ứng các điều kiện. **Thêm làm từ khóa vào…** - **Target ad groups**: Chọn các nhóm quảng cáo nhận từ khóa được sao chép. - **CPT bid**: Đặt mức giá thầu ban đầu cho các từ khóa được sao chép. - **Match type**: Chọn **Exact** hoặc **Broad**. - **Skip if keyword already exists**: Khi bật, bỏ qua các từ khóa đã có trong nhóm quảng cáo đích. **Thêm làm từ khóa phủ định vào…** - **Scope**: Chọn các nhóm quảng cáo hoặc chiến dịch nơi từ khóa phủ định được thêm vào. - **Match type**: Chọn **Exact** hoặc **Broad**. 8. Trong phần **Schedule**: - Chọn tần suất: **Every day**, **Every 2 days**, **Every week**, và các tùy chọn tương tự. - Chọn thời gian chạy (tất cả thời gian theo UTC). Các quy tắc sẽ chạy vào thời gian đã lên lịch theo UTC. Quá trình thực thi thường hoàn tất trong vài phút, sau đó bạn có thể xem các thay đổi trong Logs và trên dashboard chính. 9. Nhấn **Save** để tạo quy tắc. ## Các phương pháp tốt nhất \{#best-practices\} - **Bắt đầu với phạm vi hẹp**: Áp dụng các quy tắc mới cho một vài chiến dịch hoặc nhóm quảng cáo trước để kiểm tra hành vi, rồi mới mở rộng. - **Dùng cửa sổ lookback ngắn cho các chiến dịch đang chạy mạnh**: Với các chiến dịch biến động nhanh, 3–7 ngày trước thường hiệu quả hơn 30 ngày. - **Kết hợp chi tiêu và lượt chuyển đổi**: Tránh dùng quy tắc chỉ dựa trên một chỉ số. Kết hợp Spend với Installs, Trials hoặc ROAS để có tín hiệu đáng tin cậy hơn. - **Đặt giới hạn bid cho các quy tắc Change bid**: Giới hạn bid trên giúp tránh trường hợp bid tăng mất kiểm soát khi một tín hiệu mạnh kích hoạt quy tắc nhiều lần. - **Dùng Enable keyword cùng với dữ liệu cohort**: Một keyword bị tạm dừng sớm vì CPA ban đầu kém có thể cho thấy ROAS D31 hoặc D61 tốt khi dữ liệu cohort trưởng thành hơn. Đặt điều kiện dựa trên cohort ROAS và tự động kích hoạt lại khi nó vượt ngưỡng mục tiêu. - **Dùng Add as keyword to… cho quy trình từ thử nghiệm đến mở rộng**: Khi một keyword trong chiến dịch thử nghiệm đạt mục tiêu CPA, tự động sao chép nó sang chiến dịch scale. - **Dùng Add as negative keyword to… để giữ các chiến dịch Discovery gọn gàng**: Khi một keyword đã được xác nhận là từ khóa exact-match, hãy loại trừ nó trong các chiến dịch Discovery hoặc Search Match để tránh cạnh tranh cùng một truy vấn. - **Chờ trước khi các quy tắc bid tác động lên keyword vừa được thêm vào**: Nếu bạn dùng [tự động hóa search term](ads-manager-automations-search-terms) để đưa các từ khóa vào chiến dịch keyword, hãy để các keyword đó tích lũy dữ liệu trong một hoặc hai ngày trước. --- # File: ads-manager-automations-search-terms --- --- title: "Tự động hóa từ khóa tìm kiếm trong Adapty Ads Manager" description: "Tự động đưa các từ khóa tìm kiếm hiệu quả lên thành từ khóa và loại trừ chúng tại nguồn để mở rộng lưu lượng khám phá mà không cần thao tác thủ công" --- Discovery và Search Match campaign tạo ra dữ liệu từ khóa tìm kiếm. Để biến dữ liệu đó thành danh sách từ khóa có cấu trúc, bạn phải tải báo cáo xuống, lọc các từ, rồi thêm thủ công vào ad group. Search term automation làm việc này tự động: khi một từ đáp ứng điều kiện bạn đặt ra, rule sẽ xử lý nó theo hành động bạn cấu hình. Có hai loại hành động cho search term rule: - **Add as keyword**: Thêm từ khóa vào nhóm quảng cáo đích dưới dạng từ khóa khớp chính xác, và tùy chọn loại trừ nó ở chiến dịch nguồn để tránh chi tiêu trùng lặp. - **Add as negative keyword**: Loại trừ từ khóa trực tiếp mà không thêm vào nhóm quảng cáo nào. Dùng tính năng này để lọc bỏ các từ khóa tìm kiếm không liên quan hoặc lãng phí từ các chiến dịch Discovery và Search Match. Trường hợp sử dụng điển hình cho **Add as keyword**: để các chiến dịch Discovery hoặc Search Match thu thập các truy vấn thực tế từ người dùng, sau đó dùng một rule để phát hiện những từ khóa vượt ngưỡng hiệu suất và đẩy chúng vào chiến dịch Probing dưới dạng exact-match keyword — đồng thời loại trừ chúng khỏi nguồn ban đầu. Probing campaign là một chiến dịch Apple Search Ads dành riêng để kiểm thử các từ khóa được quảng bá ở mức bid kiểm soát. Trường hợp sử dụng điển hình cho **Add as negative keyword**: nếu một từ khóa xuất hiện thường xuyên nhưng không bao giờ chuyển đổi (ví dụ: lượt hiển thị cao nhưng lượt tap bằng không), hãy tự động loại trừ nó để không lãng phí ngân sách. ## Tạo quy tắc automation từ khóa tìm kiếm \{#create-a-search-term-automation-rule\} Bạn có thể tạo quy tắc automation từ khóa tìm kiếm từ template hoặc tạo thủ công từ đầu. :::note Trước khi tạo quy tắc, hãy đảm bảo bạn đang chạy các chiến dịch Discovery hoặc Search Match đang hoạt động và thu thập dữ liệu từ khóa tìm kiếm. Các chiến dịch chỉ dùng exact match không tạo ra báo cáo từ khóa tìm kiếm, vì vậy quy tắc sẽ không có gì để xử lý. ::: ### Từ một mẫu \{#from-a-template\} Để tạo một quy tắc từ mẫu: 1. Trong thanh bên trái, vào **Automations** và nhấp vào **Templates**. 2. Chọn một template và nhấp **Next**. 3. Xem xét và điều chỉnh các cài đặt được điền sẵn: - **Rule name**: Tự động đặt theo tên template và ngày hiện tại. - **Apply to**: Chọn nhóm chiến dịch, ứng dụng, chiến dịch hoặc nhóm quảng cáo mà rule sẽ tìm kiếm search term. - **Conditions**: Chỉnh sửa các điều kiện đã được cấu hình sẵn nếu cần. - **Actions**: Điều chỉnh nhóm quảng cáo mục tiêu, CPT bid và phạm vi negative keyword nếu cần. - **Schedule**: Đặt tần suất chạy của rule. 4. Nhấp **Save** để kích hoạt rule. ### Thủ công \{#manually\} Để tạo quy tắc tự động hóa từ khóa tìm kiếm tùy chỉnh từ đầu: 1. Trong thanh bên trái, vào **Automations**, nhấn **Create rule**, rồi chọn **Search terms** làm loại rule. 2. Nhập **Rule name** mô tả rõ mục đích của rule. 3. Trong phần **Apply to**, chọn nhóm chiến dịch, ứng dụng, chiến dịch hoặc nhóm quảng cáo mà rule sẽ tìm kiếm search terms. 4. Nhấn **Add condition** và chọn một [chỉ số](adapty-ads-manager-metrics) từ danh sách. Các chỉ số được tính toán theo khoảng thời gian đã chọn bằng đơn vị tiền tệ trong tài khoản của bạn. Dữ liệu được cập nhật gần như theo thời gian thực, vì vậy các quy tắc luôn sử dụng dữ liệu hiệu suất mới nhất. 5. Đặt khoảng thời gian (ví dụ: 3 ngày trước hoặc 7 ngày trước), chọn toán tử so sánh và nhập giá trị ngưỡng. 6. Để thêm điều kiện khác, nhấp **Add condition** và chọn toán tử **And** hoặc **Or** ở bên trái. 7. Trong phần **Action**, chọn hành động xảy ra khi một cụm từ tìm kiếm đáp ứng các điều kiện: **Thêm làm từ khóa** Thêm các cụm tìm kiếm khớp làm từ khóa exact-match vào nhóm quảng cáo đích. - **Target ad groups**: Chọn các nhóm quảng cáo nhận từ khóa được quảng bá. Để xây dựng quy trình khám phá, hãy chọn các nhóm quảng cáo trong chiến dịch Probing hoặc chiến dịch có cấu trúc khác. - **CPT bid**: Đặt giá thầu cost-per-tap ban đầu cho từng từ khóa được quảng bá. Các tùy chọn: giá thầu mặc định của nhóm quảng cáo, CPT hiện tại của cụm từ tìm kiếm, hoặc một giá trị cụ thể. - **Skip if keyword already exists**: Khi được bật, bỏ qua các cụm từ đã có trong nhóm quảng cáo đích. - **Add as negative**: Thêm các cụm từ tương tự dưới dạng từ khóa phủ định để tránh trả tiền cho cùng một lượng truy cập hai lần. - **Scope**: Chọn các nhóm quảng cáo nơi từ khóa phủ định được thêm vào. :::tip Bật **Add as negative** trong cùng một quy tắc — thêm từ khóa vào chiến dịch có cấu trúc và loại trừ nó ở nguồn trong một bước. Điều này giúp các chiến dịch Discovery của bạn gọn gàng và tự động xây dựng phễu từ khóa. ::: **Add as negative keyword** Loại trừ cụm từ tìm kiếm khớp mà không thêm nó vào chiến dịch. - **Scope**: Chọn các nhóm quảng cáo hoặc chiến dịch nơi từ khóa phủ định được thêm vào. - **Match type**: Chọn **Exact** hoặc **Broad**. Dùng hành động này để loại trừ các cụm từ tìm kiếm không liên quan hoặc có chất lượng thấp khỏi các chiến dịch Discovery và Max Conversion. Ví dụ: nếu một từ khóa có 50+ lần hiển thị và 0 lần nhấn, hãy tự động loại trừ nó. 8. Trong phần **Schedule**: - Chọn tần suất: **Every day**, **Every 2 days**, **Every week**, v.v. - Chọn thời gian chạy (tất cả thời gian đều theo UTC). Các quy tắc chạy theo lịch đã đặt theo giờ UTC. Quá trình thực thi thường hoàn tất trong vài phút, sau đó bạn có thể xem thay đổi trong Logs và trên dashboard chính. 9. Nhấp **Save** để tạo quy tắc. Sau khi quy tắc chạy, vào **Automations** → **Logs** và mở mục tương ứng với quy tắc của bạn. Một lần chạy thành công sẽ liệt kê từng từ khóa tìm kiếm được đánh giá cùng với chiến dịch nguồn, nhóm quảng cáo đích, kết quả hành động và kết quả loại trừ. Nếu không có từ khóa nào xuất hiện, nghĩa là các điều kiện chưa được đáp ứng — hãy xem lại ngưỡng của bạn hoặc mở rộng khoảng thời gian lookback. ## Thực hành tốt nhất \{#best-practices\} - **Dùng Discovery hoặc Search Match campaign làm nguồn**: Các loại campaign này thu thập truy vấn thực từ người dùng, cung cấp cho quy tắc của bạn một lượng lớn cụm từ tìm kiếm để đánh giá. - **Khớp ngưỡng với khoảng thời gian nhìn lại**: Hai lượt tải trở lên trong 7 ngày là điểm khởi đầu hợp lý. Khoảng thời gian dài hơn (14–30 ngày) hạ thấp ngưỡng hiệu quả — các cụm từ có thể đạt yêu cầu dù chuyển đổi không thường xuyên. Với ứng dụng có lượng truy cập lớn, hãy rút ngắn khoảng thời gian và nâng ngưỡng lên. - **Luôn loại trừ ở nguồn khi bạn thăng cấp**: Nếu bạn thêm một cụm từ làm từ khóa vào Probing campaign mà không loại trừ nó khỏi Discovery, cả hai campaign sẽ cạnh tranh cho cùng một truy vấn. Hãy bật **Add as negative** trong cùng quy tắc đó. - **Chọn ad group đích một cách có chủ đích**: Chuyển các cụm từ được thăng cấp vào một Probing ad group cụ thể thay vì một campaign rộng. Điều này giữ cấu trúc từ khóa gọn gàng và giúp việc phân tích hiệu suất dễ dàng hơn. - **Xem lại log sau mỗi lần chạy**: Kiểm tra tab Logs để xác nhận những cụm từ nào đã được thăng cấp và được đặt ở đâu. Ban đầu, hãy chạy quy tắc thủ công sau khi thiết lập để xác nhận nó hoạt động đúng như mong đợi. Xem [Automations](ads-manager-automations#explore-logs) để biết cách đọc log. - **Cho các từ khóa được thăng cấp thời gian trước khi keyword rule tác động lên chúng**: Nếu bạn dùng keyword rule trong cùng campaign, hãy loại trừ các từ khóa vừa được thăng cấp hoặc chờ một đến hai ngày trước khi rule chạy trên chúng. Một keyword rule có thể kích hoạt với một cụm từ mới hoàn toàn chưa có lịch sử hiệu suất và cắt giảm giá thầu trước khi nó chuyển đổi. - **Dùng Add as negative keyword cho các cụm từ có nhiều impression nhưng không có lượt tap**: Discovery và Max Conversion campaign thường hiển thị nhiều cụm từ tìm kiếm không liên quan. Một quy tắc với điều kiện "Impressions > 50 AND Taps = 0" sẽ tự động phát hiện và loại trừ chúng trước khi chúng tích lũy thêm impression lãng phí. ## Xuất từ khóa tìm kiếm \{#export-search-terms\} Để xuất bảng từ khóa tìm kiếm dưới dạng CSV, nhấp vào biểu tượng tải xuống phía trên bảng và chọn **Export current page** hoặc **Export all pages**. **Export all pages** tải tất cả từ khóa tìm kiếm trên tất cả các trang vào một file duy nhất. Một modal tiến trình theo dõi quá trình tải xuống; bạn có thể hủy bất cứ lúc nào. Bảng được xuất đúng như hiển thị trên dashboard của bạn, với các cột bạn đã chọn hiển thị. --- # File: ads-manager-automations-ad-group-rules --- --- title: "Quy tắc nhóm quảng cáo trong Adapty Ads Manager" description: "Tự động điều chỉnh giá thầu và mục tiêu CPA của nhóm quảng cáo, đồng thời bật hoặc tạm dừng nhóm quảng cáo dựa trên hiệu suất chiến dịch." --- Thêm **quy tắc nhóm quảng cáo** khi bạn muốn cài đặt của một nhóm quảng cáo tự động thay đổi dựa trên hiệu suất của nó. Khác với quy tắc từ khóa — vốn tác động lên từng từ khóa riêng lẻ — quy tắc nhóm quảng cáo thay đổi toàn bộ nhóm quảng cáo cùng một lúc. Mỗi quy tắc kết hợp một điều kiện với một hành động. Ví dụ: nếu một nhóm quảng cáo chi hơn $50 trong ba ngày mà không có trial nào, hãy giảm bid xuống 20%. Adapty kiểm tra các nhóm quảng cáo của bạn theo lịch bạn đặt — hàng giờ, hàng ngày, hàng tuần, v.v. — và áp dụng hành động mỗi khi điều kiện được đáp ứng. Và vì Adapty theo dõi các trial, gói đăng ký và doanh thu, điều kiện của bạn có thể phản hồi theo doanh thu thực tế, không chỉ là số lượt cài đặt. ## Điều kiện khả dụng \{#available-conditions\} Một rule theo dõi một tập hợp các nhóm quảng cáo và kích hoạt khi hiệu suất của chúng vượt qua ngưỡng bạn đặt. Đầu tiên, hãy chọn nhóm quảng cáo nào sẽ được theo dõi: - **Ad groups in selected campaign groups** - **Ad groups in selected apps** - **Ad groups in selected campaigns** - **Selected ad groups** Sau đó, đặt điều kiện kích hoạt rule. Mỗi điều kiện kết hợp các thành phần sau: | Phần | Mô tả | Ví dụ | | --- | --- | --- | | **Metric** | Bất kỳ [chỉ số nào mà Adapty Ads Manager theo dõi](adapty-ads-manager-metrics) — chi tiêu, lượt cài đặt, trial, gói đăng ký, doanh thu, và nhiều hơn nữa. | Spend | | **Time window** | Khoảng thời gian đo lường chỉ số. | Previous 3 days | | **Comparison** | Cách chỉ số được kiểm tra so với giá trị của bạn. | is greater than | | **Threshold** | Giá trị dùng để so sánh. | $50 | Kết hợp nhiều điều kiện với **And** hoặc **Or** để nhắm mục tiêu chính xác hơn — ví dụ: spend > $50 **and** trials = 0. ## Các hành động có sẵn và cài đặt của chúng \{#available-actions-and-their-settings\} Khi một nhóm đáp ứng điều kiện của bạn, Adapty có thể thay đổi cài đặt bằng cách kích hoạt một trong các hành động sau: | Hành động | Chức năng | Khi nào dùng | Cấu hình | | --- | --- | --- | --- | | **Change default bid** | Tăng, giảm hoặc đặt **mức đặt giá CPT (cost-per-tap) mặc định** của nhóm quảng cáo — mức tối đa bạn trả cho một lần nhấn vào quảng cáo. | Đẩy mạnh các nhóm quảng cáo có chuyển đổi tốt; kéo giảm các nhóm không hiệu quả. | **Action type**: Increase by, Decrease by, hoặc Set to.<br/>**Value type**: $ (tuyệt đối) hoặc % (so với giá thầu hiện tại).<br/>**Limit** (tùy chọn): Giới hạn trên khi tăng, hoặc giới hạn dưới khi giảm — giới hạn mức thay đổi qua các lần chạy lặp lại. | | **Change CPA goal** | Tăng, giảm hoặc đặt **mục tiêu CPA (cap)** của nhóm quảng cáo — chi phí mục tiêu cho mỗi lượt chuyển đổi. | Siết chặt ngưỡng chi phí khi tối ưu, hoặc nới lỏng để thu về nhiều lượt hơn. | **Action type**: Increase by, Decrease by, hoặc Set to.<br/>**Value type**: $ hoặc %.<br/>**Limit** (tùy chọn): Giới hạn trên khi tăng, hoặc giới hạn dưới khi giảm. | | **Enable ad group** | Kích hoạt lại một nhóm quảng cáo đã tạm dừng. | Khôi phục các nhóm quảng cáo hoạt động tốt trở lại sau khi doanh thu từ trial và gói đăng ký chậm đã về đủ. | Không có. | | **Pause ad group** | Tạm dừng một nhóm quảng cáo đang hoạt động. | Dừng các nhóm quảng cáo liên tục tiêu tiền nhưng không có chuyển đổi. | Không có. | ## Tạo quy tắc nhóm quảng cáo \{#create-an-ad-group-rule\} 1. Vào **Automations**, nhấn **Create rule**, rồi chọn **For ad groups**. 2. Nhập **Rule name**. 3. Trong **Apply to**, chọn một [phạm vi](#available-conditions) và chọn các ad group. 4. Trong **Conditions**, thêm một hoặc nhiều [điều kiện](#available-conditions). Bạn có thể kết hợp các điều kiện bằng toán tử logic. 5. Trong **Action**, chọn một [hành động](#available-actions-and-their-settings) và thiết lập các tùy chọn của nó. 6. Trong **Schedule**, đặt tần suất chạy rule và thời gian bắt đầu (UTC), hoặc chọn **Run immediately**. 7. Nhấn **Save**. Sau khi lưu, quy tắc sẽ xuất hiện trong tab **Automations**. Các cột **Date last run** và **Date next run** theo dõi lịch chạy của nó. Để xác nhận các thay đổi trạng thái qua từng lần chạy, hãy mở **Automations > Logs**. Xem [Automations](ads-manager-automations) để biết hướng dẫn về cách tạm dừng, nhân bản, xóa quy tắc hoặc chạy ngay lập tức. --- # File: ads-manager-automations-campaign-rules --- --- title: "Quy tắc chiến dịch trong Adapty Ads Manager" description: "Tự động điều chỉnh ngân sách hàng ngày và bật hoặc tạm dừng chiến dịch dựa trên hiệu suất." --- Thêm **quy tắc chiến dịch** khi bạn muốn ngân sách hoặc trạng thái của một chiến dịch tự động thay đổi dựa trên hiệu suất của nó. Khác với quy tắc nhóm quảng cáo chỉ tác động lên một nhóm quảng cáo duy nhất, quy tắc chiến dịch sẽ thay đổi toàn bộ chiến dịch cùng một lúc. Mỗi rule kết hợp một điều kiện với một hành động. Ví dụ: nếu một chiến dịch chi tiêu hơn $200 trong ba ngày mà không có gói đăng ký nào, hãy giảm ngân sách hàng ngày xuống 20%. Adapty kiểm tra các chiến dịch của bạn theo lịch bạn đặt — hàng giờ, hàng ngày, hàng tuần, v.v. — và áp dụng hành động bất cứ khi nào điều kiện được đáp ứng. Và vì Adapty theo dõi các trial, gói đăng ký và doanh thu, điều kiện của bạn có thể phản hồi dựa trên doanh thu thực tế, không chỉ số lượt cài đặt. ## Điều kiện có sẵn \{#available-conditions\} Một quy tắc theo dõi một tập hợp các chiến dịch và kích hoạt khi hiệu suất của chúng vượt qua ngưỡng bạn đặt. Trước tiên, hãy chọn những chiến dịch mà quy tắc sẽ theo dõi: - **Campaigns in selected campaign groups** - **Campaigns in selected apps** - **Selected campaigns** Sau đó, thiết lập điều kiện kích hoạt quy tắc. Mỗi điều kiện kết hợp các thành phần sau: | Phần | Mô tả | Ví dụ | | --- | --- | --- | | **Chỉ số** | Bất kỳ [chỉ số nào mà Adapty Ads Manager theo dõi](adapty-ads-manager-metrics) — chi tiêu, lượt cài đặt, dùng thử, gói đăng ký, doanh thu, v.v. | Spend | | **Khoảng thời gian** | Khoảng thời gian để đo chỉ số. | Yesterday | | **So sánh** | Cách chỉ số được kiểm tra với giá trị của bạn. | is greater than | | **Ngưỡng** | Giá trị để so sánh — một con số cố định, hoặc **Daily Budget** của chiến dịch. | Daily Budget | Kết hợp nhiều điều kiện với **And** hoặc **Or** để nhắm mục tiêu chính xác hơn — ví dụ: spend > Daily Budget **and** ROAS < 100%. ## Các hành động có thể thực hiện và cài đặt của chúng \{#available-actions-and-their-settings\} Khi một chiến dịch đáp ứng điều kiện của bạn, Adapty có thể thay đổi cài đặt của chiến dịch đó bằng cách kích hoạt một trong các hành động sau: | Hành động | Tác dụng | Khi nào dùng | Cấu hình | | --- | --- | --- | --- | | **Change daily budget** | Tăng, giảm hoặc đặt **ngân sách hàng ngày** của chiến dịch — mức tối đa có thể chi tiêu mỗi ngày. | Cắt giảm ngân sách cho các chiến dịch tiêu vượt mức; tăng ngân sách cho các chiến dịch có lợi nhuận tốt. | **Action type**: Increase by, Decrease by, hoặc Set to.<br/>**Value type**: $ (tuyệt đối) hoặc % (so với ngân sách hiện tại).<br/>**Limit** (tùy chọn): Giới hạn trên khi tăng, hoặc giới hạn dưới khi giảm — kiểm soát mức độ thay đổi ngân sách qua mỗi lần chạy. | | **Enable campaign** | Bật lại chiến dịch đang tạm dừng. | Khôi phục các chiến dịch hồi phục khi doanh thu từ trial và gói đăng ký muộn bắt đầu về. | Không có. | | **Pause campaign** | Tạm dừng chiến dịch đang hoạt động. | Dừng các chiến dịch tiếp tục tiêu tiền mà không chuyển đổi được. | Không có. | ## Tạo quy tắc chiến dịch \{#create-a-campaign-rule\} Để bắt đầu từ một mẫu có sẵn, nhấp vào **Templates** trong thanh tiêu đề **Automations** và chọn **Decrease budget for low-performing campaigns**, sau đó xem lại và lưu. Để tự tạo quy tắc từ đầu: 1. Vào **Automations**, nhấn **Create rule**, rồi chọn **For campaigns**. 2. Nhập **Rule name**. 3. Trong phần **Apply to**, chọn một [phạm vi](#available-conditions) và chọn các campaign. 4. Trong phần **Conditions**, thêm một hoặc nhiều [điều kiện](#available-conditions). Bạn có thể kết hợp các điều kiện bằng toán tử logic. 5. Trong phần **Action**, chọn một [hành động](#available-actions-and-their-settings) và cấu hình các tùy chọn của nó. 6. Trong phần **Schedule**, đặt tần suất chạy của rule và thời gian bắt đầu (UTC), hoặc chọn **Run immediately**. 7. Nhấn **Save**. Sau khi lưu, quy tắc sẽ xuất hiện trong tab **Automations**. Các cột **Date last run** và **Date next run** theo dõi lịch chạy của quy tắc. Để xác nhận các thay đổi trạng thái qua từng lần chạy, hãy mở **Automations > Logs**. Xem [Automations](ads-manager-automations) để biết hướng dẫn tạm dừng, nhân bản, xóa quy tắc hoặc chạy ngay lập tức. --- # File: ads-manager-market-intelligence --- --- title: "Market Intelligence trong Adapty Ads Manager" description: "Xem các từ khóa mà đối thủ cạnh tranh của bạn đang chạy Apple Ads trên 50+ quốc gia, và thêm trực tiếp vào chiến dịch của bạn." --- Market Intelligence cho bạn thấy những từ khóa mà đối thủ cạnh tranh đang đặt thầu trên Apple Ads, trên 50+ quốc gia. Dữ liệu được tổng hợp từ 30 ngày gần nhất và cập nhật hàng ngày. Dùng để: - **Bỏ qua chiến dịch Discovery**: Xem ngay những từ khóa mà đối thủ đang đặt giá thầu thay vì tốn ngân sách để tìm hiểu. Bắt đầu với danh sách từ khóa đã được kiểm chứng ngay từ ngày đầu tiên. - **Tìm từ khóa ít cạnh tranh**: Xác định các từ khóa đuôi dài mà đối thủ có Share of Voice thấp — ít cạnh tranh hơn, chi phí mỗi lượt nhấn thấp hơn, và CPA tốt hơn. - **Bảo vệ thương hiệu**: Xem đối thủ có đang đặt giá thầu cho tên ứng dụng của bạn không và ở những quốc gia nào, rồi lấy lại lưu lượng truy cập đó. - **Thâm nhập thị trường mới với dữ liệu thực**: Kiểm tra những từ khóa mà đối thủ đang chạy ở một quốc gia trước khi bạn chi một đồng nào. - **Phát hiện đối thủ bị bỏ sót**: Tìm kiếm theo từ khóa để xem ai đang xếp hạng tự nhiên cho các từ khóa trong danh mục của bạn và thêm họ vào phân tích. ## Chạy phân tích Market Intelligence \{#run-a-market-intelligence-analysis\} ### 1. Chọn ứng dụng của bạn \{#1-select-your-app\} Trong thanh bên trái, vào **Market Intelligence**. Chọn ứng dụng bạn muốn phân tích từ menu thả xuống, sau đó nhấp **Continue**. ### 2. Chọn đối thủ cạnh tranh \{#select-competitors\} Thêm các đối thủ cạnh tranh bạn muốn phân tích: - **Đối thủ được gợi ý**: Adapty tự động phát hiện các đối thủ có khả năng cạnh tranh dựa trên danh mục ứng dụng của bạn. Xem qua danh sách và chọn những đối thủ bạn muốn đưa vào. - **Tìm theo từ khóa hoặc tên ứng dụng**: Nhập từ khóa (ví dụ: "budget tracker") hoặc tên ứng dụng vào ô tìm kiếm. Thay đổi quốc gia nếu cần, rồi chọn ứng dụng từ kết quả. Thử với các từ khóa khác nhau để khám phá đối thủ theo nhiều mục đích tìm kiếm. - **Danh sách đã lưu**: Nhấp **Create list** để lưu tối đa 20 đối thủ nhằm tái sử dụng trong các phân tích sau. Bạn cũng có thể tải lại danh sách đã tạo trước đó. Khi đã chọn xong đối thủ cạnh tranh, nhấp vào **Run analysis**. ### 3. Khám phá kết quả \{#3-explore-results\} Kết quả được tổ chức thành bốn tab: **Overview**, **Most Contested**, **By App** và **By Country**. #### Overview \{#overview\} Tab mặc định hiển thị tóm tắt của phân tích: - **Thanh thống kê**: Tổng số đối thủ đã phân tích, số quốc gia có hoạt động Apple Ads, số từ khóa duy nhất tìm được trên tất cả thị trường, và từ khóa cạnh tranh nhất. - **Từ khóa theo quốc gia**: Biểu đồ cột thể hiện lượng từ khóa theo từng quốc gia trong top 25 thị trường. - **Top 10 đối thủ theo độ phủ từ khóa**: Bảng xếp hạng hiển thị tổng số từ khóa, chỉ số Share of Voice trung bình (Avg SOV), và các quốc gia mà mỗi đối thủ đang hoạt động. #### Tranh Giành Nhiều Nhất \{#most-contested\} Hiển thị các từ khóa mà có nhiều đối thủ cạnh tranh hoạt động đồng thời nhất. Dùng tab này để tìm các từ khóa có nhu cầu cao trong danh mục của bạn và xem nơi cạnh tranh tập trung nhiều nhất. Sử dụng ô tìm kiếm để lọc danh sách theo từ khóa. #### Theo Ứng Dụng \{#by-app\} Hiển thị dữ liệu từ khóa của từng đối thủ cạnh tranh riêng lẻ. Sử dụng tab này để phân tích chiến lược từ khóa của một ứng dụng cụ thể theo từng quốc gia. Nhấp vào **Add filter** để lọc theo ứng dụng, quốc gia hoặc từ khóa. Để xuất dữ liệu dưới dạng CSV, nhấp vào biểu tượng tải xuống. #### Theo Quốc Gia \{#by-country\} Hiển thị dữ liệu từ khóa được nhóm theo thị trường. Sử dụng tab này khi bạn muốn tập trung vào bức tranh cạnh tranh của một quốc gia cụ thể trước khi thâm nhập hoặc mở rộng tại đó. Nhấp **Add filter** để lọc theo ứng dụng, quốc gia hoặc từ khóa. Để xuất dữ liệu dưới dạng CSV, nhấp vào biểu tượng tải xuống. ### 4. Thêm từ khóa vào chiến dịch \{#add-keywords-to-campaigns\} Khi đã xác định được các từ khóa đáng thử nghiệm, bạn có thể thêm chúng vào chiến dịch ngay trong công cụ: 1. Trong bảng từ khóa, chọn các ô checkbox bên cạnh những từ khóa bạn muốn sử dụng. Để chọn tất cả từ khóa đang hiển thị, hãy dùng checkbox ở phần tiêu đề bảng. 2. Nhấp vào **Add to campaign**. 3. Chọn cách thêm: dạng từ khóa thông thường, từ khóa phủ định, hay SKAG. Sau đó, chọn chiến dịch và nhóm quảng cáo mục tiêu, đặt loại match type và giá thầu CPT, rồi xác nhận. ## Những điểm cần chú ý \{#what-to-look-for\} Hãy chú ý các xu hướng sau trong kết quả: - **Từ khóa đuôi dài có Share of Voice thấp**: Các từ khóa mà đối thủ cạnh tranh có tỷ lệ hiển thị thấp thường ít bị tranh giành hơn. Chúng thường có chi phí mỗi lượt nhấp thấp hơn và tỷ lệ chuyển đổi tốt hơn vì ý định tìm kiếm của người dùng cụ thể hơn. - **Từ khóa chưa có trong chiến dịch của bạn**: Tìm những từ khóa mà đối thủ đang chạy nhưng bạn chưa thử. Đây là những từ khóa đã được chứng minh là có thể thu hút lưu lượng truy cập Apple Ads trong danh mục của bạn. - **Phạm vi phủ sóng từ khóa thương hiệu**: Tìm kiếm tên ứng dụng của chính bạn. Nếu đối thủ xuất hiện, họ đang đặt giá thầu cho thương hiệu của bạn. Hãy thêm những từ khóa đó vào chiến dịch với mức giá thầu cao để bảo vệ lưu lượng truy cập. - **Khoảng trống theo quốc gia**: Kiểm tra xem đối thủ đang hoạt động ở những quốc gia nào. Các thị trường có ít hoặc không có hoạt động của đối thủ thường dễ thâm nhập hơn và cần ngân sách ít hơn để tạo đà tăng trưởng. --- # File: ads-manager-cpp-ab-tests --- --- title: "A/B Test CPP trong Adapty Ads Manager" description: "So sánh các trang sản phẩm tùy chỉnh trong Apple Ads và tìm ra trang hiệu quả nhất." --- A/B Test CPP cho phép bạn so sánh các trang sản phẩm tùy chỉnh (CPP) với nhau trong Apple Ads. Bạn chọn từ 2 đến 4 trang sản phẩm, và [Adapty Ads Manager](adapty-ads-manager) sẽ luân phiên phân phối traffic giữa chúng, thu thập dữ liệu hiệu suất, và cho bạn biết trang nào có tỷ lệ chuyển đổi tốt nhất. Bạn có thể đưa **trang sản phẩm mặc định** của mình vào như một trong các biến thể, để kiểm tra xem trang tùy chỉnh có hiệu quả hơn trang mặc định hiện tại hay không. ## Điều kiện tiên quyết \{#prerequisites\} Trước khi tạo CPP A/B test, hãy đảm bảo: - **Apple Ads Manager đã được kết nối**: Làm theo [hướng dẫn cài đặt](adapty-ads-manager-get-started) nếu bạn chưa thực hiện. - **Nhóm quảng cáo nguồn có lưu lượng truy cập**: Nhóm quảng cáo bạn muốn kiểm tra phải ít nhất 28 ngày tuổi và có lượt hiển thị, lượt nhấn, và lượt cài đặt trong khoảng thời gian đó. Apple Ads Manager sử dụng lịch sử này để ước tính thời gian chạy test và kích thước mẫu cần thiết. - **Bạn có ít nhất một trang sản phẩm tùy chỉnh**: Tạo CPPs trong App Store Connect trước. Apple Ads Manager sẽ đọc chúng tự động. ## Tạo CPP A/B test \{#create-a-cpp-ab-test\} Để tạo test, trong thanh bên trái, vào **CPP A/B Tests** và nhấn **Create A/B Tests**. Trình hướng dẫn có bốn bước: **Ad Group(s)**, **Ad Creative(s)**, **Testing Method**, và **Review**. ### 1. Ad Group(s) \{#1-ad-groups\} Nhập **Test Name** và nhấn **Select Ad Group** để chọn nhóm quảng cáo chứa các CPP bạn muốn kiểm tra. Bạn có thể chọn tối đa bốn nhóm quảng cáo từ cùng một chiến dịch, nhưng chỉ khi bạn muốn kiểm tra một ad creative duy nhất cho tất cả chúng. Để so sánh nhiều CPP, hãy chọn một nhóm quảng cáo. ### 2. Ad Creative(s) \{#2-ad-creatives\} Chọn các CPP bạn muốn so sánh. Bạn có thể thêm **Default Product Page** (được đánh dấu là **Control**) cùng tối đa ba **Custom Product Pages**, tổng cộng từ 2 đến 4 biến thể. - **Default Product Page**: Nhấn **+ Add Default** để thêm trang sản phẩm mặc định hiện tại làm biến thể kiểm soát. - **Custom Product Pages**: Nhấn **+ Select CPP** để chọn trang sản phẩm tùy chỉnh từ App Store Connect. ### 3. Phương pháp kiểm tra \{#3-testing-method\} Cấu hình cách thức chạy test. Adapty Ads Manager tự động tính toán **Calculated Test Duration**, **Start Time**, và **End Time** — các giá trị sẽ cập nhật mỗi khi bạn thay đổi một trong ba cài đặt dưới đây. #### Switch Time Preset \{#switch-time-preset\} Tần suất hệ thống luân chuyển giữa các biến thể. Nếu bạn chọn giá trị quá cao so với lưu lượng truy cập thực tế, hệ thống sẽ tự động hạ xuống mức phù hợp. | Khoảng thời gian | Mức lưu lượng truy cập thông thường | Thời lượng slot | Độ dài test thông thường | |------------------|----------------------------------------------|-----------------|--------------------------| | **Hourly** | Cao (5.000+ lượt hiển thị mỗi ngày) | 7 giờ | Vài ngày | | **Daily** | Bình thường | 24 giờ | Vài tuần | | **Weekly** | Thấp (ít hơn 400 lượt hiển thị mỗi ngày) | 7 ngày | Vài tháng | **Slot** là khoảng thời gian cơ bản mà một biến thể chạy trước khi hệ thống xem xét chuyển sang biến thể tiếp theo. #### Độ chính xác mong muốn \{#desired-precision\} Mức chênh lệch nhỏ nhất về tỷ lệ chuyển đổi mà A/B test có thể phát hiện một cách đáng tin cậy. Các tùy chọn: **1%**, **2%**, **3%**, **4%**, **5%**. Mặc định: **5%**. A/B test với độ chính xác 1% có thể phát hiện những khác biệt rất nhỏ nhưng cần nhiều dữ liệu hơn và chạy lâu hơn. A/B test với độ chính xác 5% kết thúc nhanh hơn nhưng chỉ phát hiện được những khác biệt lớn hơn. | Độ chính xác | Khi nào nên dùng | |-----------|-----------------------------------------------------------------------------------| | 1–2% | Bạn mong đợi sự khác biệt nhỏ giữa các CPP và có nhóm quảng cáo có lưu lượng truy cập cao. | | 3–4% | Mặc định cân bằng cho hầu hết các bài kiểm tra. | | 5% | Bạn mong đợi có người chiến thắng rõ ràng và muốn có kết quả nhanh chóng. | #### Mức độ tin cậy \{#confidence-level\} Bạn muốn chắc chắn đến mức nào rằng kết quả là thật chứ không phải nhiễu ngẫu nhiên. Các tùy chọn: **80%**, **85%**, **90%**, **95%**, **99%**. Mặc định: **90%**. Mức độ tin cậy càng cao thì càng cần nhiều dữ liệu hơn. | Độ tin cậy | Đánh đổi | |------------|---------------------------------------------------------------------------------| | 80–85% | Kết thúc nhanh hơn, nhưng khả năng kết quả là nhiễu cao hơn. | | 90% | Mặc định được khuyến nghị cho hầu hết các test. | | 95–99% | Bảo thủ nhất. Cần nhiều dữ liệu và thời gian test dài nhất. | ### 4. Review \{#4-review\} Kiểm tra lại tóm tắt — các nhóm quảng cáo đã chọn, creatives, phương pháp kiểm tra, thời gian, độ chính xác, và mức độ tin cậy — rồi nhấn **Start CPP A/B Tests**. Sau khi bạn bắt đầu test, hệ thống sẽ sao chép nhóm quảng cáo cho mỗi biến thể, kích hoạt biến thể đầu tiên, và trạng thái test chuyển sang **Running** trong vài phút. ## Theo dõi test đang chạy \{#monitor-a-running-test\} Để mở danh sách test, vào **CPP A/B Tests** ở thanh sidebar bên trái. Bốn tab ở đầu trang lọc test theo trạng thái: - **Live**: Các test đang chạy. - **Completed**: Các test đã hoàn thành. - **Draft**: Các test chưa được bắt đầu. - **Archive**: Các test cũ mà bạn không cần hiển thị ở màn hình chính nữa. Mỗi thẻ test hiển thị tên, trạng thái, khoảng thời gian chuyển đổi, độ chính xác mong muốn và thời gian đã chạy. Nhấp **View metrics** để mở rộng bảng biến thể. ### Hiệu suất biến thể \{#variant-performance\} Bảng biến thể so sánh hiệu suất của tất cả các biến thể trong A/B test: | Cột | Mô tả | |--------------------------|-----------------------------------------------------------------------------------------------------| | **Variant Name** | CPP đang được kiểm tra. Biến thể A luôn là biến thể đầu tiên bạn thêm vào. | | **Confidence Level** | Mức độ gần đạt kích thước mẫu yêu cầu của biến thể, tính theo phần trăm từ 0 đến 100. | | **Impressions** | Số lần Apple hiển thị quảng cáo cho biến thể này. | | **TTR** | Tỷ lệ nhấn qua (tap-through rate): số lần nhấn chia cho số lần hiển thị. | | **Tap → Download CR** | Tỷ lệ chuyển đổi từ nhấn sang tải xuống. | | **CPT** | Chi phí trung bình mỗi lần nhấn. | | **Avg CPA (Tap-Through)**| Chi phí thu hút khách hàng trung bình dựa trên lượt tải xuống qua tap-through. | | **Spend** | Tổng chi tiêu được ghi nhận cho biến thể. | | **Revenue** | Tổng doanh thu được ghi nhận cho biến thể. | | **ROAS** | Lợi nhuận trên chi tiêu quảng cáo: doanh thu chia cho chi tiêu. | Cho đến khi mỗi biến thể có số lần hiển thị tương đương nhau, Adapty Ads Manager sẽ không đánh dấu người chiến thắng. Trong khi dữ liệu vẫn đang được thu thập, bạn sẽ thấy một thông báo phía trên bảng: **Winner highlighting is paused — variants don't have comparable impressions yet.** ### Chỉ số chi tiết \{#detailed-metrics\} Để xem chi tiết hơn về test, nhấn **View metrics** để mở trang chỉ số chi tiết. Trang này bao gồm đường cong giữ chân cohort, so sánh ARPPU và bảng chỉ số được nhóm thành hai phần: - **Top of funnel · Apple Search Ads**: TTR, Download Rate, CPM, CPT và Avg CPA theo từng biến thể. - **Bottom of funnel · Monetization**: Paid users, Paid CR, Cost per Paid, ARPPU, Revenue và ROAS theo từng biến thể. Cột **Winner** hiển thị biến thể dẫn đầu trên từng chỉ số. Một biến thể chỉ được công nhận là người chiến thắng tổng thể khi dẫn đầu trên chỉ số chính và đạt mức độ tin cậy ít nhất 95%. Để xem định nghĩa các chỉ số, hãy tham khảo [Chỉ số trong Adapty Ads Manager](adapty-ads-manager-metrics). ## Dừng test \{#stop-a-test\} Bạn có thể dừng test bất cứ lúc nào. Test sẽ được đánh dấu là **Stopped**, nhóm quảng cáo gốc sẽ được khôi phục, và các nhóm quảng cáo đã sao chép sẽ bị tạm dừng. Để dừng test đang chạy: 1. Vào **CPP A/B Tests** trong thanh bên trái. 2. Nhấn **Stop A/B test** trên thẻ test, hoặc mở test và nhấn **Stop Test**. 3. Xác nhận trong hộp thoại **Stop A/B Test?**. :::important Dừng test là hành động cuối cùng — bạn không thể tiếp tục lại. Kết quả đã thu thập được vẫn có sẵn trong tab **Completed**. ::: ## Trạng thái của A/B test \{#test-statuses\} Mỗi A/B test đều trải qua một tập hợp trạng thái cố định: | Trạng thái | Ý nghĩa | |---------------|------------------------------------------------------------------------------------------| | **Draft** | A/B test đã được tạo nhưng chưa bắt đầu. Bạn vẫn có thể chỉnh sửa. | | **Starting** | Đang thiết lập — hệ thống đang sao chép các nhóm quảng cáo và tạo quảng cáo. | | **Running** | A/B test đang chạy. Các biến thể được luân phiên và các chỉ số đang được thu thập. | | **Completed** | Thời gian đã lên lịch hết hạn hoặc đã đạt đến độ tin cậy. Nhóm quảng cáo gốc được khôi phục. | | **Stopped** | Bạn đã dừng A/B test theo cách thủ công. Nhóm quảng cáo gốc được khôi phục. | | **Failed** | Thiết lập thất bại hoặc xảy ra quá nhiều lỗi liên tiếp. Bạn có thể khởi động lại A/B test bị lỗi. | ## Cách thức hoạt động \{#how-it-works\} Adapty Ads Manager sử dụng phương pháp **Ad Group Switch**: 1. Khi bắt đầu test, hệ thống sao chép nhóm quảng cáo nguồn một lần cho mỗi biến thể. Mỗi bản sao trỏ đến một CPP khác nhau (một trong số đó có thể là trang mặc định của bạn). 2. Mỗi lần chỉ có một bản sao chạy. Hệ thống luân phiên bản sao nào đang hoạt động theo lịch cố định (theo giờ, ngày hoặc tuần). 3. Nhóm quảng cáo gốc bị tạm dừng trong thời gian test. Nó sẽ được khôi phục về trạng thái trước đó khi test kết thúc. 4. Adapty Ads Manager thu thập số lượt hiển thị, lượt nhấn và lượt tải xuống cho từng biến thể, đồng thời theo dõi mức độ mỗi biến thể đã tiến gần đến mẫu có ý nghĩa thống kê. 5. Test tự động kết thúc khi mỗi biến thể có đủ dữ liệu, hoặc khi đạt đến thời hạn đã đặt. ## Những điều cần biết khi test đang chạy \{#what-to-expect-while-a-test-runs\} Dưới đây là một số điều cần lưu ý về cách A/B test hoạt động trên dashboard khi đang chạy: - **Các biến thể không chuyển đổi theo đồng hồ cố định**: Khoảng thời gian chuyển đổi chỉ là mốc cơ bản, nhưng Adapty Ads Manager sẽ điều chỉnh thời điểm để mỗi biến thể thu thập được lượng hiển thị công bằng. Một biến thể có thể duy trì lâu hơn một slot nếu nó còn thiếu lượt hiển thị. - **Thời gian kết thúc có thể bị dời lại**: Nếu một biến thể còn thiếu dữ liệu khi đến gần thời điểm kết thúc đã lên lịch, A/B test sẽ tự động được kéo dài để tiếp tục thu thập lượt nhấp. Thời gian kết thúc mới sẽ hiển thị trên thẻ test. - **Khi test kết thúc, nhóm quảng cáo gốc được khôi phục**: Tất cả các nhóm quảng cáo đã clone sẽ bị tạm dừng và nhóm quảng cáo nguồn trở về trạng thái trước khi test. Kết quả vẫn có thể xem trong tab **Completed**. --- # File: ads-manager-settings --- --- title: "Cài đặt trong Adapty Ads Manager" description: "Cấu hình cài đặt trong Adapty Ads Manager." --- Điều hướng đến **Settings** ở góc dưới bên trái của dashboard Adapty Ads Manager để cấu hình cài đặt tài khoản của bạn. ## Nhóm chiến dịch \{#campaign-groups\} Trong tab **Campaign groups**, bạn có thể xem tất cả các tài khoản Apple Ads đã kết nối với Adapty Ads Manager và thêm tài khoản mới. Nếu bạn kết nối nhiều tài khoản Apple Ads, toàn bộ dữ liệu analytics của chúng sẽ được tổng hợp trong một dashboard Adapty Ads Manager duy nhất. Để thêm tài khoản Apple Ads mới, nhấp vào **Connect Apple Ads account** và làm theo [hướng dẫn](adapty-ads-manager-get-started). ## Quản lý gói đăng ký \{#manage-subscription\} Trên tab **Manage subscription**, bạn có thể xem gói đăng ký hiện tại và cập nhật phương thức thanh toán. ## Cài đặt người dùng \{#user-settings\} Trong tab **User settings**, bạn có thể bật công tắc **Hide Paused by Default**. Khi được bật, các chiến dịch, nhóm quảng cáo và từ khóa đang tạm dừng sẽ bị ẩn, giúp bạn tập trung vào dữ liệu hiệu suất đang hoạt động. Không nên bật tùy chọn này nếu bạn thường xuyên thử nghiệm bằng cách chạy và tạm dừng các chiến dịch, nhóm quảng cáo và từ khóa khác nhau, vì bạn có thể cần truy cập các mục đã tạm dừng bất cứ lúc nào. --- # File: adapty-user-acquisition --- --- title: "Adapty Attribution" description: "Loại bỏ nhu cầu sử dụng MMP và tính toán toàn bộ kinh tế ứng dụng tại một nơi." --- <CustomDocCardList ids={['user-acquisition', 'ua-analytics', 'ua-integrations', 'ua-tracking-links', 'ua-deferred-data']} /> Adapty Attribution là một giải pháp attribution kết nối các chiến dịch quảng cáo với lượt cài đặt ứng dụng và doanh thu gói đăng ký bằng cách kết hợp dữ liệu từ các nền tảng quảng cáo, tracking link và ứng dụng của bạn. Nó cung cấp một dashboard phân tích marketing thống nhất, tổng hợp toàn bộ dữ liệu thu thập người dùng vào một nơi. - Tính toán ROAS (lợi nhuận trên chi phí quảng cáo) trên tất cả các kênh của bạn - Xem toàn bộ hoạt động kinh tế trong ứng dụng ở một nơi - Nhận dữ liệu attribution chính xác để đưa ra quyết định tốt hơn - Phân tích hiệu suất cohort và hành vi người dùng theo thời gian :::tip Muốn tìm hiểu thêm về cách Adapty Attribution có thể hữu ích với bạn? [Đặt lịch gặp](https://calendly.com/tnurutdinov-adapty/30min) với chúng tôi. ::: ## Tại sao nên chọn Adapty Attribution? \{#why-choose-adapty-attribution\} Đo lường hiệu suất thu hút người dùng là một thách thức lớn. Dữ liệu thường bị phân tán trên nhiều nền tảng, attribution trở nên khó khăn do các thay đổi về quyền riêng tư, và việc xây dựng giải pháp tùy chỉnh đòi hỏi rất nhiều thời gian. Attribution của Adapty cung cấp attribution tích hợp sẵn và analytics thống nhất trong một marketing dashboard duy nhất. Tất cả các chỉ số thu hút người dùng của bạn—từ chi phí quảng cáo đến lượt cài đặt đến doanh thu gói đăng ký—đều được tự động tổng hợp và cập nhật theo thời gian thực. Không còn phải đối chiếu dữ liệu qua các bảng tính hay chuyển đổi giữa nhiều công cụ nữa. Bạn có thể tập trung vào việc phát triển ứng dụng thay vì quản lý hệ thống dữ liệu. ## Cách hoạt động \{#how-it-works\} Adapty Attribution gán các lượt cài đặt ứng dụng và doanh thu từ gói đăng ký vào các chiến dịch quảng cáo bằng cách kết hợp dữ liệu từ các nền tảng quảng cáo, liên kết theo dõi và ứng dụng của bạn. Tổng quan: - Các nền tảng quảng cáo cung cấp cấu trúc chiến dịch và chi phí quảng cáo - Các liên kết theo dõi được tạo trong Adapty Attribution mang ngữ cảnh chiến dịch từ web đến lượt cài đặt ứng dụng - SDK gửi các sự kiện cài đặt và doanh thu từ ứng dụng của bạn Luồng attribution hoạt động như sau: 1. **Một link theo dõi được tạo trong Adapty Attribution và thêm vào chiến dịch quảng cáo.** Link này chứa các thông số chiến dịch như nền tảng, chiến dịch, nhóm quảng cáo và mẫu quảng cáo. 2. **Người dùng nhấp vào quảng cáo và cài đặt ứng dụng từ cửa hàng ứng dụng.** Người dùng được chuyển hướng qua link theo dõi và cài đặt ứng dụng từ App Store hoặc Google Play. 3. **Ứng dụng gửi sự kiện cài đặt đến Adapty.** Khi khởi chạy lần đầu, SDK gửi một sự kiện cài đặt. Adapty trích xuất các thông số chiến dịch liên quan đến lần cài đặt này. 4. **Lượt cài đặt được gán cho một chiến dịch.** Sử dụng các tham số chiến dịch từ liên kết theo dõi, Adapty liên kết lượt cài đặt với chiến dịch đã tạo ra nó. 5. **Chi phí quảng cáo và doanh thu được kết nối.** Adapty lấy dữ liệu chi phí quảng cáo từ các nền tảng quảng cáo được hỗ trợ (hiện tại là Meta Ads và TikTok for Business) và liên kết các sự kiện gói đăng ký và mua hàng với các lượt cài đặt đã được attribution. Kết quả là Adapty cung cấp các chỉ số ở cấp độ chiến dịch như lượt cài đặt, doanh thu, LTV và ROAS trong một dashboard phân tích thống nhất. Bạn có thể phân tích cohort, theo dõi hiệu suất theo thời gian và đưa ra quyết định tối ưu hóa dựa trên dữ liệu — tất cả mà không cần thủ công đối chiếu dữ liệu từ nhiều nguồn khác nhau. :::tip Các liên kết theo dõi cũng có thể chứa tham số tùy chỉnh, cho phép ứng dụng của bạn xử lý [deferred deep link](ua-deferred-data) và phản ứng với dữ liệu chiến dịch khi xử lý sự kiện cài đặt. ::: --- # File: user-acquisition --- --- title: "Bắt đầu với Adapty Attribution" description: "Kết nối với Adapty Attribution để kết hợp chi tiêu quảng cáo và doanh thu gói đăng ký, giúp bạn nhìn thấy toàn bộ hoạt động kinh tế của ứng dụng trong một nơi." --- Adapty Attribution giúp bạn kết nối chi phí quảng cáo với doanh thu gói đăng ký trong các chiến dịch web-to-app, cho bạn cái nhìn toàn diện về kinh tế ứng dụng của mình trong một nơi duy nhất. Để xem dữ liệu doanh thu trong Adapty Attribution, trước tiên bạn cần bật tích hợp này trên Adapty Dashboard. Bạn không cần truyền bất kỳ API key, token hay định danh nào. Chỉ cần cập nhật và cấu hình Adapty SDK. :::important Adapty Attribution có thể sử dụng với: - iOS, Android và Flutter SDK phiên bản 3.9.1 trở lên. - React Native và Capacitor SDK phiên bản 3.10.0 trở lên. - Unity SDK phiên bản 3.12.0 trở lên. - Kotlin Multiplatform SDK phiên bản 3.15.0 trở lên. ::: ## Trước khi bắt đầu \{#before-you-start\} Để kết nối dữ liệu doanh thu với hiệu suất chiến dịch, hãy để Adapty theo dõi các giao dịch mua của bạn: - Nếu bạn **đã triển khai in-app purchase với Adapty**, bạn không cần làm gì thêm ở bước này. - Nếu bạn **chưa triển khai in-app purchase và muốn dùng Adapty**, hãy hoàn thành các bước trong [hướng dẫn quickstart](quickstart) để ủy quyền xử lý giao dịch mua cho Adapty. - Nếu bạn **đã triển khai in-app purchase mà không dùng Adapty** và không có kế hoạch migrate sang Adapty, hãy [cài đặt SDK Adapty cho nền tảng của bạn ở chế độ observer](implement-observer-mode). Ở bước này, bạn chỉ cần thêm SDK vào dự án, kích hoạt nó với chế độ observer được bật, và báo cáo các giao dịch: Thiết lập này cho phép attribution từ web sang app: - Khi người dùng cài đặt app, SDK sẽ lấy thông tin cài đặt từ các tham số link, giúp Adapty Attribution có thể lấy được thông tin chiến dịch - SDK biết về tất cả các sự kiện liên quan đến doanh thu trong app và có thể gán attribution cho các chiến dịch web. ## Bước 1. Mở Adapty Attribution \{#step-1-open-adapty-attribution\} :::important Nếu **Attribution** không xuất hiện khi bạn nhấp vào logo Adapty, hãy xóa cookie và dữ liệu trang web cho adapty.io trong cài đặt trình duyệt, rồi tải lại trang. ::: Nhấp vào logo Adapty trên thanh tiêu đề và chọn **Attribution**. Các sự kiện gói đăng ký sẽ tự động bắt đầu đổ vào Adapty Attribution. Dữ liệu chiến dịch sẽ xuất hiện sau khi bạn kết nối nguồn dữ liệu ở Bước 2. Để tạm dừng việc gửi sự kiện, mở **Integrations > Adapty** trong Adapty Dashboard và tắt toggle. ### Các sự kiện được hỗ trợ \{#supported-events\} Theo mặc định, Adapty gửi ba nhóm sự kiện đến User Acquisition: - Trials - Gói đăng ký - Sự cố Bạn có thể xem danh sách đầy đủ các sự kiện được hỗ trợ [tại đây](events). <img src="/assets/shared/img/events-ua.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 2. Kết nối nền tảng quảng cáo và thêm tracking link \{#step-2-connect-your-ad-platform-and-add-tracking-links\} Adapty sử dụng tracking link để liên kết lượt cài đặt ứng dụng với dữ liệu chiến dịch. Bạn phải dùng tracking link làm URL đích trong mọi chiến dịch quảng cáo muốn đo lường trong Adapty Attribution. Nếu chạy quảng cáo trên nhiều nền tảng, hãy thiết lập tracking link riêng cho từng nền tảng. Có hai cách Adapty hoạt động với các nền tảng quảng cáo: - **Tích hợp gốc (Meta Ads, TikTok Ads).** Adapty kết nối trực tiếp với nền tảng quảng cáo. Các liên kết theo dõi được tạo tự động, và các tham số chiến dịch được điền động dựa trên vị trí sử dụng liên kết. Bạn có thể dùng cùng một liên kết cho nhiều chiến dịch, nhóm quảng cáo hoặc mẫu quảng cáo khác nhau, và Adapty sẽ tự động nhận đúng dữ liệu chiến dịch cùng chi phí quảng cáo. - **Chỉ dùng tracking link (các nền tảng quảng cáo khác).** Adapty không kết nối trực tiếp với nền tảng quảng cáo. Tracking link được tạo thủ công, và tất cả các tham số chiến dịch phải được định nghĩa rõ ràng khi tạo link. Dữ liệu chi phí quảng cáo không có sẵn cho các nền tảng này. <Tabs> <TabItem value="meta" label="Meta Ads" default> Để tạo tracking link cho Meta Ads: 1. Vào [Integrations > Meta](https://app.adapty.io/ua/integrations/facebook/accounts) trong Adapty Attribution Dashboard và nhấp **Continue with Facebook**. 2. Đăng nhập bằng tài khoản Facebook của bạn và nhấp **Continue**. 3. Xem lại các quyền được yêu cầu và nhấp **Save**. 4. Chuyển sang tab **Web campaigns** và nhấp **Create campaign**. Chọn ứng dụng và nhấp **Save**. 5. Trong tab **General**, mở rộng mục **iOS** và/hoặc **Android** và dán URL ứng dụng trên App Store và/hoặc Google Play. Sau đó, nhấp **Save**. 6. Sao chép giá trị trường **Click link** cho **một link** hoặc cho link theo từng nền tảng. Sau đó, trong Meta Ads Manager, mở quảng cáo của bạn và dán link này vào làm URL đích. :::important Trong trường **Website URL**, dán `https://api-ua.adapty.io/api/v1/attribution/click`. Dán phần còn lại của link vào trường **URL parameters** trong phần **Tracking**. Điều này sẽ giúp quảng cáo Meta của bạn được phê duyệt. Xem thêm [các khuyến nghị về cách thiết lập quảng cáo trong Meta Ads Manager](meta-create-campaign). ::: 7. Bây giờ, khi bạn chạy quảng cáo trên Meta Ads, dữ liệu của quảng cáo đó sẽ được hiển thị để phân tích trong Adapty Attribution dashboard. </TabItem> <TabItem value="tiktok" label="TikTok for Business"> Để tạo liên kết theo dõi cho TikTok for Business: 1. Truy cập [Integrations > TikTok Ads](https://app.adapty.io/ua/integrations/tiktok/accounts) trong Adapty Attribution Dashboard và nhấn **Continue with TikTok**. 2. Đăng nhập bằng tài khoản TikTok của bạn và nhấn **Continue**. 3. Xem lại các quyền được yêu cầu và nhấn **Save**. 4. Chuyển sang tab **Web campaigns** và nhấp **Create campaign**. Chọn ứng dụng và nhấp **Save**. 5. Trong tab **General**, mở rộng phần **iOS** và/hoặc **Android** rồi dán URL ứng dụng từ App Store và/hoặc Google Play. Sau đó, nhấp **Save**. 6. Sao chép giá trị trường **Click link** cho **một link** hoặc cho link theo từng nền tảng. Sau đó, trong TikTok Ads Manager, khi tạo quảng cáo, dán giá trị này vào trường **Tracking URL** trong phần **Advanced Settings**. Điều này cho phép Adapty liên kết các lượt cài đặt và mua hàng với quảng cáo trên TikTok. Xem [hướng dẫn thiết lập chiến dịch trong TikTok Ads](tiktok-create-campaign). 7. Bây giờ, khi bạn chạy quảng cáo trên TikTok for Business, dữ liệu của quảng cáo sẽ có sẵn để phân tích trong dashboard Attribution của Adapty. </TabItem> <TabItem value="others" label="Nền tảng quảng cáo khác"> Để tạo tracking link cho các nền tảng quảng cáo khác: 1. Trong Adapty Attribution dashboard, chọn **Tracking links** từ menu sidebar. Tại đó, nhấn **Create link**. 2. Chọn ứng dụng của bạn từ danh sách và nhấn **Next**. 3. Điền các tham số liên kết để khớp với chiến dịch và quảng cáo bạn muốn theo dõi. 4. Theo mặc định, bạn đang tạo một One Link. Liên kết này tự động phát hiện nền tảng của người dùng và chuyển hướng họ đến App Store hoặc Google Play sau khi ghi nhận lượt nhấp. Nếu bạn muốn sử dụng URL chuyển hướng riêng cho từng nền tảng, hãy bỏ chọn ô **One Link** và cung cấp thủ công các liên kết cửa hàng theo từng nền tảng. 5. Nhấp vào **Create**. 6. Mở trang tracking link và sao chép **Click link** từ một trong các mục sau: - **One link** – dùng liên kết này để theo dõi lượt nhấp và tự động chuyển hướng người dùng đến đúng cửa hàng. - **iOS link** hoặc **Android link** — phiên bản theo nền tảng tùy chọn nếu bạn muốn có liên kết riêng cho từng cửa hàng. 7. Vào nền tảng quảng cáo của bạn và dán liên kết vào quảng cáo dưới dạng URL đích. </TabItem> </Tabs> ## Bước 3. Khởi chạy chiến dịch web-to-app và xem kết quả \{#step-3-launch-your-web-to-app-campaign-and-view-results\} Sau khi chiến dịch của bạn được phát và người dùng bắt đầu cài đặt ứng dụng, Adapty bắt đầu attribution lượt cài đặt và doanh thu vào các chiến dịch của bạn. Trong [Adapty UA analytics dashboard](ua-analytics), bạn sẽ thấy các chỉ số theo cấp độ chiến dịch như: - Lượt cài đặt và chuyển đổi - Doanh thu gói đăng ký và giao dịch mua - Phân tích hiệu suất theo nền tảng quảng cáo, chiến dịch, nhóm quảng cáo và creative Các chỉ số xuất hiện ngay khi nhận được sự kiện cài đặt và doanh thu từ ứng dụng của bạn. Dữ liệu chi phí quảng cáo có sẵn cho các nền tảng có tích hợp gốc. ## Tìm hiểu thêm \{#learn-more\} Tiếp tục với tài liệu chuyên sâu về Adapty Attribution analytics và các hướng dẫn thực tế cho việc chạy chiến dịch trên các nền tảng quảng cáo lớn: - [**Analytics trong Adapty Attribution**](ua-analytics): Xem cách sử dụng dashboard analytics hiệu quả. - [**Chỉ số trong Adapty Attribution**](ua-metrics): Khám phá các chỉ số có sẵn để phân tích user acquisition. - [**Tích hợp**](ua-integrations): Xem các nền tảng quảng cáo và tích hợp được Adapty Attribution hỗ trợ. - [**Chạy quảng cáo trong Meta Ads Manager**](meta-create-campaign): Tìm hiểu cách thiết lập và chạy chiến dịch trong Meta Ads Manager. - [**Chạy quảng cáo trong TikTok for Business**](tiktok-create-campaign): Tìm hiểu cách thiết lập và chạy chiến dịch trong TikTok for Business. --- # File: ua-metrics --- --- title: "Chỉ số trong Adapty UA" description: "Tìm hiểu về các chỉ số có sẵn trong Adapty UA." --- Adapty User Acquisition cung cấp các **chỉ số** toàn diện để đo lường hiệu suất chiến dịch và hành vi người dùng. Các chỉ số này có sẵn dưới dạng giá trị tiêu chuẩn, và một số chỉ số còn được cung cấp dưới dạng **chỉ số cohort** để phân tích nhóm người dùng theo thời gian. ## Chỉ số tiêu chuẩn \{#standard-metrics\} | **Chỉ số** | Mô tả | Cohort | |-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| | **Spend** | Tổng chi phí cho mỗi lần người dùng nhấp vào quảng cáo của bạn. | No | | **Impressions** | Số lần quảng cáo của bạn được hiển thị trong khoảng thời gian đã chọn. | No | | **Clicks** | Số lần người dùng nhấp vào quảng cáo của bạn trong kỳ báo cáo. | No | | **CPI** | **CPI (Cost per Install)** là số tiền bạn trả cho mỗi lượt cài đặt. <br/>**Công thức**: `Spend / Installs` | No | | **CPC** | **CPC (Cost per Click)** là số tiền bạn trả cho mỗi lần nhấp vào quảng cáo. <br/>**Công thức**: `Spend / Clicks` | No | | **CPM** | **CPM (Cost per Mille)** là số tiền bạn trả cho mỗi một nghìn lượt hiển thị quảng cáo. <br/>**Công thức**: `Spend / (Impressions / 1000)` | No | | **ICR** | **ICR (Install Conversion Rate)** là tỷ lệ phần trăm số lần nhấp vào quảng cáo dẫn đến cài đặt. <br/>**Công thức**: `(Installs / Clicks) × 100%` | No | | **IPM** | **IPM (Installs per Mille)** là số lượt cài đặt trên mỗi một nghìn lượt hiển thị quảng cáo. <br/>**Công thức**: `(Installs / Impressions) × 1000` | No | | **CTR** | **CTR (Click-Through Rate)** là tỷ lệ phần trăm số lượt hiển thị dẫn đến nhấp chuột. <br/>**Công thức**: `(Clicks / Impressions) × 100%` | No | | **Inline link clicks** | Số lần người dùng nhấp vào các liên kết nội tuyến trong nội dung quảng cáo hoặc trang ứng dụng của bạn. | No | | **Cost per inline link click** | Số tiền trung bình bạn trả cho mỗi lần nhấp vào liên kết nội tuyến. <br/>**Công thức**: `Spend / Inline Link Clicks` | No | | **Inline link click CTR** | Tỷ lệ phần trăm số lượt hiển thị dẫn đến nhấp vào liên kết nội tuyến. <br/>**Công thức**: `(Inline Link Clicks / Impressions) × 100%` | No | | **Installs** | Tổng số người dùng đã cài đặt ứng dụng của bạn (bao gồm cả cài đặt lại) trong kỳ báo cáo. | No | | **Revenue** | Tổng doanh thu từ các giao dịch mua liên kết với chiến dịch này (trước khi trừ hoa hồng của cửa hàng) trong khoảng thời gian đã chọn. | Yes | | **ROAS** | **ROAS (Return on Ad Spend)** là doanh thu từ quảng cáo chia cho chi phí quảng cáo, tính theo phần trăm. <br/> **Công thức**: `(Revenue / Spend) × 100% nếu Spend > 0, ngược lại 0%` | Yes | | **ARPU** | **ARPU (Average Revenue per User)** là doanh thu trung bình trên mỗi người dùng trong cohort. <br/>**Công thức**: `Revenue / Users` | Yes | | **LTV** | **LTV (Lifetime Value)** là doanh thu trung bình quy về một người dùng trong toàn bộ vòng đời của họ. <br/>**Công thức**: `Revenue / Installs` | No | | **Cost per trial** | Số tiền trung bình bạn trả cho mỗi lần dùng thử được bắt đầu. <br/>**Công thức**: `Spend / Count trial started` | No | | **Cost per subscription** | Số tiền trung bình bạn trả cho mỗi gói đăng ký được mua. <br/>**Công thức**: `Spend / Count subscription started` | No | | **Count subscription events** | Nhóm chỉ số đếm các sự kiện liên quan đến gói đăng ký trong kỳ báo cáo. Các chỉ số bao gồm: <br/>- Count subscription started<br/>- Count subscription renewed<br/>- Count subscription renewal cancelled<br/>- Count subscription renewal reactivated<br/>- Count subscription expired<br/>- Count [subscription deferred](https://adapty.io/glossary/subscription-purchase-deferral/)<br/>- Count subscription refunded | Yes | | **Count trial events** | Nhóm chỉ số đếm các sự kiện liên quan đến dùng thử trong kỳ báo cáo. Các chỉ số bao gồm: <br/>- Count trial started<br/>- Count trial converted<br/>- Count trial expired<br/>- Count trial renewal reactivated | Yes | | **Count billing issue detected** | Số lượng sự cố thanh toán được phát hiện trong kỳ báo cáo. | Yes | | **Count entered grace period** | Số gói đăng ký bước vào thời gian ân hạn do sự cố thanh toán. | Yes | | **Count non-subscription events** | Nhóm chỉ số đếm các sự kiện không liên quan đến gói đăng ký trong kỳ báo cáo. Các chỉ số bao gồm: <br/>- Count non-subscription purchased<br/>- Count non-subscription refunded | Yes | | **Subscription events rate** | Các chỉ số thể hiện tỷ lệ sự kiện liên quan đến gói đăng ký so với số lượt cài đặt ứng dụng trong kỳ báo cáo. Các chỉ số bao gồm: <br/>- Rate subscription started<br/>- Rate subscription renewed<br/>- Rate subscription renewal cancelled<br/>- Rate subscription renewal reactivated<br/>- Rate subscription expired<br/>- Rate [subscription deferred](https://adapty.io/glossary/subscription-purchase-deferral/)<br/>- Rate subscription refunded | Yes | | **Trial events rate** | Các chỉ số thể hiện tỷ lệ sự kiện liên quan đến dùng thử so với số lượt cài đặt ứng dụng trong kỳ báo cáo. Các chỉ số bao gồm: <br/>- Rate trial started<br/>- Rate trial converted<br/>- Rate trial expired<br/>- Rate trial renewal reactivated | Yes | | **Rate billing issue detected** | Tỷ lệ sự cố thanh toán so với số lượt cài đặt ứng dụng trong kỳ báo cáo. | Yes | | **Rate entered grace period** | Tỷ lệ gói đăng ký bước vào thời gian ân hạn so với số lượt cài đặt ứng dụng trong kỳ báo cáo. | Yes | | **Non-subscription events rate** | Các chỉ số thể hiện tỷ lệ sự kiện không liên quan đến gói đăng ký so với số lượt cài đặt ứng dụng trong kỳ báo cáo. Các chỉ số bao gồm: <br/>- Rate non-subscription purchased<br/>- Rate non-subscription refunded | Yes | ## Chỉ số dự đoán \{#predicted-metrics\} Chỉ số dự đoán chiếu hiệu suất trong tương lai của một cohort dựa trên dữ liệu lịch sử của chính ứng dụng. Chúng có sẵn ở nhiều kỳ cohort khác nhau, bao gồm D30, D60, D90, D180 và D360, cùng với các kỳ tùy chỉnh mà bạn có thể thêm theo ngày. Để biết cách tính các giá trị này, xem [Chỉ số dự đoán trong Adapty Attribution](ua-predicted-metrics). | **Chỉ số** | Mô tả | Cohort | |---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------| | **pRevenue** | Tổng doanh thu dự đoán mà một cohort được kỳ vọng tạo ra tính đến thời điểm mục tiêu. Được mô hình hóa từ lịch sử giữ chân cohort của ứng dụng. | Có | | **pROAS** | Lợi nhuận trên chi tiêu quảng cáo dự đoán trong cùng khoảng thời gian. **Công thức**: `(pRevenue / Spend) × 100%` | Có | | **pAdProfit** | Doanh thu dự đoán sau khi trừ chi phí quảng cáo trong khoảng thời gian đó. **Công thức**: `pRevenue − Spend` | Có | | **pARPU** | Doanh thu trung bình dự đoán trên mỗi lượt cài đặt trong khoảng thời gian đó (LTV dự đoán). **Công thức**: `pRevenue / Installs` | Có | | **pARPPU** | Doanh thu trung bình dự đoán trên mỗi người dùng trả tiền trong khoảng thời gian đó. **Công thức**: `pRevenue / paying users at d{N}`, trong đó `d{N}` khớp với khoảng thời gian đã chọn. | Có | --- # File: ua-predicted-metrics --- --- title: "Chỉ số dự đoán trong Adapty Attribution" description: "Dự báo doanh thu, ROAS, lợi nhuận quảng cáo và LTV cho các cohort trong Adapty Attribution." --- :::important Bài viết này chỉ đề cập đến các dự đoán trong Adapty Attribution. Để xem dự đoán LTV và doanh thu trên trang phân tích Cohort, hãy xem [Dự đoán trong cohort](predicted-ltv-and-revenue). ::: Adapty Attribution dự báo doanh thu và kinh tế đơn vị trong tương lai cho từng cohort, giúp bạn so sánh các chiến dịch trước khi chúng có đủ thời gian để trưởng thành. Các dự đoán được tạo ra từ dữ liệu cohort lịch sử của chính ứng dụng và cập nhật hàng ngày. Chúng hữu ích nhất khi đánh giá các cohort gần đây chưa hoàn thành các chu kỳ đăng ký dài. ## Chỉ số dự đoán \{#predicted-metrics\} | **Chỉ số** | Mô tả | |---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **pRevenue** | Tổng doanh thu dự đoán mà một cohort được kỳ vọng tạo ra đến thời điểm mục tiêu. Được mô hình hóa dựa trên dữ liệu lịch sử giữ chân cohort của ứng dụng. | | **pROAS** | Lợi tức trên chi phí quảng cáo dự đoán trong cùng khoảng thời gian. **Công thức**: `(pRevenue / Spend) × 100%` | | **pAdProfit** | Doanh thu dự đoán sau khi trừ chi phí quảng cáo trong khoảng thời gian đó. **Công thức**: `pRevenue − Spend` | | **pARPU** | Doanh thu trung bình dự đoán trên mỗi lượt cài đặt trong khoảng thời gian đó (LTV dự đoán). **Công thức**: `pRevenue / Installs` | | **pARPPU** | Doanh thu trung bình dự đoán trên mỗi người dùng trả phí trong khoảng thời gian đó. **Công thức**: `pRevenue / paying users at d{N}`, trong đó `d{N}` khớp với khoảng thời gian đã chọn. | `pRevenue` là giá trị cơ sở. Bốn chỉ số còn lại được tính từ nó bằng cách sử dụng dữ liệu cohort thực tế — chi tiêu, lượt cài đặt và người dùng có mua hàng — chứ không phải bằng cách chạy mô hình riêng lẻ. Mỗi chỉ số dự đoán có thể xem theo nhiều khoảng thời gian cohort khác nhau: D0, D3, D7, D30, D60, D90, D180 và D360. Bạn cũng có thể thêm khoảng thời gian tùy chỉnh tính bằng ngày. Khoảng thời gian này xác định mức độ dự đoán kéo dài bao xa vào tương lai, tính từ ngày cài đặt của cohort. ## Cách tính toán dự đoán \{#how-predictions-are-calculated\} Dự đoán được xây dựng từ các cohort lịch sử của chính ứng dụng. Mô hình đo lường mức tăng trưởng doanh thu của các cohort trong quá khứ sau ngày cơ sở, sau đó chiếu cohort hiện tại về phía trước theo cùng một quỹ đạo. ### Ngày cơ sở \{#baseline-day\} Các dự đoán chỉ khả dụng sau khi cohort đạt đến ngày cơ sở. Ngày cơ sở là ngày đầu tiên mà thông thường 90% doanh thu ban đầu của một cohort đã được ghi nhận. Doanh thu ban đầu bao gồm các lần bắt đầu gói đăng ký, chuyển đổi dùng thử, và sản phẩm mua một lần; các lần gia hạn không được tính vào ngưỡng này. Ngày cơ sở phụ thuộc vào thời gian dùng thử và cơ cấu sản phẩm của ứng dụng: - **Ứng dụng không có dùng thử**: Ngày cơ sở thường rơi vào vài ngày đầu sau khi cài đặt. - **Ứng dụng có dùng thử ngắn**: Ngày cơ sở thường xuất hiện ngay sau khi dùng thử chuyển đổi thành công. - **Ứng dụng có dùng thử dài hơn**: Ngày cơ sở có thể nằm ở tuần thứ hai hoặc lâu hơn sau khi cài đặt, vì phần lớn doanh thu ban đầu chỉ thực sự phát sinh sau khi thời gian dùng thử kết thúc. ### Chiếu theo loại gói đăng ký \{#projection-by-subscription-type\} Tại ngày cơ sở, doanh thu ban đầu của cohort được chia thành năm danh mục — gói đăng ký theo tháng, năm, tuần, quý và các sản phẩm mua một lần. Mỗi danh mục được chiếu về phía trước một cách độc lập theo quỹ đạo được đo từ các cohort trong quá khứ của ứng dụng. Mô hình ưu tiên các cohort gần đây hơn và các cohort có kinh tế học tương đương, chẳng hạn như doanh thu trên mỗi giao dịch tương tự và cấu trúc sản phẩm tương tự. Do đó, một dự đoán phản ánh cách các cohort gần đây và tương tự nhất trong ứng dụng đã thực sự hoạt động. ## Khi nào dự đoán khả dụng \{#when-predictions-are-available\} Dự đoán chỉ hiển thị khi cohort có đủ dữ liệu. Khi không thể tạo ra giá trị, cột sẽ hiển thị dấu gạch ngang (`—`) thay thế. Các lý do phổ biến khiến dự đoán không khả dụng: - **Cohort chưa đạt đến ngày cơ sở**: Mô hình cần doanh thu ban đầu của cohort ổn định trước khi có thể dự đoán về phía trước. - **Dữ liệu lịch sử của ứng dụng không đủ**: Nếu ứng dụng không có đủ các cohort trong quá khứ của loại gói đăng ký liên quan, mô hình sẽ không thể tính toán tỷ lệ giữ chân đáng tin cậy. Dự đoán được tính toán lại mỗi ngày với dữ liệu giao dịch mới nhất, vì vậy các giá trị của cùng một cohort có thể thay đổi khi có thêm doanh thu được ghi nhận. --- # File: ua-tracking-links --- --- title: "Tracking links trong Adapty Attribution" description: "Theo dõi các chiến dịch của bạn và đo lường hiệu quả ở bất kỳ đâu." --- Tracking link cho phép bạn đo lường nguồn gốc người dùng và kết nối lượt cài đặt với các chiến dịch quảng cáo. Khi ai đó nhấp vào quảng cáo của bạn, Adapty ghi lại lượt nhấp đó và sau đó khớp nó với sự kiện cài đặt được gửi bởi SDK. Nhờ vậy, bạn có thể xem kênh, chiến dịch, nhóm quảng cáo và quảng cáo nào mang lại doanh thu cao nhất trên [trang Analytics](ua-analytics) của mình. Bạn có thể tạo hai loại tracking link: - **One link** — một link toàn cầu tự động phát hiện nền tảng của người dùng, ghi lại lượt nhấp và chuyển hướng họ đến App Store hoặc Google Play. - **Liên kết theo cửa hàng** — liên kết nhắm mục tiêu theo nền tảng, vừa ghi lại lượt nhấp vừa tự động chuyển hướng người dùng đến App Store hoặc Google Play. Bạn cũng có thể thêm tham số deep link trì hoãn vào các liên kết này. ## Tạo tracking link \{#create-tracking-links\} Để tạo tracking link: 1. Trong Adapty Attribution dashboard, chọn **Tracking links** từ menu sidebar. Tại đó, nhấn **Create link**. 2. Chọn app của bạn từ danh sách và nhấn **Next**. 3. Điền các tham số link để khớp với chiến dịch và quảng cáo bạn muốn theo dõi. | Tham số | Mô tả | |-------------------|--------------------------------------------------------------------------------------------------------| | **Name** | Tên liên kết theo dõi nội bộ. | | **Channel** | Nguồn lưu lượng truy cập, chẳng hạn như Meta, Reddit hoặc TikTok. Dùng để nhóm các chiến dịch trong analytics. | | **Campaign ID** | Mã định danh duy nhất của chiến dịch trên nền tảng quảng cáo của bạn. | | **Campaign name** | Tên dễ đọc của chiến dịch. | | **Ad set ID** | Mã định danh duy nhất của nhóm quảng cáo (ad group) trên nền tảng quảng cáo của bạn. | | **Ad set name** | Tên của nhóm quảng cáo. | | **Ad ID** | Mã định danh duy nhất của mẫu quảng cáo riêng lẻ. | | **Ad name** | Tên của mẫu quảng cáo hoặc biến thể. | 4. Theo mặc định, bạn đang tạo One Link. Tính năng này tự động phát hiện nền tảng của người dùng và chuyển hướng họ đến App Store hoặc Google Play sau khi ghi nhận lượt nhấp. Nếu bạn muốn dùng các URL chuyển hướng riêng cho từng nền tảng, hãy bỏ chọn hộp kiểm **One Link** và cung cấp liên kết cửa hàng theo từng nền tảng theo cách thủ công. 5. Nhấn **Create**. 6. Mở trang tracking link của bạn và sao chép **Click link** từ một trong các phần sau: - **One link** – dùng link này để theo dõi lượt nhấp và tự động chuyển hướng người dùng đến đúng cửa hàng. - **iOS link** hoặc **Android link** — các phiên bản tùy chọn theo nền tảng nếu bạn muốn dùng link riêng cho từng cửa hàng. :::tip Bạn cũng có thể thiết lập thêm tham số link để [làm việc với dữ liệu deferred](ua-deferred-data). Ví dụ, bạn có thể triển khai deferred deep linking. ::: 7. Truy cập nền tảng quảng cáo của bạn và dán liên kết vào quảng cáo làm URL đích của quảng cáo. Bây giờ, các lượt cài đặt ứng dụng sẽ được khớp với quảng cáo và chiến dịch tương ứng, giúp bạn đo lường hiệu quả chiến dịch trên trang **Analytics**. --- # File: ua-deferred-data --- --- title: "Deferred deeplinks trong Adapty Attribution" description: "Thiết lập deferred deeplinks trong Adapty Attribution." --- Deferred deeplinks cho phép bạn truyền dữ liệu tùy chỉnh vào ứng dụng khi người dùng cài đặt ứng dụng sau khi nhấp vào quảng cáo của bạn. Ví dụ: bạn có thể điều hướng họ đến một vị trí cụ thể trong ứng dụng ngay sau khi họ cài đặt và khởi chạy ứng dụng lần đầu tiên. Đây là cách hoạt động: 1. Khi người dùng nhấp vào quảng cáo của bạn, Adapty lưu dữ liệu từ lượt nhấp đó. 2. Khi Adapty ghi nhận sự kiện cài đặt, nó sẽ lấy dữ liệu deferred từ lượt nhấp. 3. Sau khi người dùng cài đặt ứng dụng và khởi chạy lần đầu, Adapty truy xuất dữ liệu đã lưu và ứng dụng của bạn nhận được các tham số tùy chỉnh, cho phép bạn xử lý các giá trị khác nhau trong code ứng dụng. Adapty hỗ trợ các tham số deferred data sau: - `ios_deferred_data` - `android_deferred_data` - `deferred_data_sub[1-10]` Để thêm tham số deferred data, hãy gắn chúng vào link của bạn trong cài đặt chiến dịch: 1. Mở chiến dịch của bạn từ trang **Integrations -> Meta/TikTok Ads**. Hoặc mở tracking link từ trang **Tracking links**. Sao chép click link bạn sẽ dùng trong chiến dịch. 2. Trong nền tảng quảng cáo của bạn (Meta, TikTok, Google Ads, v.v.), dán link vào trường URL đích của quảng cáo, sau đó thêm các tham số dữ liệu hoãn vào dưới dạng query parameter — mỗi tham số có tiền tố `&`. Ví dụ: để đưa người dùng iOS đến màn hình 'Welcome' sau khi cài đặt, thêm `&ios_deferred_data=welcome`. URL đích cuối cùng sẽ có dạng như sau: ``` https://api-ua.adapty.io/api/v1/attribution/click?adpt_cid=__ADAPTY__ID__&ios_deferred_data=welcome&campaign_id=__CAMPAIGN_ID__&adset_id=__AID__&ad_id=__CID__&campaign_name=__CAMPAIGN_NAME__&adset_name=__AID_NAME__&ad_name=__CID_NAME__&redirect_url=__APP_LINK__ ``` 3. Xử lý các tham số trong code của ứng dụng. Lưu ý rằng các tham số deferred data nằm trong tham số `payload`, và `payload` là một JSON đã được escape, vì vậy bạn cần parse nó trong code ứng dụng. Ví dụ, đây là cách bạn có thể xử lý các lượt cài đặt có `ios_deferred_data` là `welcome`: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers Adapty.delegate = self nonisolated func onInstallationDetailsSuccess(_ details: AdaptyInstallationDetails) { guard let payloadStr = details.payload, let data = payloadStr.data(using: .utf8), let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let deeplink = payload["ios_deferred_data"] as? String, deeplink == "welcome" else { return } DispatchQueue.main.async { print("Navigate to welcome screen") // navigate to your screen here } } ``` </TabItem> <TabItem value="android" label="Kotlin"> ```kotlin showLineNumbers Adapty.setOnInstallationDetailsListener(object : OnInstallationDetailsListener { override fun onInstallationDetailsSuccess(details: AdaptyInstallationDetails) { details.payload?.let { runCatching { val json = JSONObject(it) if (json.optString("android_deferred_data") == "welcome") { println("Navigate to welcome screen") // navigate here } }.onFailure(Throwable::printStackTrace) } } }) ``` </TabItem> <TabItem value="rn" label="React Native" default> ```typescript showLineNumbers adapty.addEventListener('onInstallationDetailsSuccess', details => { // Parse the payload JSON and navigate to welcome screen if needed try { if (details.payload) { const payload = JSON.parse(details.payload); if (payload.ios_deferred_data === 'welcome') { // Navigate to welcome screen // Replace with your app's navigation logic // For example, using React Navigation: // navigation.navigate('Welcome'); console.log('Navigate to welcome screen'); } } } catch (error) { console.error('Error parsing installation details payload:', error); } }); ``` </TabItem> <TabItem value="flutter" label="Flutter"> ```dart showLineNumbers Adapty().onUpdateInstallationDetailsSuccessStream.listen((details) { final payloadStr = details.payload; if (payloadStr == null) return; final payload = json.decode(payloadStr) as Map<String, dynamic>; if (payload['ios_deferred_data'] == 'welcome') { print('Navigate to welcome screen'); } }); ``` </TabItem> </Tabs> --- # File: ua-attribution-data --- --- title: "Nhận dữ liệu attribution trong ứng dụng của bạn" description: "Truy cập dữ liệu attribution chiến dịch trong ứng dụng sau khi Adapty khớp một lượt cài đặt với một chiến dịch." --- Khi Adapty khớp một lượt cài đặt với một chiến dịch, nó sẽ trả về dữ liệu attribution về ứng dụng của bạn thông qua callback `onInstallationDetailsSuccess`. Sử dụng dữ liệu này để cá nhân hóa trải nghiệm người dùng dựa trên kênh hoặc chiến dịch đã dẫn đến lượt cài đặt. Dữ liệu attribution được trả về dưới dạng một đối tượng `attribution` lồng nhau bên trong trường `payload`. Nó chứa các trường sau: | Trường | Mô tả | |---|---| | `channel` | Kênh mua người dùng (ví dụ: `facebook`, `tiktok`, `google`, `organic`) | | `campaign_id` | Mã định danh chiến dịch | | `campaign_name` | Tên chiến dịch | | `adset_id` | Mã định danh nhóm quảng cáo / ad group | | `adset_name` | Tên nhóm quảng cáo / ad group | | `ad_id` | Mã định danh quảng cáo / creative | | `ad_name` | Tên quảng cáo / creative | Tất cả các trường đều là tùy chọn. Đối với các lượt cài đặt organic hoặc khi không thể xác định được attribution, trường `payload` sẽ không bao gồm đối tượng `attribution`. Để đọc dữ liệu attribution trong ứng dụng của bạn: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers Adapty.delegate = self nonisolated func onInstallationDetailsSuccess(_ details: AdaptyInstallationDetails) { guard let payloadDict = details.payload?.dictionary, let attribution = payloadDict["attribution"] as? [String: Any] else { return } let channel = attribution["channel"] as? String let campaignName = attribution["campaign_name"] as? String let adName = attribution["ad_name"] as? String print("Channel: \(channel ?? "organic")") } ``` </TabItem> <TabItem value="android" label="Kotlin"> ```kotlin showLineNumbers Adapty.setOnInstallationDetailsListener(object : OnInstallationDetailsListener { override fun onInstallationDetailsSuccess(details: AdaptyInstallationDetails) { val payloadStr = details.payload ?: return runCatching { val payload = JSONObject(payloadStr) val attribution = payload.optJSONObject("attribution") ?: return val channel = attribution.optString("channel") val campaignName = attribution.optString("campaign_name") val adName = attribution.optString("ad_name") println("Channel: $channel") }.onFailure(Throwable::printStackTrace) } }) ``` </TabItem> <TabItem value="rn" label="React Native"> ```typescript showLineNumbers adapty.addEventListener('onInstallationDetailsSuccess', details => { try { if (!details.payload) return; const payload = JSON.parse(details.payload); const attribution = payload.attribution; if (!attribution) return; const channel = attribution.channel; const campaignName = attribution.campaign_name; const adName = attribution.ad_name; console.log('Channel:', channel ?? 'organic'); } catch (error) { console.error('Error parsing payload:', error); } }); ``` </TabItem> <TabItem value="flutter" label="Flutter"> ```dart showLineNumbers Adapty().onUpdateInstallationDetailsSuccessStream.listen((details) { final payloadStr = details.payload; if (payloadStr == null) return; final payload = json.decode(payloadStr) as Map<String, dynamic>; final attribution = payload['attribution'] as Map<String, dynamic>?; if (attribution == null) return; final channel = attribution['channel'] as String?; final campaignName = attribution['campaign_name'] as String?; final adName = attribution['ad_name'] as String?; print('Channel: ${channel ?? 'organic'}'); }); ``` </TabItem> </Tabs> --- # File: ua-facebook --- --- title: "Tích hợp Meta Ads với Adapty Attribution" description: "Kết nối Meta Ads với Adapty Attribution để theo dõi và tối ưu hóa hiệu suất chiến dịch trên Facebook, Instagram, Messenger và Audience Network." --- Tích hợp Meta của Adapty Attribution cho phép bạn theo dõi và tối ưu hóa hiệu suất chiến dịch trên Facebook, Instagram, Messenger và Audience Network. :::tip Xem [hướng dẫn thiết lập quảng cáo trong Meta Ads Manager](meta-create-campaign) của chúng tôi. ::: ## Bước 1. Kết nối tài khoản Facebook của bạn \{#step-1-connect-your-facebook-account\} Để kết nối Meta Ads với Adapty Attribution, vào **Integrations > Meta** trên thanh sidebar bên trái. Bạn có hai lựa chọn: - **Continue with Facebook**: Kết nối qua OAuth. Dùng tùy chọn này nếu bạn đăng nhập Meta Ads Manager bằng tài khoản Facebook cá nhân hoặc doanh nghiệp. - **Add system token**: Kết nối bằng token của system user cố định. Dùng tùy chọn này nếu tổ chức của bạn quản lý tài khoản quảng cáo thông qua Meta Business system user. <Tabs> <TabItem value="oauth" label="Continue with Facebook"> :::important Đảm bảo tài khoản Facebook của bạn có quyền truy cập vào các chiến dịch và pixel bạn cần. ::: 1. Nhấp vào **Continue with Facebook**. 2. Đăng nhập bằng tài khoản Facebook của bạn và nhấp vào **Continue**. 3. Xem lại các quyền được yêu cầu và nhấp vào **Save**. </TabItem> <TabItem value="system" label="Add system token"> Tạo [system user access token](https://developers.facebook.com/documentation/ads-commerce/marketing-api/collaborative-ads/managed-partner-ads/api-guide/prerequisites/generate-access-token-system-user) trong Meta Business Settings, sau đó thêm vào Adapty Attribution. :::important Trước khi bắt đầu, bạn cần có một [system user](https://www.facebook.com/business/help/503306463479099) trong Meta Business portfolio của mình, với các tài khoản quảng cáo bạn muốn theo dõi đã được gán vào. Bạn cũng cần có một app được thêm vào portfolio — bạn sẽ chọn app này khi tạo token. ::: **Trong Meta Business Settings, tạo token:** 1. Truy cập **Business Settings**. 2. Trong mục **Users**, chọn **System users**. 3. Chọn system user của bạn, rồi nhấn **Generate new token**. 4. Chọn app của bạn từ danh sách thả xuống. 5. Trong danh sách quyền, bật `ads_read`. Đây là quyền duy nhất Adapty Attribution cần để đọc dữ liệu chiến dịch và quảng cáo của bạn. 6. Nhấn **Generate token**. 7. Sao chép token và lưu trữ an toàn. Meta chỉ hiển thị token một lần duy nhất. :::note Cài đặt **Token expiration** kiểm soát thời gian kết nối duy trì hoạt động. Token có ngày hết hạn phải được tạo lại và kết nối lại trước khi hết hạn, nếu không attribution sẽ dừng lại. Token không có ngày hết hạn giúp tránh vấn đề này, nhưng đây là thông tin xác thực tồn tại lâu dài. Hãy lưu trữ an toàn và thu hồi ngay nếu bị lộ. ::: **Trong Adapty Attribution, thêm token:** 1. Nhấn **Add system token**. 2. Dán token vào và nhấn **Connect**. </TabItem> </Tabs> Sau đó, tất cả tài khoản quảng cáo của bạn sẽ được thêm vào Adapty Attribution. Bạn có thể tiến hành thêm các chiến dịch. ## Bước 2. Thêm chiến dịch \{#step-2-add-campaigns\} Để thêm chiến dịch Meta vào Adapty Attribution và theo dõi hiệu quả quảng cáo Meta trong Adapty: 1. Chuyển sang tab **Web Campaigns** và nhấn **Create configuration**. 2. Trong tab **General**, mở rộng phần **iOS** và/hoặc **Android** rồi dán URL ứng dụng từ App Store và/hoặc Google Play. 3. Sao chép giá trị trong trường **Click link**. Sau đó, trong Meta Ads Manager, mở quảng cáo của bạn và dán link này vào. Điều này giúp Adapty kết nối các lượt cài đặt và giao dịch mua với quảng cáo trên Meta. 4. Để gửi các sự kiện chuyển đổi ngược lại cho Meta, bạn cũng có thể liên kết các pixel Meta với các chiến dịch trong Adapty Attribution. Để thực hiện, hãy chọn một trong các pixel hiện có của bạn trong dropdown **Pixel**. Sau khi chọn pixel, bạn có thể nhấn **Send test event** để xác minh kết nối. ## Bước 3. Ánh xạ sự kiện \{#step-3-map-events\} Để gửi các sự kiện chuyển đổi về Meta nhằm tối ưu hóa chiến dịch, bạn cần cấu hình ánh xạ sự kiện trong phần **Events names**. Điều này cho phép Adapty tự động gửi các sự kiện gói đăng ký đến Meta pixel của bạn khi người dùng thực hiện hành động trong ứng dụng. Trong phần **Events names**, bật các sự kiện bạn muốn theo dõi trong Meta Ads Manager. Với mỗi sự kiện được bật, chọn sự kiện Meta tương ứng từ danh sách thả xuống hoặc đặt một sự kiện tùy chỉnh. Theo mặc định, Adapty ánh xạ các sự kiện Adapty sang các sự kiện chuẩn của Meta. Nhấp **Save** để áp dụng cấu hình ánh xạ sự kiện. ## Cấu hình bổ sung \{#additional-configuration\} ### Tham số bổ sung \{#additional-parameters\} Trường **Additional parameter** cho phép bạn thêm các điểm dữ liệu tùy chỉnh để phân tích bên ngoài Adapty. Tính năng này hữu ích khi bạn cần truyền dữ liệu chiến dịch hoặc dữ liệu người dùng cụ thể đến các công cụ phân tích bên ngoài hoặc đối tác attribution. Trong trường **Additional parameter**, nhập bất kỳ dữ liệu tùy chỉnh nào bạn muốn đính kèm vào quá trình theo dõi attribution. Tham số bổ sung này sẽ được bao gồm trong toàn bộ dữ liệu attribution gửi đến Meta và có thể dùng để phân tích và tối ưu hóa chiến dịch nâng cao. Ví dụ, nếu bạn đang chạy nhiều biến thể của cùng một chiến dịch, bạn có thể thêm `variant=A` hoặc `variant=B` để phân biệt các hướng tiếp cận sáng tạo khác nhau. :::important Các tham số bổ sung sẽ thay đổi **Click link** mà bạn dán vào Meta Ads Manager. Nếu bạn đã sao chép link này ở đó và thêm tham số tùy chỉnh sau, hãy đảm bảo bạn sao chép và dán lại click link đã cập nhật có chứa tham số tùy chỉnh đó. ::: <br/> ### Cài đặt \{#settings\} Tab **Settings** kiểm soát cách Adapty khớp hành động người dùng với các chiến dịch Meta Ads của bạn. Các cài đặt này xác định khoảng thời gian cho cả hai phương thức khớp attribution: tất định (deterministic) và xác suất (probabilistic). Để cấu hình, hãy vào tab **Settings** trong cài đặt chiến dịch của bạn. Tại đây bạn sẽ thấy hai cài đặt chính cần điều chỉnh: - **Cửa sổ khớp xác định**: Sử dụng mã định danh thiết bị chính xác (như IDFA trên iOS hoặc Advertising ID trên Android) để khớp người dùng với các chiến dịch với độ chính xác cao. Đặt thời gian này thành 168 giờ (7 ngày) để đạt độ chính xác attribution tối đa — đây là giá trị mặc định và được khuyến nghị. Khi người dùng nhấp vào quảng cáo Meta và cài đặt ứng dụng trong khoảng thời gian này, Adapty có thể xác định chắc chắn rằng lượt cài đặt đó đến từ lần nhấp vào quảng cáo cụ thể đó thông qua mã định danh thiết bị. - **Probabilistic matching window**: Cửa sổ này sử dụng mô hình thống kê và device fingerprinting để ghép người dùng khi không thể thực hiện deterministic matching. Đặt giá trị này là 6 giờ cho hầu hết các chiến dịch - đây là giá trị mặc định và hoạt động tốt trong hầu hết các trường hợp. Đối với các chiến dịch có lượng click lớn, bạn có thể giảm xuống còn 1-2 giờ. Với những người dùng không thể ghép theo cách deterministic (do cài đặt quyền riêng tư hoặc các yếu tố khác), Adapty sẽ sử dụng probabilistic matching trong khoảng thời gian ngắn hơn này. Nhấn **Save** để áp dụng cài đặt. ### Ghi đè doanh thu \{#revenue-override\} Nếu bạn theo dõi các sự kiện dùng thử và muốn Meta ghi nhận doanh thu cho chúng, hãy sử dụng phần **Revenue override**. Phần này xuất hiện khi sự kiện **Trial started** được bật. Với mỗi mục tiêu sự kiện dùng thử, hãy đặt tỷ lệ phần trăm giá gói đăng ký để báo cáo là doanh thu. Ví dụ: nếu đặt 30%, Adapty sẽ gửi 30% giá gói đăng ký làm giá trị chuyển đổi cho các sự kiện dùng thử. Để thêm một ghi đè: 1. Bật **Trial started** trong phần **Events names**. 2. Trong **Revenue override**, nhấn **Add override**. 3. Chọn sự kiện mục tiêu và nhập phần trăm doanh thu (0–100). 4. Nhấn **Save**. ### Gửi tất cả sự kiện \{#send-all-events\} Theo mặc định, Adapty chỉ gửi sự kiện đến pixel của bạn cho những người dùng được quy attribution vào chiến dịch Meta. Bật **Send all events** để cũng chuyển tiếp sự kiện từ người dùng organic và không được quy attribution đến pixel. Khi được bật, mọi sự kiện cài đặt và giao dịch đều được gửi đến pixel, bất kể attribution chiến dịch. Dùng tùy chọn này để cung cấp cho Meta dữ liệu chuyển đổi rộng hơn cho việc mô hình hóa đối tượng và tối ưu chiến dịch. Để bật tùy chọn này, trong cài đặt chiến dịch, chọn **Send all events (forward organic/non-attributed events to this pixel)** và nhấn **Save**. --- # File: ua-tiktok --- --- title: "Tích hợp TikTok for Business với Adapty Attribution" description: "Kết nối TikTok for Business với Adapty Attribution để theo dõi và tối ưu hóa hiệu suất chiến dịch trong TikTok Ads Manager." --- Tích hợp TikTok for Business trong Adapty Attribution cho phép bạn theo dõi và tối ưu hóa hiệu suất chiến dịch trên TikTok. :::tip Xem [hướng dẫn thiết lập quảng cáo trong TikTok for Business](tiktok-create-campaign) của chúng tôi. ::: ## Bước 1. Kết nối tài khoản TikTok của bạn \{#step-1-connect-your-tiktok-account\} 1. Vào **Integrations > TikTok Ads** từ thanh bên trái và nhấp vào **Continue with TikTok**. 2. Đăng nhập bằng tài khoản TikTok của bạn và nhấp **Continue**. 3. Xem lại các quyền được yêu cầu và nhấp **Save**. Sau đó, tất cả tài khoản quảng cáo của bạn sẽ được thêm vào Adapty UA. Bạn có thể tiến hành thêm các chiến dịch. ## Bước 2. Thêm chiến dịch \{#step-2-add-campaigns\} Để thêm chiến dịch TikTok for Business vào Adapty Attribution và theo dõi hiệu quả quảng cáo TikTok của bạn trong Adapty: 1. Chuyển sang tab **Web Campaigns** và nhấp **Create configuration**. 2. Trong tab **General**, mở rộng phần **iOS** và/hoặc **Android** rồi dán URL ứng dụng trên App Store và/hoặc Google Play. 3. Sao chép giá trị trong trường **Click link**. Sau đó, trong TikTok Ads Manager, khi tạo quảng cáo, dán giá trị này vào trường **Tracking URL** trong phần **Advanced Settings**. Thao tác này sẽ cho phép Adapty kết nối các lượt cài đặt và mua hàng với quảng cáo trên TikTok. 4. (Tùy chọn) Để gửi các sự kiện chuyển đổi trở lại TikTok, bạn cũng có thể liên kết các pixel TikTok của mình với các chiến dịch trong Adapty Attribution. Để thực hiện, hãy chọn một trong các pixel hiện có của bạn trong danh sách thả xuống **Pixel**. Sau khi chọn pixel, bạn có thể nhấp vào **Send test event** để xác minh kết nối. ## Bước 3. Ánh xạ sự kiện \{#step-3-map-events\} Để gửi các sự kiện chuyển đổi về TikTok nhằm tối ưu hóa chiến dịch, bạn cần cấu hình ánh xạ sự kiện trong phần **Events names**. Điều này cho phép Adapty tự động gửi các sự kiện gói đăng ký đến TikTok pixel của bạn khi người dùng thực hiện các hành động trong ứng dụng. Trong phần **Events names**, bật các sự kiện bạn muốn theo dõi trong TikTok Ads Manager. Với mỗi sự kiện được bật, hãy chọn sự kiện TikTok tương ứng từ danh sách thả xuống hoặc tự đặt tên tùy chỉnh. Theo mặc định, Adapty ánh xạ các sự kiện Adapty sang các sự kiện chuẩn của TikTok. Nhấn **Save** để áp dụng cấu hình ánh xạ sự kiện của bạn. ## Cấu hình bổ sung \{#additional-configuration\} ### Thông số bổ sung \{#additional-parameters\} Trường **Additional parameter** cho phép bạn thêm các điểm dữ liệu tùy chỉnh để phân tích bên ngoài Adapty. Tính năng này hữu ích khi bạn cần truyền dữ liệu chiến dịch hoặc dữ liệu người dùng cụ thể đến các công cụ phân tích bên ngoài hoặc đối tác attribution. Trong trường **Additional parameter**, nhập bất kỳ dữ liệu tùy chỉnh nào bạn muốn đưa vào quá trình theo dõi attribution. Thông số bổ sung này sẽ được đưa vào tất cả dữ liệu attribution gửi đến TikTok và có thể dùng để phân tích cũng như tối ưu hóa chiến dịch nâng cao. Ví dụ, nếu bạn đang chạy nhiều biến thể của cùng một chiến dịch, bạn có thể thêm `variant=A` hoặc `variant=B` để phân biệt các cách tiếp cận sáng tạo khác nhau. :::important Các tham số bổ sung sẽ thay đổi **Click link** mà bạn dán vào TikTok Ads Manager. Nếu bạn đã sao chép link này ở đó và thêm tham số tùy chỉnh sau đó, hãy đảm bảo sao chép và dán lại click link đã được cập nhật có chứa tham số tùy chỉnh. ::: <br/> ### Cài đặt \{#settings\} Tab **Settings** kiểm soát cách Adapty khớp hành động của người dùng với các chiến dịch TikTok Ads của bạn. Các cài đặt này xác định khoảng thời gian cho cả việc khớp attribution xác định (deterministic) và xác suất (probabilistic). Để cấu hình, hãy vào tab **Settings** trong cấu hình chiến dịch của bạn. Tại đây bạn sẽ thấy hai cài đặt chính cần điều chỉnh: - **Cửa sổ khớp xác định**: Phương thức này sử dụng định danh thiết bị chính xác (như IDFA trên iOS hoặc Advertising ID trên Android) để khớp người dùng với các chiến dịch với độ chính xác cao. Đặt giá trị này thành 168 giờ (7 ngày) để đạt độ chính xác attribution tối đa — đây là giá trị mặc định và được khuyến nghị. Khi người dùng nhấp vào quảng cáo TikTok của bạn và cài đặt ứng dụng trong khoảng thời gian này, Adapty có thể xác định attribution cho lượt cài đặt đó đối với lần nhấp quảng cáo cụ thể đó bằng cách sử dụng định danh thiết bị. - **Probabilistic matching window**: Tùy chọn này sử dụng mô hình thống kê và device fingerprinting để ghép nối người dùng khi không thể thực hiện khớp xác định. Đặt giá trị này là 6 giờ cho hầu hết các chiến dịch — đây là mặc định và hoạt động tốt trong hầu hết các trường hợp. Đối với các chiến dịch có lượng click cao, bạn có thể giảm xuống còn 1-2 giờ. Với những người dùng không thể khớp xác định (do cài đặt quyền riêng tư hoặc các yếu tố khác), Adapty sẽ sử dụng probabilistic matching trong khoảng thời gian ngắn hơn này. Nhấn **Save** để áp dụng cài đặt. ### Ghi đè doanh thu \{#revenue-override\} Nếu bạn theo dõi các sự kiện dùng thử và muốn TikTok ghi nhận doanh thu cho chúng, hãy sử dụng phần **Revenue override**. Phần này xuất hiện khi sự kiện **Trial started** được bật. Với mỗi mục tiêu sự kiện dùng thử, hãy đặt phần trăm giá gói đăng ký để báo cáo làm doanh thu. Ví dụ: nếu đặt 30%, Adapty sẽ gửi 30% giá gói đăng ký làm giá trị chuyển đổi cho các sự kiện dùng thử. Để thêm một ghi đè: 1. Bật **Trial started** trong phần **Events names**. 2. Trong **Revenue override**, nhấp vào **Add override**. 3. Chọn sự kiện mục tiêu và nhập phần trăm doanh thu (0–100). 4. Nhấp vào **Save**. ### Gửi tất cả sự kiện \{#send-all-events\} Theo mặc định, Adapty chỉ gửi sự kiện đến pixel của bạn cho những người dùng được attribution vào một chiến dịch TikTok. Bật **Send all events** để cũng chuyển tiếp các sự kiện từ người dùng organic và không được attribution đến pixel. Khi được bật, mọi sự kiện cài đặt và giao dịch đều được gửi đến pixel, bất kể attribution chiến dịch. Dùng tính năng này để cung cấp cho TikTok dữ liệu chuyển đổi rộng hơn nhằm mô hình hóa đối tượng và tối ưu chiến dịch. Để bật tùy chọn này, trong cài đặt chiến dịch, chọn **Send all events (forward organic/non-attributed events to this pixel)** và nhấp **Save**. --- # File: ua-funnelfox --- --- title: "Tích hợp FunnelFox với Adapty Attribution" description: "Kết nối các funnel web-to-app của FunnelFox với Adapty Attribution để theo dõi toàn bộ hành trình thu hút người dùng từ điểm chạm trên web đến người đăng ký trả phí." --- [FunnelFox](https://funnelfox.com) là nền tảng xây dựng web2app funnel, cho phép bạn thu hút và tính phí người dùng bên ngoài App Store — bỏ qua phí và các hạn chế của App Store. Sau khi kết nối, FunnelFox gửi các sự kiện giao dịch đến Adapty Attribution, cung cấp cho bạn toàn bộ đường dẫn attribution từ điểm chạm web đến người dùng đã trả tiền. Để thiết lập tích hợp, hãy liên kết một hoặc nhiều dự án FunnelFox với ứng dụng Adapty của bạn bằng **Project ID**. ## Cách hoạt động \{#how-it-works\} Khi người dùng hoàn tất một giao dịch mua trong funnel FunnelFox của bạn, FunnelFox sẽ gửi sự kiện giao dịch đó tới Adapty Attribution. Adapty sử dụng **Project ID** để xác định ứng dụng nào mà giao dịch thuộc về. Sự kiện sau đó được lưu trữ và hiển thị trong Adapty Attribution analytics của bạn. Mỗi giao dịch bao gồm: - **Sự kiện vòng đời gói đăng ký**: bắt đầu, gia hạn, hủy, chuyển đổi dùng thử, hoàn tiền, và nhiều hơn nữa - **Dữ liệu attribution**: chiến dịch, adset và ID quảng cáo; tham số UTM; click ID của nền tảng (fbclid, ttclid, gclid) - **Dữ liệu funnel và thử nghiệm**: tên funnel và tên thử nghiệm của FunnelFox, giúp bạn so sánh các biến thể A/B test Adapty tự động xác định **kênh** (Facebook, TikTok, Google, hoặc organic) từ click ID trong giao dịch. Bạn không cần cấu hình thủ công. :::note Các giao dịch của FunnelFox sử dụng **ngày thanh toán đầu tiên** làm ngày cohort thay vì ngày cài đặt, vì các giao dịch mua trên web không có sự kiện cài đặt ứng dụng. ::: ## Cấu hình tích hợp \{#configure-integration\} ### Bước 1. Lấy Project ID trong FunnelFox \{#step-1-get-your-project-id-in-funnelfox\} 1. Trong dashboard FunnelFox của bạn, nhấp **Settings** ở thanh bên trái. 2. Trong phần **Project info**, sao chép giá trị **ID**. ### Bước 2. Thêm dự án trong Adapty UA \{#step-2-add-the-project-in-adapty-ua\} 1. Trong Adapty UA, vào [**Integrations > FunnelFox**](https://app.adapty.io/ua/integrations/funnelfox). 2. Dán Project ID bạn đã sao chép từ FunnelFox. 3. Nhấp **Save**. Để kết nối thêm các dự án FunnelFox, nhấp **Add project** và lặp lại cả hai bước cho từng dự án. --- # File: ua-custom-s3 --- --- title: "Custom S3 trong Adapty Attribution" description: "Xuất dữ liệu thu hút người dùng sang bộ nhớ tương thích S3 tùy chỉnh của bạn để phân tích và báo cáo nâng cao." --- Tích hợp của Adapty Attribution với custom S3-compatible storage cho phép bạn lưu trữ dữ liệu chiến dịch thu hút người dùng một cách an toàn trong giải pháp lưu trữ S3-compatible của riêng bạn. Bạn có thể lưu dữ liệu hiệu suất chiến dịch, dữ liệu attribution và các sự kiện thu hút người dùng vào S3 bucket tùy chỉnh của mình dưới dạng file .csv. Để thiết lập tích hợp này, bạn cần thực hiện một vài bước đơn giản trong console S3-compatible storage và Adapty Attribution dashboard của mình. :::note Adapty Attribution gửi dữ liệu của bạn **mỗi 24h** lúc 4:00 UTC. Mỗi file sẽ chứa dữ liệu cho các sự kiện được tạo trong toàn bộ ngày dương lịch trước đó theo UTC. Ví dụ: dữ liệu được xuất tự động lúc 4:00 UTC ngày 8 tháng 3 sẽ chứa tất cả các sự kiện được tạo vào ngày 7 tháng 3 từ 00:00:00 đến 23:59:59 theo UTC. ::: ## Thiết lập tích hợp Custom S3 \{#set-up-custom-s3-integration\} Để bắt đầu nhận dữ liệu, hãy cấu hình tích hợp trong Adapty Attribution: 1. Truy cập [**Integrations** -> **Custom S3**](https://app.adapty.io/ua/integrations/custom-s3) 2. Bật nút chuyển **Export install events to custom S3**. 3. Điền vào các trường bắt buộc để thiết lập kết nối giữa bộ nhớ S3 tùy chỉnh của bạn và các hồ sơ người dùng Adapty Attribution. | Field | Description | |:----------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Access Key ID** | Mã định danh duy nhất dùng để xác thực quyền truy cập của người dùng hoặc ứng dụng vào dịch vụ lưu trữ tương thích S3. Tìm ID này trong bảng điều khiển của nhà cung cấp dịch vụ lưu trữ. | | **Secret Access Key** | Khóa riêng tư được dùng kết hợp với Access Key ID để xác thực quyền truy cập của người dùng hoặc ứng dụng vào dịch vụ lưu trữ tương thích S3. Tìm khóa này trong bảng điều khiển của nhà cung cấp dịch vụ lưu trữ. | | **S3 Bucket Name** | Tên duy nhất toàn cầu dùng để xác định một S3 bucket cụ thể trong môi trường lưu trữ của bạn. S3 bucket là dịch vụ lưu trữ đơn giản cho phép người dùng lưu trữ và truy xuất các đối tượng dữ liệu như tệp và hình ảnh trên đám mây. | | **Region** (Tùy chọn) | Lấy Region của bạn từ Management Console. | | **Folder Inside the Bucket** (Tùy chọn) | Tên thư mục bạn muốn tạo bên trong S3 bucket đã chọn. Lưu ý rằng S3 mô phỏng thư mục bằng cách sử dụng tiền tố khóa đối tượng, về cơ bản chính là tên thư mục. | | **Custom Endpoint URL** | URL endpoint cho dịch vụ lưu trữ tương thích S3 của bạn. Thông tin này do nhà cung cấp dịch vụ lưu trữ cung cấp (ví dụ: MinIO, DigitalOcean Spaces, Wasabi, v.v.). | :::note Bạn cũng có thể chỉ định các thư mục con trong trường tên S3 bucket, ví dụ: `adapty-ua-events/com.sample-app` ::: ## Xuất dữ liệu thủ công \{#manual-data-export\} Ngoài tính năng xuất dữ liệu sự kiện tự động sang bộ nhớ S3 tùy chỉnh, Adapty UA còn cung cấp chức năng xuất file thủ công. Với tính năng này, bạn có thể chọn ngày cho dữ liệu thu hút người dùng và xuất thủ công vào S3 bucket của mình. Điều này giúp bạn kiểm soát tốt hơn dữ liệu cần xuất và thời điểm xuất. ## Cấu trúc bảng \{#table-structure\} Trong tích hợp S3 tùy chỉnh, Adapty Attribution cung cấp một bảng để lưu trữ dữ liệu lịch sử cho các sự kiện cài đặt. Bảng chứa thông tin về hồ sơ người dùng, doanh thu và lợi nhuận, cửa hàng gốc, cùng nhiều điểm dữ liệu khác. :::warning Lưu ý rằng cấu trúc này có thể mở rộng theo thời gian — khi chúng tôi hoặc các bên thứ ba mà chúng tôi hợp tác giới thiệu dữ liệu mới. Hãy đảm bảo rằng code xử lý dữ liệu của bạn đủ linh hoạt, dựa vào các trường cụ thể thay vì toàn bộ cấu trúc. ::: Dưới đây là cấu trúc bảng cho các sự kiện: | Cột | Mô tả | |--------------------------|-------------------------------------------| | `adapty_profile_id` | Mã định danh hồ sơ người dùng Adapty | | `install_id` | Mã định danh cài đặt duy nhất | | `created_at` | Thời điểm tạo bản ghi (ISO 8601) | | `installed_at` | Thời điểm cài đặt ứng dụng (ISO 8601) | | `store` | Cửa hàng ứng dụng (`ios`, `android`) | | `country` | Mã quốc gia của người dùng (ISO 3166-1 alpha-2) | | `ip_address` | Địa chỉ IP của client | | `idfa` | iOS Identifier for Advertisers | | `idfv` | iOS Identifier for Vendors | | `gaid` | Google Advertising ID (Android) | | `android_id` | ID thiết bị Android | | `app_set_id` | Android App Set ID | | `bundle_id` | Mã bundle của ứng dụng (ví dụ: `com.example.app`) | | `device_brand` | Thương hiệu thiết bị (ví dụ: `Apple`, `Samsung`) | | `device_model` | Model thiết bị (ví dụ: `iPhone15,2`) | | `os_version` | Phiên bản hệ điều hành chính | | `app_version` | Phiên bản ứng dụng do SDK Adapty báo cáo | | `sdk_version` | Phiên bản SDK Adapty | | `channel` | Kênh attribution | | `campaign_id` | Mã định danh chiến dịch | | `campaign_name` | Tên chiến dịch | | `adset_id` | Mã định danh nhóm quảng cáo | | `adset_name` | Tên nhóm quảng cáo | | `ad_id` | Mã định danh quảng cáo | | `ad_name` | Tên quảng cáo | | `keyword_id` | Mã định danh từ khóa | | `keyword_name` | Tên từ khóa | | `asa_org_id` | ID tổ chức Apple Search Ads | | `asa_keyword_match_type` | Loại khớp từ khóa ASA (`Exact`, `Broad`) | | `asa_attribution` | Dữ liệu attribution ASA (chuỗi JSON) | | `asa_conversion_type` | Loại chuyển đổi ASA | | `asa_country_or_region` | Quốc gia hoặc khu vực ASA | | `asa_creative_set_name` | Tên bộ sáng tạo ASA | | `fbclid` | Facebook Click ID | | `ttclid` | TikTok Click ID | | `utm_source` | Tham số UTM source | | `utm_medium` | Tham số UTM medium | | `utm_campaign` | Tham số UTM campaign | | `utm_term` | Tham số UTM term | | `utm_content` | Tham số UTM content | --- # File: ua-amazon-s3 --- --- title: "Amazon S3 trong Adapty Attribution" description: "Xuất dữ liệu thu hút người dùng sang S3 để phân tích và báo cáo nâng cao." --- Tích hợp Adapty Attribution với Amazon S3 cho phép bạn lưu trữ dữ liệu chiến dịch thu thập người dùng một cách an toàn tại một vị trí trung tâm. Bạn có thể lưu dữ liệu hiệu suất chiến dịch, dữ liệu attribution và các sự kiện thu thập người dùng vào bucket Amazon S3 dưới dạng file .csv. Để thiết lập tích hợp này, bạn cần thực hiện một vài bước đơn giản trong AWS Console và Adapty Attribution dashboard. :::note Adapty Attribution gửi dữ liệu của bạn mỗi **24h** lúc 4:00 UTC. Mỗi file sẽ chứa dữ liệu cho các sự kiện được tạo trong toàn bộ ngày dương lịch trước đó theo giờ UTC. Ví dụ: dữ liệu được xuất tự động lúc 4:00 UTC ngày 8 tháng 3 sẽ chứa tất cả các sự kiện được tạo vào ngày 7 tháng 3 từ 00:00:00 đến 23:59:59 theo giờ UTC. ::: ## Cách thiết lập tích hợp Amazon S3 \{#how-to-set-up-amazon-s3-integration\} Để bắt đầu nhận dữ liệu, bạn cần có các thông tin xác thực sau: 1. Access key ID 2. Secret access key 3. Tên S3 bucket 4. Tên thư mục bên trong S3 bucket :::note Thư mục lồng nhau Bạn có thể chỉ định các thư mục lồng nhau trong trường tên Amazon S3 bucket, ví dụ: adapty-ua-events/com.sample-app ::: ### Bước 1. Tạo thông tin xác thực Amazon S3 \{#step-1-create-amazon-s3-credentials\} Hướng dẫn này sẽ giúp bạn tạo các thông tin xác thực cần thiết trong AWS Console. #### 1.1. Tạo Access Policy \{#11-create-access-policy\} 1. Truy cập [IAM Policy Dashboard](https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/policies) trong AWS Console của bạn 2. Chọn tùy chọn **Create Policy** <img src="/assets/shared/img/7af075c-CleanShot_2023-03-21_at_10.52.002x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong Policy editor, dán đoạn JSON sau và thay `adapty-s3-integration-test` bằng tên bucket của bạn: ```json showLineNumbers title="Json" { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowListObjectsInBucket", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::adapty-s3-integration-test" }, { "Sid": "AllowAllObjectActions", "Effect": "Allow", "Action": "s3:*Object", "Resource": [ "arn:aws:s3:::adapty-s3-integration-test/*", "arn:aws:s3:::adapty-s3-integration-test" ] }, { "Sid": "AllowBucketLocation", "Effect": "Allow", "Action": "s3:GetBucketLocation", "Resource": "arn:aws:s3:::adapty-s3-integration-test" } ] } ``` <img src="/assets/shared/img/d4e474a-CleanShot_2023-03-21_at_10.56.212x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Sau khi hoàn tất cấu hình policy, bạn có thể chọn thêm tag (tùy chọn) rồi nhấn **Next** để tiếp tục bước cuối cùng 5. Ở bước này, bạn đặt tên cho policy và nhấn nút **Create policy** để hoàn tất quá trình tạo <img src="/assets/shared/img/7dcb02f-CleanShot_2023-03-21_at_11.03.372x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> #### 1.2. Tạo IAM user \{#12-create-iam-user\} Để cho phép Adapty Attribution tải các báo cáo dữ liệu thô lên bucket của bạn, bạn cần cung cấp Access Key ID và Secret Access Key cho một user có quyền ghi vào bucket đó. 1. Điều hướng đến IAM Console và chọn [mục Users](https://console.aws.amazon.com/iamv2/home#/users) 2. Nhấn vào nút **Add users** <img src="/assets/shared/img/bb612c8-CleanShot_2023-03-21_at_11.12.392x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Đặt tên cho người dùng, chọn **Access key – Programmatic access**, rồi tiến hành cài đặt quyền <img src="/assets/shared/img/467ee4d-j6aoX.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Ở bước tiếp theo, hãy chọn tùy chọn **Add user to group** rồi nhấp vào nút **Create group** <img src="/assets/shared/img/bfd0e80-CleanShot_2023-03-21_at_11.24.592x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Tiếp theo, bạn cần đặt tên cho User Group và chọn policy mà bạn đã tạo trước đó 6. Sau khi chọn xong policy, nhấn vào nút **Create group** để hoàn tất quá trình <img src="/assets/shared/img/df29c12-CleanShot_2023-03-21_at_11.28.052x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Sau khi tạo nhóm thành công, hãy **chọn nhóm đó** và tiếp tục bước tiếp theo <img src="/assets/shared/img/1f3722e-CleanShot_2023-03-21_at_11.36.192x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 8. Đây là bước cuối cùng của phần này, bạn chỉ cần nhấn vào nút **Create User** để tiếp tục <img src="/assets/shared/img/ea43722-CleanShot_2023-03-21_at_11.40.462x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 9. Cuối cùng, bạn có thể **tải thông tin đăng nhập xuống dưới dạng .csv** hoặc sao chép và dán trực tiếp từ dashboard <img src="/assets/shared/img/bcf35e1-S3created.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Bước 2. Cấu hình tích hợp trong Adapty Attribution \{#step-2-configure-integration-in-adapty-attribution\} 1. Truy cập [**Integrations** -> **Amazon S3**](https://app.adapty.io/ua/integrations/s3) 2. Bật toggle **Export install events to Amazon S3**. 3. Điền vào các trường sau để thiết lập kết nối giữa Amazon S3 và các hồ sơ người dùng Adapty Attribution: | Trường | Mô tả | |:-----------------------------| :----------------------------------------------------------- | | **Access Key ID** | Mã định danh duy nhất dùng để xác thực quyền truy cập của người dùng hoặc ứng dụng vào dịch vụ AWS. Tìm ID này trong [tệp csv](ua-amazon-s3#step-1-create-amazon-s3-credentials) đã tải xuống. | | **Secret Access Key** | Khóa riêng tư dùng kết hợp với Access Key ID để xác thực quyền truy cập của người dùng hoặc ứng dụng vào dịch vụ AWS. Tìm khóa này trong [tệp csv](ua-amazon-s3#step-1-create-amazon-s3-credentials) đã tải xuống. | | **S3 Bucket Name** | Tên duy nhất trên toàn cầu để xác định một S3 bucket cụ thể trong đám mây AWS. S3 bucket là dịch vụ lưu trữ đơn giản cho phép người dùng lưu trữ và truy xuất các đối tượng dữ liệu như tệp và hình ảnh trên đám mây. | | **Folder Inside the Bucker** | Tên thư mục bạn muốn tạo bên trong S3 bucket đã chọn. Lưu ý rằng S3 mô phỏng thư mục bằng cách sử dụng tiền tố khóa đối tượng, về cơ bản chính là tên thư mục. | | **Region** (Tùy chọn) | Lấy Region của bạn từ AWS Management Console trong tài khoản người dùng IAM. | ## Xuất dữ liệu thủ công \{#manual-data-export\} Ngoài tính năng tự động xuất dữ liệu sự kiện sang Amazon S3, Adapty UA còn cung cấp chức năng xuất file thủ công. Với tính năng này, bạn có thể chọn một ngày cụ thể để lấy dữ liệu thu hút người dùng và xuất sang S3 bucket theo cách thủ công. Điều này giúp bạn kiểm soát tốt hơn dữ liệu cần xuất và thời điểm xuất. ## Cấu trúc bảng \{#table-structure\} Trong tích hợp AWS S3, Adapty Attribution cung cấp một bảng để lưu trữ dữ liệu lịch sử cho các sự kiện cài đặt. Bảng chứa thông tin về hồ sơ người dùng, doanh thu và lợi nhuận, cửa hàng gốc, cùng nhiều điểm dữ liệu khác. :::warning Lưu ý rằng cấu trúc này có thể phát triển theo thời gian — khi có dữ liệu mới được chúng tôi hoặc các bên thứ ba mà chúng tôi hợp tác giới thiệu. Hãy đảm bảo rằng code xử lý dữ liệu của bạn đủ linh hoạt và chỉ phụ thuộc vào các trường cụ thể, không phụ thuộc vào toàn bộ cấu trúc. ::: Đây là cấu trúc bảng cho các sự kiện: | Cột | Mô tả | |--------------------------|---------------------------------------------------| | `adapty_profile_id` | Mã định danh hồ sơ người dùng Adapty | | `install_id` | Mã định danh cài đặt duy nhất | | `created_at` | Thời điểm tạo bản ghi (ISO 8601) | | `installed_at` | Thời điểm cài đặt ứng dụng (ISO 8601) | | `store` | Cửa hàng ứng dụng (`ios`, `android`) | | `country` | Mã quốc gia của người dùng (ISO 3166-1 alpha-2) | | `ip_address` | Địa chỉ IP của client | | `idfa` | iOS Identifier for Advertisers | | `idfv` | iOS Identifier for Vendors | | `gaid` | Google Advertising ID (Android) | | `android_id` | ID thiết bị Android | | `app_set_id` | Android App Set ID | | `channel` | Kênh attribution | | `campaign_id` | Mã định danh chiến dịch | | `campaign_name` | Tên chiến dịch | | `adset_id` | Mã định danh nhóm quảng cáo | | `adset_name` | Tên nhóm quảng cáo | | `ad_id` | Mã định danh quảng cáo | | `ad_name` | Tên quảng cáo | | `keyword_id` | Mã định danh từ khóa | | `keyword_name` | Tên từ khóa | | `asa_org_id` | ID tổ chức Apple Search Ads | | `asa_keyword_match_type` | Kiểu khớp từ khóa ASA (`Exact`, `Broad`) | | `asa_attribution` | Dữ liệu attribution ASA (chuỗi JSON) | | `asa_conversion_type` | Loại chuyển đổi ASA | | `asa_country_or_region` | Quốc gia hoặc khu vực ASA | | `asa_creative_set_name` | Tên bộ creative ASA | | `fbclid` | Facebook Click ID | | `ttclid` | TikTok Click ID | | `utm_source` | Tham số UTM source | | `utm_medium` | Tham số UTM medium | | `utm_campaign` | Tham số UTM campaign | | `utm_term` | Tham số UTM term | | `utm_content` | Tham số UTM content | --- # File: ua-google-cloud-storage --- --- title: "Google Cloud Storage trong Adapty Attribution" description: "Tích hợp Google Cloud Storage với Adapty Attribution để lưu trữ dữ liệu thu thập người dùng an toàn." --- Tích hợp Adapty Attribution với Google Cloud Storage cho phép bạn lưu trữ dữ liệu chiến dịch thu hút người dùng một cách an toàn tại một nơi tập trung. Bạn có thể lưu dữ liệu hiệu suất chiến dịch, dữ liệu attribution, và các sự kiện thu hút người dùng vào bucket Google Cloud Storage của mình dưới dạng file .csv. Để thiết lập tích hợp này, bạn cần thực hiện một vài bước đơn giản trong Google Cloud Console và Adapty Attribution Dashboard. :::note Lịch chạy Adapty Attribution gửi dữ liệu của bạn đến Google Cloud Storage mỗi 24 giờ lúc 4:00 UTC. Mỗi file sẽ chứa dữ liệu về các sự kiện được tạo ra trong toàn bộ ngày theo lịch trước đó theo giờ UTC. Ví dụ: dữ liệu được xuất tự động lúc 4:00 UTC ngày 8 tháng 3 sẽ chứa tất cả các sự kiện được tạo vào ngày 7 tháng 3 từ 00:00:00 đến 23:59:59 theo giờ UTC. ::: ## Cách thiết lập tích hợp Google Cloud Storage \{#how-to-set-up-google-cloud-storage-integration\} ### Bước 1. Tạo thông tin xác thực Google Cloud Storage \{#step-1-create-google-cloud-storage-credentials\} Hướng dẫn này sẽ giúp bạn tạo các thông tin xác thực cần thiết trong Google Cloud Platform Console. Để Adapty Attribution có thể tải lên các báo cáo dữ liệu thô vào bucket của bạn, cần có khóa của service account cũng như quyền ghi vào bucket tương ứng. Bằng cách cung cấp khóa của service account và cấp quyền ghi vào bucket, bạn cho phép Adapty Attribution truyền dữ liệu thô một cách an toàn và hiệu quả từ nền tảng của mình sang môi trường lưu trữ của bạn. :::warning Xin lưu ý rằng chúng tôi chỉ hỗ trợ xác thực bằng Service Account HMAC key, do đó bạn cần đảm bảo rằng Service Account HMAC key của mình có các quyền "Storage Object Viewer", "Storage Legacy Bucket Writer" và "Storage Object Creator" để có thể truy cập đúng cách vào Google Cloud Storage. ::: #### 2.1. Tạo Service Account \{#21-create-service-account\} 1. Truy cập vào mục [IAM](https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts) trong tài khoản Google Cloud của bạn và chọn dự án phù hợp hoặc tạo dự án mới <img src="/assets/shared/img/30a81ef-CleanShot_2023-03-17_at_15.22.142x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Tiếp theo, tạo một service account mới cho Adapty Attribution bằng cách nhấn vào nút "+ CREATE SERVICE ACCOUNT" <img src="/assets/shared/img/98f8ebf-CleanShot_2023-03-17_at_15.40.062x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Điền thông tin vào các trường ở bước đầu tiên, vì quyền truy cập sẽ được cấp ở bước sau. Để đọc thêm chi tiết về trang này, hãy xem tài liệu [tại đây](https://docs.cloud.google.com/iam/docs/service-accounts-create) <img src="/assets/shared/img/2190c50-CleanShot_2023-03-17_at_15.48.552x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Để tạo và tải xuống [khóa JSON riêng tư](https://docs.cloud.google.com/iam/docs/keys-create-delete), hãy điều hướng đến mục KEYS và nhấp vào nút "ADD KEY" <img src="/assets/shared/img/8a45468-CleanShot_2023-03-17_at_15.58.092x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Trong phần DETAILS, tìm giá trị Email được liên kết với service account vừa tạo và sao chép lại. Thông tin này sẽ cần thiết cho các bước tiếp theo để cấp quyền cho tài khoản và cho phép nó ghi vào bucket <img src="/assets/shared/img/6ccd0f0-CleanShot_2023-03-17_at_16.03.162x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> #### 2.2. Cấu hình quyền Bucket \{#22-configure-bucket-permissions\} 6. Truy cập trang [Buckets](https://console.cloud.google.com/storage/browser) của Google Cloud Storage và chọn một bucket hiện có hoặc tạo mới để lưu trữ các báo cáo dữ liệu thu hút người dùng từ Adapty Attribution 7. Điều hướng đến phần PERMISSIONS và chọn tùy chọn [GRANT ACCESS](https://docs.cloud.google.com/identity/docs/how-to?hl=en) <img src="/assets/shared/img/3cdd937-CleanShot_2023-03-17_at_16.14.232x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 8. Trong phần PERMISSIONS, nhập Email của tài khoản dịch vụ đã lấy ở bước thứ năm đã đề cập trước đó, sau đó chọn vai trò Storage Object Creator 9. Cuối cùng, nhấn **SAVE** để áp dụng các thay đổi <img src="/assets/shared/img/62801f4-CleanShot_2023-03-17_at_16.17.312x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 10. Hãy nhớ lưu lại tên bucket để sử dụng sau này 11. Sau khi hoàn thành các bước này, bạn đã thiết lập xong các bước cần thiết trong Google Cloud Console! Bước cuối cùng là nhập tên bucket và tải xuống file JSON để sử dụng trong Adapty Attribution <img src="/assets/shared/img/c967e16-CleanShot_2023-03-17_at_16.23.332x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Bước 2. Cấu hình tích hợp trong Adapty Attribution \{#step-2-configure-integration-in-adapty-attribution\} 1. Truy cập [**Integrations** -> **Google Cloud Storage**](https://app.adapty.io/ua/integrations/google-cloud-storage) 2. Bật toggle **Export install events to Google Cloud Storage** 3. Điền vào các trường bắt buộc để thiết lập kết nối giữa Google Cloud Storage và Adapty Attribution: | Trường | Mô tả | |:------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Google Cloud service account key file** | File JSON key riêng tư đã tải xuống [JSON key file](ua-google-cloud-storage#step-1-create-google-cloud-storage-credentials). | | **Google Cloud bucket name** | Tên bucket trong Google Cloud Storage nơi bạn muốn lưu trữ dữ liệu. Tên này phải là duy nhất trong môi trường Google Cloud Storage và không được chứa khoảng trắng. | | **Folder inside the bucket** | Tên thư mục bên trong bucket nơi bạn muốn lưu trữ dữ liệu. Tên này phải là duy nhất trong bucket và có thể dùng để sắp xếp dữ liệu. Trường này không bắt buộc điền. | ## Xuất dữ liệu thủ công \{#manual-data-export\} Ngoài tính năng tự động xuất dữ liệu sự kiện sang Google Cloud Storage, Adapty UA còn cung cấp tính năng xuất file thủ công. Với tính năng này, bạn có thể chọn một ngày cụ thể cho dữ liệu thu hút người dùng và xuất thủ công sang bucket GCS của mình. Điều này giúp bạn kiểm soát tốt hơn dữ liệu cần xuất và thời điểm xuất. ## Cấu trúc bảng \{#table-structure\} Trong tích hợp Google Cloud Storage, Adapty Attribution cung cấp một bảng để lưu trữ dữ liệu lịch sử cho các sự kiện cài đặt. Bảng chứa thông tin về hồ sơ người dùng, doanh thu và lợi nhuận, cửa hàng gốc, cùng nhiều điểm dữ liệu khác. :::warning Lưu ý rằng cấu trúc này có thể mở rộng theo thời gian — khi chúng tôi hoặc các bên thứ ba mà chúng tôi hợp tác giới thiệu dữ liệu mới. Hãy đảm bảo rằng code xử lý dữ liệu của bạn đủ linh hoạt và chỉ phụ thuộc vào các trường cụ thể, không phụ thuộc vào toàn bộ cấu trúc. ::: Dưới đây là cấu trúc bảng cho các sự kiện: | Cột | Mô tả | |--------------------------|----------------------------------------------------| | `adapty_profile_id` | Mã định danh hồ sơ người dùng Adapty | | `install_id` | Mã định danh cài đặt duy nhất | | `created_at` | Thời điểm tạo bản ghi (ISO 8601) | | `installed_at` | Thời điểm cài đặt ứng dụng (ISO 8601) | | `store` | Cửa hàng ứng dụng (`ios`, `android`) | | `country` | Mã quốc gia của người dùng (ISO 3166-1 alpha-2) | | `ip_address` | Địa chỉ IP của client | | `idfa` | iOS Identifier for Advertisers | | `idfv` | iOS Identifier for Vendors | | `gaid` | Google Advertising ID (Android) | | `android_id` | ID thiết bị Android | | `app_set_id` | Android App Set ID | | `channel` | Kênh attribution | | `campaign_id` | Mã định danh chiến dịch | | `campaign_name` | Tên chiến dịch | | `adset_id` | Mã định danh nhóm quảng cáo | | `adset_name` | Tên nhóm quảng cáo | | `ad_id` | Mã định danh quảng cáo | | `ad_name` | Tên quảng cáo | | `keyword_id` | Mã định danh từ khóa | | `keyword_name` | Tên từ khóa | | `asa_org_id` | ID tổ chức Apple Search Ads | | `asa_keyword_match_type` | Kiểu khớp từ khóa ASA (`Exact`, `Broad`) | | `asa_attribution` | Dữ liệu attribution ASA (chuỗi JSON) | | `asa_conversion_type` | Loại chuyển đổi ASA | | `asa_country_or_region` | Quốc gia hoặc khu vực ASA | | `asa_creative_set_name` | Tên creative set ASA | | `fbclid` | Facebook Click ID | | `ttclid` | TikTok Click ID | | `utm_source` | Tham số UTM source | | `utm_medium` | Tham số UTM medium | | `utm_campaign` | Tham số UTM campaign | | `utm_term` | Tham số UTM term | | `utm_content` | Tham số UTM content | --- # File: adapty-mail --- --- title: "Adapty Mail" description: "Chiến dịch email do AI tạo ra, giúp chuyển đổi người dùng dùng thử thành người đăng ký trả phí." --- <CustomDocCardList ids={['mail-get-started', 'mail-brand', 'mail-collect-emails', 'mail-send-data-via-api', 'mail-sending-domain', 'mail-create-campaign', 'mail-analytics']} /> Adapty Mail chuyển đổi dữ liệu người dùng Adapty của bạn thành các chuỗi email do AI tạo ra, giúp chuyển đổi người dùng dùng thử thành người đăng ký trả phí. Nó sử dụng dữ liệu hồ sơ người dùng đã có trong dự án Adapty của bạn để xây dựng, gửi và attribution chiến dịch — không cần nền tảng email riêng. ## Tại sao nên dùng Adapty Mail? \{#why-adapty-mail\} Gửi chiến dịch email có mục tiêu đòi hỏi nội dung, thiết kế, hạ tầng gửi mail và attribution doanh thu. Mỗi yếu tố đó là một bài toán riêng cần giải. Adapty Mail xử lý tất cả. Hồ sơ thương hiệu của bạn được xây dựng từ URL cửa hàng và bất kỳ nguồn nào khác bạn thêm vào, và một chuỗi email hoàn chỉnh được tạo ra từ đó trong chưa đầy 2 phút — gửi từ tên miền của bạn với các liên kết thanh toán được cá nhân hóa và attribution mua hàng. ## Cách hoạt động \{#how-it-works\} 1. **Thu thập email**: Ứng dụng của bạn truyền email người dùng và giá trị `customer_user_id` đến Adapty qua SDK. Adapty Mail sử dụng dữ liệu này để xác định người nhận và attribution doanh thu về email cụ thể đã thúc đẩy mỗi lần mua hàng. Bạn cũng có thể gửi dữ liệu này từ máy chủ của mình bằng [Adapty Mail API](mail-send-data-via-api). 2. **Xây dựng web paywall**: Trang thanh toán mà mọi email liên kết đến. 3. **Tạo chuỗi email**: AI sử dụng hồ sơ thương hiệu của bạn để tạo ra 1–15 email — nội dung, thiết kế, ảnh hero và các liên kết thanh toán được cá nhân hóa phù hợp với danh mục và phong cách thương hiệu của ứng dụng. 4. **Khởi chạy flow**: Chọn một trigger (chưa từng mua, đã hủy gia hạn, vấn đề thanh toán, gói đăng ký hết hạn hoặc đã hoàn tiền) và một phân khúc, sau đó gắn chiến dịch của bạn. Email bắt đầu được gửi tự động, và doanh thu từ các lần mua hàng do email thúc đẩy được attribution về email cụ thể đã dẫn đến chuyển đổi. ## Yêu cầu \{#requirements\} Để sử dụng Adapty Mail, bạn cần: - Một tài khoản Adapty - Thiết lập thu thập email trong ứng dụng của bạn — xem [Thu thập email người dùng](mail-collect-emails) - `customer_user_id` được cấu hình trong Adapty SDK của bạn - Một tên miền bạn kiểm soát với quyền truy cập vào cài đặt DNS của nó - Một nhà cung cấp thanh toán web (Stripe, Paddle hoặc PayPal) ## Bắt đầu \{#get-started\} Làm theo hướng dẫn [Bắt đầu với Adapty Mail](mail-get-started) để hoàn tất thiết lập và khởi chạy chiến dịch đầu tiên của bạn. --- # File: mail-get-started --- --- title: "Bắt đầu với Adapty Mail" description: "Thiết lập Adapty Mail và khởi chạy email flow đầu tiên của bạn." --- Trong hướng dẫn này, bạn sẽ thiết lập Adapty Mail và khởi chạy email flow đầu tiên của mình. :::note Bạn cũng có thể gửi dữ liệu đến Adapty Mail từ server của mình mà không cần dùng Adapty SDK. Nếu bạn đã lưu email người dùng và thông tin mua hàng trên backend, hoặc đang import người dùng đăng ký từ nguồn khác, hãy xem [Gửi email và giao dịch qua Adapty Mail API](mail-send-data-via-api). ::: Quá trình thiết lập gồm sáu bước: 1. [Cấu hình Adapty SDK của bạn](#1-configure-your-adapty-sdk) 2. [Thiết lập domain gửi của bạn](#2-set-up-your-sending-domain) 3. [Tạo một web paywall](#3-create-a-web-paywall) 4. [Tạo chiến dịch với AI](#4-generate-a-campaign-with-ai) 5. [Khởi chạy một flow](#5-launch-a-flow) 6. [Bật tính năng gửi](#6-enable-sending) :::tip Nếu bạn đăng ký Adapty Mail thông qua Adapty, **hồ sơ thương hiệu** của bạn sẽ được tạo tự động từ URL cửa hàng của dự án. Mở **Brand** bất cứ lúc nào để xem lại hoặc chỉnh sửa — xem [Brand](mail-brand). Nếu bạn đăng ký độc lập, hãy thiết lập thương hiệu trên cùng trang đó trước khi tạo campaign hoặc web paywall. ::: ## Trước khi bắt đầu \{#before-you-start\} Đảm bảo các điều kiện sau đã được đáp ứng trước khi bắt đầu: - **Quyền truy cập DNS**: Bạn có thể thêm bản ghi vào domain gốc của mình. - **Nhà cung cấp thanh toán web**: Bạn có tài khoản Stripe, Paddle hoặc PayPal với các sản phẩm gói đăng ký đã được cấu hình. ## 1. Cấu hình Adapty SDK của bạn \{#1-configure-your-adapty-sdk\} :::important Adapty Mail là một **sản phẩm độc lập**. Bạn có thể sử dụng nó ngay cả khi paywall, gói đăng ký hay analytics của bạn không được xử lý bởi Adapty — không cần migrate toàn bộ hệ thống. Để có dữ liệu doanh thu chính xác, cấu hình tối thiểu là cài đặt Adapty SDK ở chế độ observer và bật thông báo server App Store. ::: Adapty Mail cần ba thứ từ ứng dụng của bạn: dữ liệu mua hàng (để có thể quy doanh thu về email đã thúc đẩy mỗi lượt chuyển đổi), một mã định danh người dùng ổn định, và email của người dùng. 1. **Để Adapty theo dõi doanh thu của bạn.** Bước đầu tiên phụ thuộc vào việc bạn đã triển khai in-app purchase chưa: - Nếu bạn **đã triển khai in-app purchase với Adapty**, bạn không cần làm gì thêm ở bước này. - Nếu bạn **đã triển khai in-app purchase mà không dùng Adapty** và không có kế hoạch migrate sang Adapty, hãy cài đặt SDK của Adapty cho nền tảng của bạn ở chế độ observer. Ở bước này, bạn chỉ cần thêm SDK vào dự án, kích hoạt nó với chế độ observer được bật, và báo cáo các giao dịch. Hướng dẫn theo nền tảng: [iOS](implement-observer-mode), [Android](implement-observer-mode-android), [React Native](implement-observer-mode-react-native), [Flutter](implement-observer-mode-flutter), [Unity](implement-observer-mode-unity), [Kotlin Multiplatform](implement-observer-mode-kmp), [Capacitor](implement-observer-mode-capacitor). - Nếu bạn **chưa triển khai in-app purchase và muốn sử dụng Adapty**, hãy hoàn thành các bước trong [hướng dẫn bắt đầu nhanh](quickstart) để giao việc xử lý mua hàng cho Adapty. Sau đó [bật thông báo máy chủ App Store trong Adapty](enable-app-store-server-notifications) để nhận các cập nhật liên quan đến doanh thu trực tiếp từ App Store. 2. **Thiết lập nhận dạng người dùng.** Truyền một ID ổn định — ID người dùng backend, Firebase UID, hoặc tương tự — bằng cách gọi `Adapty.identify()` hoặc truyền `customerUserId` vào `.activate()` khi khởi động SDK. `customer_user_id` là cách Adapty Mail liên kết các chiến dịch, lượt nhấp và giao dịch mua về đúng hồ sơ người dùng. Hướng dẫn theo nền tảng: [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), [Unity](unity-identifying-users), [Kotlin Multiplatform](kmp-identifying-users), [Capacitor](capacitor-identifying-users). 3. **Thu thập email người dùng.** Khi người dùng cung cấp email trong ứng dụng của bạn (ví dụ: khi đăng ký hoặc thanh toán), hãy truyền email đó vào Adapty bằng cách gọi `updateProfile` với thuộc tính email. Mỗi người nhận trong chiến dịch đều cần có giá trị này. Hướng dẫn theo nền tảng: [iOS](setting-user-attributes), [Android](android-setting-user-attributes), [React Native](react-native-setting-user-attributes), [Flutter](flutter-setting-user-attributes), [Unity](unity-setting-user-attributes), [Kotlin Multiplatform](kmp-setting-user-attributes), [Capacitor](capacitor-setting-user-attributes). Nếu ứng dụng của bạn chưa thu thập email, hãy xem [Chiến lược thu thập email](mail-collect-emails#email-collection-strategies). ## 2. Thiết lập tên miền gửi email \{#set-up-your-sending-domain\} Mở Adapty Mail: Nhấp vào logo Adapty ở phần header và chọn **Mail**. Adapty Mail gửi email từ tên miền của bạn. Bạn chỉ cần thêm bản ghi DNS một lần — tất cả các chiến dịch đều dùng chung tên miền đã xác minh đó. 1. Trong Adapty Mail, vào **Settings → Email Domains**. 2. Nhập tên miền gốc của bạn (ví dụ: `yourapp.com`) và nhấp **Preview**. Chỉ chấp nhận apex domain — các subdomain như `app.yourapp.com` sẽ bị từ chối ngay khi nhập. 3. Adapty tạo hai subdomain gửi mail (`mail.yourapp.com` và `email.yourapp.com`). Nhấn **Confirm** để xem các bản ghi DNS cần thiết. 4. Trong trình quản lý domain của bạn, thêm 10 bản ghi DNS được hiển thị (5 bản ghi mỗi subdomain): - 3 bản ghi CNAME (DKIM) mỗi subdomain - 1 bản ghi MX (Mail-From) mỗi subdomain - 1 bản ghi TXT (SPF, `v=spf1 include:amazonses.com ~all`) mỗi subdomain 5. Tùy chọn, thêm bản ghi DMARC TXT trên root domain của bạn (khuyến nghị). 6. Quay lại **Settings → Email Domains** và nhấn **Check Verification**. Tổng quan về thời gian xác minh: - **Tự động kiểm tra**: Lần kiểm tra đầu tiên diễn ra khoảng 5 phút sau khi bạn gửi. Các khoảng thời gian kiểm tra tăng dần lên mỗi giờ một lần cho đến khi tìm thấy bản ghi. - **Kiểm tra thủ công**: Nhấp vào **Check Verification** bất kỳ lúc nào để kích hoạt kiểm tra ngay lập tức. - **Lan truyền DNS**: Thường chỉ mất vài phút, nhưng trong một số trường hợp hiếm gặp có thể lên đến 48 giờ. - **Thời hạn xác minh**: 7 ngày. Nếu hết hạn, các bản ghi DNS của bạn vẫn được giữ nguyên — hãy nhập lại domain trong **Settings → Email Domains** để bắt đầu một cửa sổ mới. Để biết chi tiết về từng loại bản ghi và quá trình khởi động domain, xem [Thiết lập domain gửi mail của bạn](mail-sending-domain). ## 3. Tạo web paywall \{#3-create-a-web-paywall\} Mỗi email đều liên kết đến một web paywall — trang thanh toán mà người dùng truy cập khi nhấp vào CTA. Bạn có hai lựa chọn: - **Tạo bằng AI**: Để trình tạo web paywall tích hợp sẵn tạo một trang cho ứng dụng của bạn. - **Dùng paywall tự host**: Kết nối một paywall bạn đã tự host. Để bắt đầu, trong Adapty Mail vào **Web Paywalls → Create**. ### Tùy chọn A: Tạo bằng AI \{#option-a-generate-with-ai\} Trang hiển thị danh sách kiểm tra **Prerequisites** với các nút inline — hãy thực hiện theo thứ tự, sau đó quay lại và tạo. Danh sách kiểm tra bao gồm: đăng nhập vào paywall builder, kết nối Stripe, thêm sản phẩm, và xem lại kết quả. Xem [Thiết lập checkout](mail-checkout) để biết hướng dẫn đầy đủ. Khi tất cả điều kiện tiên quyết đã được đánh dấu xanh, nhấn **Generate** để mở hộp thoại tạo: - **Environment**: Chọn **Production** hoặc **Sandbox**. Sandbox sử dụng sản phẩm ở chế độ test của Stripe và là lựa chọn mặc định an toàn cho môi trường dev và local. - **Plans**: Chọn tối đa **3 Stripe plans** (mỗi plan là một sản phẩm + giá). Đây là các ưu đãi mà paywall được tạo ra sẽ hiển thị cho người dùng khi thanh toán. Nhấp **Generate** để bắt đầu quá trình tạo. Khi hoàn tất, mở editor để xem lại và xuất bản. :::important Paywall phải được xuất bản trước khi có thể phục vụ lưu lượng checkout. Các paywall chưa xuất bản sẽ trả về lỗi khi người dùng nhấp vào các liên kết email checkout. ::: ### Lựa chọn B: Dùng paywall tự host \{#option-b-use-your-own-hosted-paywall\} 1. Chọn **Enter URL manually**. 2. Dán URL của paywall bạn đã host. URL phải bao gồm các placeholder `{email}` và `{external_profile_id}` dưới dạng query parameter — Adapty Mail điền các giá trị này cho từng người nhận để trang thanh toán biết ai đang truy cập. Ví dụ: ``` https://example.com/paywall?email={email}&profile={external_profile_id} ``` 3. Lưu và publish. Để hiểu cấu trúc checkout funnel và cách cá nhân hóa hoạt động, xem [Thiết lập checkout](mail-checkout). ## 4. Tạo chiến dịch với AI \{#generate-a-campaign-with-ai\} AI sẽ tạo toàn bộ chuỗi email cho bạn — nội dung, thiết kế, ảnh hero và các link thanh toán được cá nhân hóa, tất cả đều phù hợp với thương hiệu của bạn. 1. Trong Adapty Mail, vào **Campaigns** và nhấp **Create**. 2. Đặt tên cho chiến dịch. 3. Trong dropdown **Web paywall**, chọn web paywall bạn đã thêm ở bước trước. 4. Nhấp vào **Generate emails**. 5. Điền vào hộp thoại tạo nội dung — giọng điệu, ngôn ngữ, câu lệnh tùy chỉnh (tùy chọn, tối đa 2.000 ký tự) và số lượng email (1–15, mặc định là 4). Xem [Tạo chiến dịch](mail-create-campaign) để biết ý nghĩa của từng trường. 6. Nhấp **Generate**. Quá trình tạo thường mất vài phút. Hệ thống sẽ hết thời gian chờ sau 5 phút nếu không hoàn thành được — hãy thử lại nếu điều đó xảy ra. 7. Xem trước từng email. Thanh tiêu đề xem trước có **Theme toggle** (Auto, Light, Dark) để kiểm soát cách hiển thị bản xem trước — nội dung được tạo ra giống nhau ở tất cả các chế độ. Bạn có thể tạo lại từng email, chỉnh sửa nội dung, hoặc mở trình chỉnh sửa HTML để kiểm soát chi tiết hơn. 8. Nhấp **Create** để lưu chiến dịch. Chiến dịch sẽ được lưu ở trạng thái **draft** và chưa được gửi đi — chiến dịch chỉ hoạt động khi được gắn vào một flow (bước tiếp theo). Không có thao tác "publish" riêng trong trình chỉnh sửa chiến dịch. ## 5. Khởi chạy một flow \{#5-launch-a-flow\} Một flow kết hợp một **trigger** (sự kiện như gói đăng ký hết hạn) với một **phân khúc**, và gửi cho phân khúc đó **campaign** mà bạn chọn. Adapty Mail đi kèm với năm trigger cố định, mỗi trigger có giao diện flow riêng. 1. Trong Adapty Mail, vào **Flows**, sau đó mở trigger bạn muốn cấu hình: - **Never purchased** — người dùng đã đăng ký nhưng chưa thực hiện mua hàng. - **Renewal cancelled** — người dùng đã tắt tự động gia hạn nhưng vẫn còn gói đăng ký đang hoạt động. - **Billing issue** — thanh toán thất bại, thẻ bị từ chối hoặc hết hạn, hoặc đang trong thời gian ân hạn. - **Expired** — gói đăng ký đã hết hạn và quyền truy cập bị mất. - **Refunded** — người dùng đã yêu cầu hoàn tiền sau khi mua. Xem [Flows](mail-flows) để biết mục tiêu và hướng dẫn về giọng văn phù hợp cho từng trigger. 2. Nhấn **Create** để mở hộp thoại. 3. Trong hộp thoại: - Chọn **Segment** (ví dụ: **All Users** để nhắm đến tất cả người dùng khi họ đạt trigger này, hoặc tạo phân khúc mới dựa trên các thuộc tính hồ sơ người dùng). - Giữ nguyên loại nội dung là **Campaign** (tùy chọn A/B Test được đề cập trong [A/B testing](mail-ab-testing)). - Chọn **Campaign** bạn đã lưu ở Bước 4. 4. Nhấn **Save**. Flow sẽ hoạt động ngay lập tức — không cần bước khởi chạy riêng. Từ thời điểm này, người dùng khớp với phân khúc sẽ bắt đầu nhận campaign ngay khi họ đạt đến trigger event. :::note Bạn có thể thêm nhiều hơn một hàng phân khúc → chiến dịch vào cùng một trigger; chúng chạy theo thứ tự ưu tiên. Hàng **All Users**, nếu được sử dụng, phải là hàng cuối cùng (ưu tiên thấp nhất) để bắt tất cả những người không khớp với phân khúc cụ thể hơn. ::: ## 6. Bật tính năng gửi mail \{#enable-sending\} Cho đến lúc này, chiến dịch của bạn đã được kết nối nhưng chưa thực sự hoạt động — **tích hợp Adapty** đồng bộ các sự kiện gói đăng ký vào Adapty Mail vẫn đang tắt. Bật tính năng này là bước cuối cùng: các sự kiện bắt đầu được truyền vào, các phân khúc bắt đầu khớp, và email bắt đầu được gửi đi. Bước này chỉ mở khóa sau khi hoàn thành Bước 5. Trước khi bạn khởi chạy một flow, nút **Enable** trong **Settings → Integrations** sẽ bị vô hiệu hóa kèm theo gợi ý *"Set up at least one flow before enabling Adapty integration."* 1. Trong Adapty Mail, vào **Settings → Integrations**. 2. Nhấp vào **Enable Adapty integration** (hoặc **Enable** nếu integration đã tồn tại từ lần thiết lập trước). Khi được bật, Adapty sẽ gửi mọi sự kiện gói đăng ký — gói đăng ký mới, gia hạn, dùng thử, chuyển đổi, hoàn tiền, vấn đề thanh toán — vào Adapty Mail. Các sự kiện này điều khiển thành viên phân khúc, định tuyến chiến dịch, và điều kiện dừng để tạm dừng một chuỗi khi người dùng chuyển đổi. :::note Toggle **Adapty integration** trong Settings *không phải* là workspace đối tác Adapty mà bạn dùng để đăng nhập vào Adapty Mail. Workspace đối tác là thứ đã tạo tài khoản và (nếu bạn đăng ký qua Adapty) thương hiệu của bạn. Toggle integration ở đây kiểm soát việc đồng bộ sự kiện — bạn cần bật nó cho từng dự án. ::: ## Khắc phục sự cố \{#troubleshooting\} | Vấn đề | Giải pháp | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Xác minh DNS bị treo | Kiểm tra xem các bản ghi có khớp chính xác không — không có dấu chấm ở cuối, CNAME target đúng. Chờ 5–10 phút, rồi nhấn **Check Verification** lại | | Cửa sổ xác minh hết hạn | Các bản ghi của bạn vẫn còn nguyên. Nhập lại tên miền trong **Settings → Email Domains** để bắt đầu cửa sổ mới | | Tạo thất bại hoặc hết thời gian chờ | Kiểm tra kết nối internet và thử lại. Nếu vấn đề vẫn còn, hãy liên hệ bộ phận hỗ trợ Adapty | ## Tìm hiểu thêm \{#learn-more\} - **[Thu thập email người dùng](mail-collect-emails)**: Chiến lược lấy email nếu ứng dụng của bạn chưa thu thập chúng. - **[Thiết lập domain gửi email](mail-sending-domain)**: Chi tiết bản ghi DNS, các cấp độ khởi động, và xử lý sự cố. - **[Thiết lập checkout](mail-checkout)**: Cấu trúc checkout funnel và cá nhân hóa. - **[Phân tích campaign](mail-analytics)**: Theo dõi khả năng gửi, mức độ tương tác, và doanh thu. - **[A/B testing](mail-ab-testing)**: Kiểm thử nhiều phiên bản chuỗi. --- # File: mail-collect-emails --- --- title: "Thu thập email người dùng cho Adapty Mail" description: "Truyền email và mã định danh ổn định của người dùng vào Adapty để các chiến dịch có thể tiếp cận người dùng của bạn." --- Adapty Mail cần một `customer_user_id` ổn định và email cho mỗi người dùng mà nó gửi tới. Hãy kết nối cả hai trong code ứng dụng của bạn trước khi chạy chiến dịch. ## Thu thập email người dùng \{#collect-user-emails\} Mỗi người dùng cần cung cấp cho Adapty hai giá trị: một `customer_user_id` ổn định để định danh người dùng, và email. Việc định danh phải được thực hiện trước — nếu không có nó, Adapty không có hồ sơ người dùng để gắn email vào. 1. **Định danh người dùng.** Truyền một ID ổn định — ID người dùng từ backend, Firebase UID, hoặc tương tự — bằng cách truyền nó dưới dạng `customerUserId` vào `.activate()` khi khởi động SDK, hoặc gọi `Adapty.identify()` sau đó (ví dụ, khi đăng nhập). Dù theo cách nào, ID phải được thiết lập trước khi hiển thị bất kỳ paywall nào. Hướng dẫn theo nền tảng: [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), [Unity](unity-identifying-users), [Kotlin Multiplatform](kmp-identifying-users), [Capacitor](capacitor-identifying-users). 2. **Truyền email.** Ngay khi người dùng cung cấp email của họ, hãy gửi nó đến Adapty thông qua `updateProfile` bằng tham số `email`. Hướng dẫn theo nền tảng: [iOS](setting-user-attributes), [Android](android-setting-user-attributes), [React Native](react-native-setting-user-attributes), [Flutter](flutter-setting-user-attributes), [Unity](unity-setting-user-attributes), [Kotlin Multiplatform](kmp-setting-user-attributes), [Capacitor](capacitor-setting-user-attributes). :::important - Luôn truyền một `customer_user_id` **ổn định**, không bao giờ dùng mã định danh ẩn danh. Nếu người dùng gỡ cài đặt và cài lại ứng dụng, Adapty dùng ID này để liên kết lần cài đặt lại với hồ sơ người dùng hiện có và gán các giao dịch mua về đúng người dùng. - Hãy xin sự đồng ý rõ ràng từ người dùng trước khi thu thập và gửi email đến Adapty. Bạn có trách nhiệm tuân thủ GDPR, CAN-SPAM và các quy định tương tự ở thị trường mục tiêu của mình. ::: <Details> <summary>Kiểm tra mức độ bao phủ email của bạn</summary> Sau khi triển khai thu thập, hãy kiểm tra mức độ bao phủ trong Adapty: 1. Vào **Customers → Profiles**. 2. Lọc theo các hồ sơ người dùng đã có email. Hãy đạt ít nhất 30–50% bao phủ email trong số người dùng đang hoạt động trước khi chạy chiến dịch đầu tiên. Bạn không cần đợi đến 100% — hãy ra mắt ngay khi đạt 30%. Những người dùng cung cấp email sau đó sẽ tự động được đưa vào các chiến dịch đang chạy khi họ đủ điều kiện. </Details> ## Các chiến lược thu thập email \{#email-collection-strategies\} Hầu hết các ứng dụng không thu thập email theo mặc định. Hãy chọn cách tiếp cận phù hợp với trạng thái hiện tại của ứng dụng bạn. | Chiến lược | Phù hợp nhất với | Cách hoạt động | | ---------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | **Xác thực hiện có** | Ứng dụng có bất kỳ hình thức đăng nhập nào | Bạn đã có email — truyền nó cho Adapty sau khi người dùng xác thực. Xem tài liệu tham khảo về phương thức xác thực bên dưới để biết cách đọc nó. | | **Cổng email trước paywall** | Ứng dụng không có xác thực — sức khỏe, wellness, chiêm tinh, chỉnh sửa ảnh | Thêm một màn hình nhập email giữa onboarding và paywall. Tỷ lệ chuyển đổi thường đạt 70–90% vì người dùng đã đầu tư thời gian. | | **Web paywall builder checkout** | Ít tích hợp SDK; email được thu thập trên web | Màn hình đầu tiên của web paywall builder thu thập email và truyền vào Adapty — hữu ích cho người dùng click vào chiến dịch trước khi cổng trong ứng dụng được triển khai. | | **Bước trong onboarding** | Onboarding dạng quiz (thể dục, dinh dưỡng, giáo dục) | Đặt ô nhập email ở bước 2–3 trong onboarding. Đóng khung như cung cấp giá trị ("Chúng tôi sẽ gửi email kế hoạch cá nhân hóa của bạn") và tránh để bước này có thể bỏ qua. | | **Adapty Mail API** | Gửi email từ server của bạn, không cần Adapty SDK | Gửi hồ sơ người dùng đến endpoint [Save profile](api-mail/operations/saveProfile) của Adapty Mail API. Xem [Gửi email và giao dịch qua Adapty Mail API](mail-send-data-via-api). | ## Giới hạn \{#limitations\} - **Người dùng ẩn danh**: Người dùng không có `customer_user_id` ổn định không thể nhận được chiến dịch. Hãy định danh họ khi họ tạo tài khoản hoặc đăng nhập — từ đó, mọi email họ cung cấp sẽ được khớp với hồ sơ người dùng Adapty của họ. - **Người dùng không có email**: Hồ sơ người dùng không có email sẽ bị loại khỏi việc gửi chiến dịch và không xuất hiện trong phân tích chiến dịch. Ngay khi họ cung cấp email, họ sẽ đủ điều kiện cho các chiến dịch trong tương lai. --- # File: mail-send-data-via-api --- --- title: "Gửi email và giao dịch qua Adapty Mail API" description: "Gửi hồ sơ người dùng và giao dịch đến Adapty Mail trực tiếp từ máy chủ của bạn, không cần Adapty SDK." --- Adapty Mail API cho phép bạn gửi hồ sơ người dùng và giao dịch đến Adapty Mail trực tiếp từ máy chủ, không cần chuyển dữ liệu qua Adapty SDK. Sử dụng API này khi bạn muốn: - Thêm người đăng ký khi chưa có danh sách nào trong Adapty Mail. - Tái sử dụng danh sách người đăng ký từ các ứng dụng khác của bạn. - Cung cấp dữ liệu cho Adapty Mail theo kiểu server-to-server, với backend của bạn là nguồn dữ liệu chính. :::note **API hay SDK?** Hầu hết các ứng dụng gửi dữ liệu đến Adapty Mail thông qua Adapty SDK, vì SDK tự động thu thập email và thông tin mua hàng. Chọn API khi ứng dụng của bạn không tích hợp Adapty SDK, khi dữ liệu đã có sẵn trên máy chủ của bạn, hoặc khi bạn import người đăng ký từ nguồn khác. ::: ## Trước khi bắt đầu \{#before-you-start\} :::warning Hoàn tất thiết lập Adapty Mail trước khi gửi dữ liệu — bao gồm tạo chiến dịch, phân khúc (nếu cần), web paywall và khởi chạy flow. Adapty Mail chỉ gửi email đến các hồ sơ người dùng được tạo sau khi hoàn tất thiết lập; các hồ sơ gửi trước đó sẽ không nhận được email nào. Hãy làm theo hướng dẫn [Bắt đầu với Adapty Mail](mail-get-started) trước, rồi quay lại đây. ::: Bạn cũng cần API key và base URL: - **Secret API key**: Trong Adapty Mail, vào **Settings** và sao chép secret API key của bạn. Key này gắn với từng project, giúp API biết dữ liệu thuộc về project nào. - **Base URL**: Tất cả các request đều gửi đến `https://api-mail.adapty.io`. - **Xác thực**: Gửi key trong header **Authorization** theo dạng `Bearer {your_secret_api_key}`. :::important Hãy xin phép người dùng một cách rõ ràng trước khi thu thập email và gửi đến Adapty Mail. Bạn có trách nhiệm tuân thủ GDPR, CAN-SPAM và các quy định tương tự tại thị trường của mình. ::: ## Gửi hồ sơ người dùng \{#send-user-profiles\} Một hồ sơ chứa email và các thuộc tính của người dùng. Để tạo hoặc cập nhật hồ sơ, gửi request POST đến `/api/v1/profile/save/`. Ba trường bắt buộc: - `external_profile_id` ổn định do ứng dụng hoặc backend của bạn quản lý - `email` mà Adapty Mail dùng để gửi chiến dịch - `external_created_at` — thời điểm tạo người dùng, có thể dùng trong phân khúc :::important Luôn gửi `external_profile_id` ổn định, không dùng giá trị ẩn danh hoặc thay đổi theo từng lần cài đặt. Adapty Mail dùng giá trị này để liên kết email, lượt nhấp và giao dịch mua vào một hồ sơ duy nhất. ::: ```bash curl --request POST \ --url 'https://api-mail.adapty.io/api/v1/profile/save/' \ --header 'Authorization: Bearer {your_secret_api_key}' \ --header 'Content-Type: application/json' \ --data '{ "external_profile_id": "user_12345", "external_created_at": "2026-06-01T10:30:00Z", "email": "jane@example.com", "country": "US", "custom_attributes": { "plan": "trial" } }' ``` Xem tài liệu tham khảo [Save profile](api-mail/operations/saveProfile) để biết tất cả các trường có thể dùng. ## Gửi sự kiện giao dịch \{#send-transaction-events\} :::note Một hồ sơ có email là đủ để tiếp cận người dùng trong flow **chưa từng mua hàng**. Người dùng trong mọi flow khác cũng cần có sự kiện giao dịch. ::: Mọi flow ngoại trừ flow **chưa từng mua hàng** đều dựa vào lịch sử mua hàng. Gửi sự kiện giao dịch của hồ sơ khi xử lý các giao dịch mua, gia hạn và hủy, để Adapty Mail có thể đưa hồ sơ vào đúng flow. Sự kiện giao dịch cũng hỗ trợ attribution doanh thu. Bỏ qua bước này chỉ khi bạn chỉ chạy chiến dịch **chưa từng mua hàng** mà thôi. Để ghi lại một giao dịch, gửi request POST đến `/api/v1/profile/transaction-event/save/`. Dùng cùng `external_profile_id` đã gửi với hồ sơ để Adapty Mail liên kết giao dịch với đúng người dùng. ```bash curl --request POST \ --url 'https://api-mail.adapty.io/api/v1/profile/transaction-event/save/' \ --header 'Authorization: Bearer {your_secret_api_key}' \ --header 'Content-Type: application/json' \ --data '{ "event_type": "subscription_started", "event_id": "evt_abc123", "event_datetime": "2026-06-10T14:20:05Z", "external_profile_id": "user_12345", "store": "app_store", "store_product_id": "premium_monthly", "store_transaction_id": "1000000123456789", "store_original_transaction_id": "1000000123456789", "purchased_at": "2026-06-10T14:20:00Z", "originally_purchased_at": "2026-06-10T14:20:00Z", "price_usd": "9.99" }' ``` Xem tài liệu tham khảo [Save transaction event](api-mail/operations/saveTransactionEvent) để biết tất cả các trường có thể dùng. ### Ánh xạ sự kiện vào flow \{#map-your-events-to-flows\} Gửi `event_type` tương ứng với những gì đã xảy ra. Adapty Mail suy ra trạng thái của hồ sơ từ lịch sử sự kiện và chuyển hồ sơ vào flow phù hợp. | `event_type` | Gửi khi | Flow | | --- | --- | --- | | `subscription_started` | Người dùng bắt đầu gói đăng ký mới. | Đang hoạt động — không có flow tái tương tác | | `subscription_renewed` | Gói đăng ký tự động gia hạn. | Đang hoạt động — không có flow tái tương tác | | `subscription_renewal_reactivated` | Người dùng bật lại tự động gia hạn. | Đang hoạt động — không có flow tái tương tác | | `non_subscription_purchase` | Người dùng thực hiện sản phẩm mua một lần. | Đang hoạt động — không có flow tái tương tác | | `subscription_renewal_cancelled` | Người dùng tắt tự động gia hạn (vẫn còn hiệu lực đến khi hết hạn). | Đã hủy gia hạn | | `billing_issue_detected` | Thanh toán gia hạn thất bại. | Vấn đề thanh toán | | `entered_grace_period` | Thanh toán thất bại nhưng người dùng vẫn đang trong thời gian ân hạn. | Vấn đề thanh toán | | `subscription_expired` | Gói đăng ký hết hạn và quyền truy cập bị ngừng. | Đã hết hạn | | `subscription_refunded` | Giao dịch mua gói đăng ký được hoàn tiền. | Đã hoàn tiền | | `non_subscription_purchase_refunded` | Sản phẩm mua một lần được hoàn tiền. | Đã hoàn tiền | --- # File: mail-brand --- --- title: "Thương hiệu trong Adapty Mail" description: "Xem xét và tinh chỉnh hồ sơ thương hiệu phục vụ việc tạo email và web paywall." --- **Thương hiệu** là hồ sơ tổng hợp mà Adapty Mail xây dựng từ các nguồn công khai của ứng dụng — trang App Store hoặc Google Play, trang landing page, trang điều khoản và quyền riêng tư, cùng các hồ sơ mạng xã hội. Nó chi phối nội dung email, giọng điệu, hình ảnh, nội dung web paywall và bản demo. Mỗi dự án chỉ có một thương hiệu; mọi tính năng đều đọc dữ liệu từ cùng một hồ sơ này. Mở thương hiệu từ mục **Brand** trong thanh bên của Adapty Mail. - **Nếu bạn đăng ký Adapty Mail qua Adapty**: Thương hiệu của bạn đã được tạo tự động từ URL cửa hàng của dự án Adapty. Trang Brand mở ra với toàn bộ hồ sơ, sẵn sàng để bạn xem xét và tinh chỉnh. - **Nếu bạn đăng ký độc lập**: Trang Brand mở ra màn hình thiết lập — xem [Thiết lập từ đầu](#set-up-from-scratch). ## Hồ sơ thương hiệu gồm những gì \{#whats-in-a-brand-profile\} Một thương hiệu có 13 phần. Adapty Mail sử dụng trực tiếp các phần này khi tạo email và web paywall. - **Identity**: Tên ứng dụng, mô tả ngắn, tagline. - **Visual identity**: Màu sắc (primary, background, secondary, accent, text, CTA), kiểu chữ, ghi chú phong cách, URL logo. - **Audience**: Nhân khẩu học, ngôn ngữ, thị trường. - **Features**: Mỗi tính năng có tên, lợi ích mang lại và mô tả tùy chọn. - **Insights**: Đề xuất giá trị độc đáo, nhận xét, các phản đối phổ biến kèm phản hồi, và tags. - **Brand voice**: Giọng điệu, mức độ trang trọng, từ vựng, cảm xúc. - **Voice samples**: Tiêu đề mẫu, CTA mẫu, và các preset giọng điệu dùng khi tạo email. - **Social proof**: Số lượng người dùng, đánh giá, số lượt đánh giá, đề cập báo chí, các chỉ số quan trọng. - **Social links**: Twitter, Instagram, TikTok, YouTube, Facebook, LinkedIn. - **Legal links**: URL điều khoản, URL quyền riêng tư, email hỗ trợ. - **Reviews**: Đánh giá người dùng được trích xuất từ nguồn cửa hàng — nội dung, tác giả, điểm đánh giá, nguồn. - **Pain points**: Các điểm đau được rút ra từ đánh giá hoặc tín hiệu mạng xã hội. - **FAQs**: Câu hỏi và câu trả lời dùng trong nội dung email và các phần web paywall. ## Chỉnh sửa một phần thủ công \{#edit-a-section-manually\} Mỗi phần trong giao diện thương hiệu đều có nút **Edit**. Nhấn vào đó sẽ mở trình chỉnh sửa ngay trong trang cho phần đó. 1. Nhấn **Edit** trên phần bạn muốn thay đổi. 2. Cập nhật các trường tại chỗ. Adapty Mail lưu các thay đổi chưa lưu dưới dạng bản nháp. 3. Nhấn **Save** trong banner bản nháp ở đầu trang để áp dụng. Nhấn **Discard** để hủy các thay đổi. Chỉ có thể mở một phần tại một thời điểm. Nếu bạn cố mở phần thứ hai, phần đầu tiên sẽ nhắc bạn hoàn thành hoặc hủy bỏ. :::important Các chỉnh sửa bị tạm dừng trong khi một nguồn đang xử lý — nguồn đang hoàn tất có thể ghi đè lên các thay đổi bạn đang thực hiện. Mọi trình chỉnh sửa đang mở sẽ tự động đóng khi quá trình xử lý bắt đầu, và nút **Save** trong banner bản nháp sẽ bị vô hiệu hóa cho đến khi quá trình xử lý hoàn tất. ::: ## Tinh chỉnh với AI \{#refine-with-ai\} Nút **Refine with AI** ở góc dưới bên phải mở bảng chat bên cạnh thương hiệu. Dùng nó để mô tả thay đổi bằng ngôn ngữ tự nhiên; AI sẽ đề xuất một bản nháp tinh chỉnh mà bạn có thể lưu hoặc từ chối. 1. Nhấn **Refine with AI**. 2. Mô tả thay đổi bạn muốn. Phạm vi chat chỉ giới hạn ở việc chỉnh sửa thương hiệu — nó sẽ không trả lời các câu hỏi chung hoặc viết lại nội dung ngoài hồ sơ thương hiệu. 3. Xem lại bản nháp được đề xuất trong giao diện chính. Adapty Mail sẽ đánh dấu các phần đã thay đổi. 4. Nhấn **Save** trong banner bản nháp để áp dụng, hoặc **Discard** để giữ nguyên thương hiệu đã lưu. Các gợi ý hữu ích: - "Make the tone more playful." - "Add the offline mode feature." - "Strengthen the unique value proposition." - "Rewrite the audience description for a US market." ## Thêm nguồn dữ liệu \{#add-more-sources\} Nguồn từ cửa hàng là nền tảng của hồ sơ. Các loại nguồn bổ sung giúp tinh chỉnh các phần cụ thể — landing page cải thiện nhận diện thương hiệu và nội dung, trang điều khoản và quyền riêng tư cải thiện các liên kết pháp lý, hồ sơ mạng xã hội cải thiện mẫu giọng nói và bằng chứng xã hội. Trong giao diện thương hiệu, bảng **Sources** liệt kê mọi nguồn và có form **Add** bên dưới. Chọn loại, dán URL vào và nhấn **Add**. - **App Store**: `https://apps.apple.com/...` - **Google Play**: `https://play.google.com/store/apps/details?id=...` - **Landing page**: Trang marketing của bạn, ví dụ `https://yourapp.com`. - **Terms / Privacy**: Liên kết trực tiếp đến trang điều khoản hoặc quyền riêng tư của bạn. - **Social profile**: URL Twitter, Instagram, TikTok, YouTube, Facebook hoặc LinkedIn. Mỗi loại chỉ có một nguồn. Bộ chọn sẽ vô hiệu hóa một loại khi nguồn của loại đó đang được xử lý hoặc đã hoàn tất. Để thay thế một nguồn, hãy xóa thương hiệu và thiết lập lại từ đầu — không thể xóa từng nguồn riêng lẻ. ## Thiết lập từ đầu \{#set-up-from-scratch\} Nếu dự án của bạn chưa có thương hiệu (thường gặp với người dùng đăng ký Adapty Mail độc lập), trang **Brand** sẽ mở ra màn hình thiết lập. Nguồn từ cửa hàng — App Store hoặc Google Play — là nền tảng; các loại nguồn khác có thể được thêm sau. 1. Chọn cửa hàng (**App Store** hoặc **Google Play**) và dán URL trang listing. 2. Nhấn **Build my brand**. Adapty Mail sẽ tải trang, phân tích đánh giá và suy luận giọng điệu thương hiệu của bạn. Quá trình xử lý thường mất dưới một phút. 3. Khi quá trình xử lý hoàn tất, giao diện thương hiệu sẽ mở ra với tất cả 13 phần được điền đầy đủ. Nếu một nguồn thất bại (URL không hợp lệ, trang không truy cập được, lỗi parser), màn hình sẽ hiển thị thông báo lỗi và nút **Try again**. Sửa URL và gửi lại. ## Thương hiệu được dùng ở đâu \{#where-the-brand-is-used\} Thương hiệu được sử dụng bởi mọi tính năng liên quan cần biết ứng dụng của bạn trông và nghe như thế nào: - **Tạo email**: Nội dung, giọng điệu, hình ảnh, khối người gửi và hình ảnh hero đều đọc từ thương hiệu. Xem [Tạo chiến dịch](mail-create-campaign). - **Web paywall builder**: Thương hiệu là điều kiện tiên quyết để tạo paywall — nếu không có `brand_saved`, quá trình tạo sẽ bị chặn. Xem [Thiết lập checkout](mail-checkout). - **Onboarding**: Bước **Set up brand** trong danh sách kiểm tra onboarding sẽ chuyển sang hoàn thành khi có thương hiệu. ## Xóa thương hiệu \{#delete-a-brand\} Thao tác **Delete brand** nằm trong phần **Danger zone** ở cuối giao diện thương hiệu. 1. Nhấn **Delete** trong phần Danger zone. 2. Xác nhận trong hộp thoại. Việc xóa sẽ xóa hồ sơ thương hiệu và mọi nguồn. Không thể hoàn tác — để khôi phục, hãy dán URL App Store hoặc Google Play trên màn hình bắt đầu và thiết lập lại từ đầu. :::warning Các chiến dịch hiện có vẫn giữ bản snapshot thương hiệu được dùng khi tạo, nhưng các chiến dịch mới và việc tạo paywall sẽ bị chặn cho đến khi bạn thiết lập lại thương hiệu. ::: ## Giới hạn \{#limitations\} - **Một thương hiệu mỗi dự án**: Mỗi dự án Adapty Mail chỉ có một thương hiệu duy nhất. Để nhắm đến một ứng dụng khác, hãy tạo dự án mới. - **Một nguồn mỗi loại**: Một thương hiệu chỉ có tối đa một nguồn App Store, một Google Play, một landing page, một terms/privacy và một hồ sơ mạng xã hội. - **Không thể xóa từng nguồn**: Không thể xóa riêng lẻ từng nguồn trong giao diện. Dùng **Delete brand** nếu cần thay thế một nguồn. - **Chỉnh sửa bị tạm dừng khi đang xử lý**: Chỉnh sửa phần, lưu từ refine-chat và xóa thương hiệu đều bị chặn khi một nguồn đang ở trạng thái `pending` hoặc `processing`. - **Các nguồn thất bại vẫn được liệt kê**: Nguồn ở trạng thái `failed` vẫn hiển thị trong bảng kèm thông báo lỗi. Gửi lại cùng loại đó để thử lại — mục thất bại sẽ được thay thế khi lịch mới thành công. --- # File: mail-sending-domain --- --- title: "Cài đặt domain gửi email cho Adapty Mail" description: "Thêm bản ghi DNS, xác minh domain của bạn và tìm hiểu về quá trình warm-up để Adapty Mail có thể gửi email thay mặt bạn." --- Adapty Mail gửi chiến dịch từ domain của chính bạn — không phải từ địa chỉ dùng chung — để uy tín người gửi luôn nằm trong tầm kiểm soát của bạn. Bạn chỉ cần thiết lập một lần, và mọi chiến dịch đều sử dụng cùng một domain đã xác minh. Để biết các bước tối thiểu, xem phần domain trong [Bắt đầu với Adapty Mail](mail-get-started#2-set-up-your-sending-domain). Bài viết này trình bày đầy đủ quy trình thiết lập, cách xác minh hoạt động và hành vi warm-up tự động. ## Yêu cầu \{#requirements\} - **Apex domain**: Nhập domain gốc của bạn (ví dụ: `yourapp.com`), không phải subdomain. Các giá trị như `app.yourapp.com` sẽ bị từ chối khi kiểm tra. - **Bản ghi NS đang hoạt động**: Domain phải phân giải được. Adapty Mail thực hiện tra cứu DNS trong quá trình thiết lập và từ chối các domain không có bản ghi NS hợp lệ. - **Một domain cho mỗi dự án Adapty**: Domain không thể dùng chung giữa các dự án. Nếu domain đã được đăng ký cho bất kỳ dự án nào — của bạn hay của người khác — quá trình thiết lập sẽ thất bại. ## Cài đặt domain gửi email \{#set-up-your-sending-domain\} Trình hướng dẫn thiết lập gồm ba màn hình: nhập domain, xác nhận các subdomain được tạo, và thêm bản ghi DNS. Tất cả đều nằm trong **Settings → Email Domains**. 1. **Nhập domain của bạn.** Gõ apex domain vào trường **Domain** và nhấn **Preview**. Adapty Mail kiểm tra định dạng (ASCII, hai nhãn, không có dấu gạch ngang ở đầu hoặc cuối, TLD từ 2 ký tự trở lên) và kiểm tra xem DNS có phân giải được không. 2. **Xác nhận subdomain.** Adapty Mail tạo hai subdomain gửi với tiền tố cố định — `mail.yourapp.com` và `email.yourapp.com` — mỗi cái có danh tính SES riêng. Nó cũng tạo một subdomain Mail-From cho mỗi cái (`hello.mail.yourapp.com` và `hello.email.yourapp.com`). Xem lại rồi nhấn **Confirm**. 3. **Thêm bản ghi DNS.** Màn hình cuối liệt kê tất cả các bản ghi cần thêm — tổng cộng 10, mỗi subdomain gửi 5 bản ghi, cộng thêm một bản ghi DMARC tùy chọn trên root. Dùng **Download CSV** để xuất danh sách đầy đủ, hoặc sao chép từng bản ghi vào nhà đăng ký domain của bạn. Nhấn **Done** khi đã đặt bản ghi xong. <Details> <summary>Tài liệu tham chiếu bản ghi DNS</summary> Với mỗi subdomain gửi (`mail.yourapp.com` và `email.yourapp.com`), hãy thêm: **DKIM — 3 bản ghi CNAME.** Chữ ký mật mã chứng minh email không bị thay đổi trong quá trình truyền. | Trường | Định dạng | | ------ | ----------------------------------- | | Type | CNAME | | Name | `{token}._domainkey.{subdomain}` | | Value | `{token}.dkim.amazonses.com` | **Mail-From — 1 bản ghi MX.** Xử lý bounce. | Trường | Định dạng | | -------- | --------------------------------------------------------------- | | Type | MX | | Name | `hello.{subdomain}` (ví dụ: `hello.mail.yourapp.com`) | | Priority | `10` | | Value | `feedback-smtp.{region}.amazonses.com` | **SPF — 1 bản ghi TXT.** Cho phép Adapty gửi email thay mặt bạn. | Trường | Định dạng | | ------ | -------------------------------------- | | Type | TXT | | Name | `hello.{subdomain}` | | Value | `"v=spf1 include:amazonses.com ~all"` | Trên domain root, thêm bản ghi DMARC tùy chọn: | Trường | Định dạng | | ------ | --------------------- | | Type | TXT | | Name | `_dmarc.{domain}` | | Value | `v=DMARC1; p=reject` | Token, region và các giá trị khác được lấy từ AWS SES lúc thiết lập. Luôn sao chép chúng từ màn hình bản ghi DNS trong Adapty Mail, không phải từ tài liệu tham chiếu này. </Details> ## Cách xác minh hoạt động \{#how-verification-works\} Khi bản ghi DNS đã được thêm, Adapty Mail tự động kiểm tra DNS định kỳ, và bạn cũng có thể kích hoạt kiểm tra thủ công. - **Kiểm tra tự động**: Bắt đầu sau 5 phút kể từ khi bạn gửi, sau đó tăng gấp đôi mỗi vòng — 10 phút, 20 phút, 40 phút — trước khi dừng ở mức 60 phút. Quá trình tiếp tục cho đến khi tìm thấy bản ghi hoặc hết 7 ngày. - **Kiểm tra thủ công**: Nhấn **Check Verification** để kích hoạt kiểm tra ngay lập tức. Có thời gian chờ 60 giây giữa các lần kiểm tra thủ công — kích hoạt quá nhanh sẽ trả về *"Verification check is on cooldown."* - **Trạng thái**: DKIM và Mail-From của mỗi subdomain được theo dõi độc lập là **Pending**, **Success**, hoặc **Failed**. Domain chỉ được coi là đã xác minh đầy đủ khi tất cả bốn trạng thái đều là **Success**. - **Thời hạn 7 ngày**: Nếu xác minh không hoàn thành trong 7 ngày, danh tính sẽ được đánh dấu là **Failed**. Bản ghi DNS của bạn vẫn còn trong nhà đăng ký — nhập lại domain trong **Settings → Email Domains** để bắt đầu một khoảng thời gian mới. - **Sau khi xác minh**: Nếu bạn xóa hoặc thay đổi bản ghi DNS sau này, AWS SES sẽ dần hạ cấp danh tính. Hãy giữ nguyên các bản ghi miễn là bạn còn muốn gửi email. - **Lan truyền DNS**: Thường mất vài phút; trong một số trường hợp hiếm có thể mất đến 48 giờ. ## Warm-up domain \{#domain-warm-up\} Domain mới chưa có uy tín với các nhà cung cấp email như Gmail hay Yahoo, vì vậy gửi số lượng lớn từ domain mới có nguy cơ bị vào thư mục spam. Adapty Mail xử lý warm-up tự động bằng cách tăng dần giới hạn gửi hàng ngày qua 14 cấp độ. Không cần cấu hình gì thêm. ### Cách các cấp độ hoạt động \{#how-tiers-work\} Domain của bạn bắt đầu ở **Cấp 1** (200 lượt gửi/ngày) và tự động tiến lên khi các chỉ số deliverability vẫn ở mức tốt. Nếu tỷ lệ bounce tăng hoặc tỷ lệ khiếu nại tăng, việc tiến cấp sẽ tạm dừng và có thể bị lùi lại cho đến khi uy tín phục hồi. | Cấp độ | Giới hạn hàng ngày | | ------ | ------------------ | | 1 | 200 | | 2 | 400 | | 3 | 800 | | 4 | 1.500 | | 5 | 2.500 | | 6 | 4.000 | | 7 | 6.000 | | 8 | 8.000 | | 9 | 10.000 | | 10 | 13.000 | | 11 | 16.000 | | 12 | 20.000 | | 13 | 25.000 | | 14 | 30.000 | Cấp độ hiện tại và giới hạn hàng ngày của bạn được hiển thị trong **Settings → Email Domains**. ### Ảnh hưởng đến lần ra mắt theo quy mô đối tượng \{#impact-on-launch-by-audience-size\} | Quy mô đối tượng | Ảnh hưởng khi ra mắt | | ----------------- | --------------------------------------------- | | Dưới 200 người | Toàn bộ đối tượng nhận được ngay ngày đầu tiên | | 200–2.000 người | Giao hàng trải rộng trong vài ngày | | 2.000+ người | Giao hàng trải rộng trong 1–2 tuần | :::tip Hãy ra mắt chiến dịch đầu tiên ngay khi xác minh DNS hoàn thành. Bạn bắt đầu gửi càng sớm, domain của bạn càng nhanh tiến qua các cấp độ và đạt công suất tối đa mỗi ngày. ::: ## Giới hạn \{#limitations\} - **Một domain cho mỗi dự án**: Mỗi dự án Adapty chỉ có thể có một domain gửi. Để chuyển sang domain khác, hãy liên hệ hỗ trợ — dashboard không có tùy chọn "thay đổi domain". - **Tính duy nhất giữa các dự án**: Domain đã đăng ký cho dự án khác không thể dùng lại. Nếu bạn thấy *"Domain is already registered to another project"*, hãy chọn domain khác hoặc liên hệ hỗ trợ. - **Domain đã xác minh không thể xóa**: Khi bất kỳ subdomain nào đạt trạng thái **Success**, dashboard sẽ chặn việc xóa. Domain đang chờ xử lý có thể xóa được, nhưng bạn vẫn cần tự xóa bản ghi DNS khỏi nhà đăng ký của mình. - **Tiền tố subdomain cố định**: `mail.`, `email.`, và tiền tố Mail-From `hello.` được mã hóa cứng — không thể tùy chỉnh. Nếu các subdomain đó đã được sử dụng trong DNS của bạn, quá trình thiết lập sẽ xung đột. - **Chỉ chấp nhận apex domain**: Subdomain, dấu chấm ở cuối, và hostname một nhãn đều bị từ chối. - **Không hỗ trợ domain quốc tế hóa**: Punycode và IDN không được hỗ trợ. Domain phải chỉ gồm ký tự ASCII. ## Xử lý sự cố \{#troubleshooting\} | Vấn đề | Giải pháp | | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | | "Enter a valid domain (e.g. example.com)" | Kiểm tra đầu vào: chỉ apex domain, chỉ ASCII, TLD từ 2 ký tự trở lên, không có dấu gạch ngang ở đầu hoặc cuối. | | "Domain does not have valid DNS records" | Apex domain phải phân giải được. Xác nhận bản ghi NS đang hoạt động trước khi thử lại. | | "Domain is already registered to another project" | Chọn domain khác, hoặc liên hệ hỗ trợ nếu bạn cho rằng đăng ký đó là nhầm lẫn. | | "Verification check is on cooldown" | Chờ 60 giây giữa các lần kiểm tra thủ công. Kiểm tra tự động vẫn tiếp tục chạy nền. | | Xác minh bị kẹt ở trạng thái Pending | Kiểm tra xem bản ghi DNS có khớp chính xác không — không có dấu chấm ở cuối, đúng CNAME target. DNS có thể mất đến 48 giờ để lan truyền. | | "Cannot delete domain: one or more identities have been successfully verified" | Domain đã xác minh không thể xóa khỏi dashboard. Liên hệ hỗ trợ để được trợ giúp. | | Email bị vào thư mục spam | Xác nhận bản ghi DMARC đã được đăng ký. Domain mới cần thời gian warm-up — xem [Warm-up domain](#domain-warm-up). | | Tỷ lệ bounce cao | Kiểm tra danh sách đối tượng của bạn có chứa địa chỉ hợp lệ và đã đăng ký. Bounce làm chậm hoặc tạm dừng tiến trình lên cấp. | --- # File: mail-checkout --- --- title: "Thiết lập checkout cho Adapty Mail" description: "Xây dựng web paywall và kết nối nhà cung cấp thanh toán để cung cấp web checkout được cá nhân hóa cho chiến dịch email của bạn." --- Mỗi email mà Adapty Mail gửi đều chứa một link checkout duy nhất dành riêng cho người nhận đó. Khi họ nhấp vào, họ sẽ đến một web checkout funnel nhận dạng họ theo hồ sơ người dùng, hiển thị ưu đãi của bạn và xử lý thanh toán. Các checkout funnel nằm trong **Web Paywalls** bên trong Adapty Mail và được chỉnh sửa trong **web paywall builder** tích hợp sẵn. ## Yêu cầu \{#requirements\} - Một nhà cung cấp thanh toán web đã được cấu hình với các sản phẩm gói đăng ký của bạn. **Generate with AI** chỉ hỗ trợ Stripe và kết nối trong builder. **Use your own hosted paywall** chấp nhận bất kỳ nhà cung cấp nào — Stripe, Paddle, PayPal hoặc khác — vì việc thanh toán được xử lý ở phía bạn. Bạn không cần tài khoản riêng để dùng web paywall builder. Nó được tích hợp sẵn với Adapty Mail: một workspace sẽ được tự động tạo lần đầu tiên bạn đăng nhập, và bạn được đăng nhập vào editor bằng thông tin đăng nhập Adapty của mình. Điều này độc lập với bất kỳ web paywall nào bạn có thể đã cấu hình trên trang paywall của Adapty Dashboard chính — các web paywall của Adapty Mail là các thực thể riêng biệt, được quản lý hoàn toàn từ Adapty Mail. ## Thiết lập checkout funnel của bạn \{#set-up-your-checkout-funnel\} Trong Adapty Mail, đi tới **Web Paywalls → Create**. Bạn có hai lựa chọn: - **Generate with AI**: Web paywall builder tích hợp của Adapty Mail sẽ tự động tạo funnel cho bạn. Chỉ hỗ trợ Stripe — đối với Paddle hoặc PayPal, hãy dùng lựa chọn thứ hai. - **Use your own hosted paywall**: Kết nối một paywall bạn đã tự host, trên bất kỳ nhà cung cấp thanh toán nào. ### Generate with AI \{#generate-with-ai\} Trang Create hiển thị bảng **Prerequisites** ở đầu trang với các nút thao tác nội tuyến hướng dẫn bạn qua từng điều kiện tiên quyết — sẵn sàng thương hiệu, đăng nhập builder, kết nối Stripe, sản phẩm và bước xem lại và đăng cuối cùng. Thực hiện từng bước; bảng sẽ tự động làm mới khi mỗi bước hoàn tất. Khi các điều kiện tiên quyết đã xanh, nhấp **Generate** để mở hộp thoại tạo. Có hai lựa chọn cần thực hiện: - **Environment**: Chọn **Production** hoặc **Sandbox**. Sandbox sử dụng các sản phẩm chế độ test Stripe của bạn và là lựa chọn mặc định an toàn cho môi trường dev và local — tài khoản của nó được tách biệt hoàn toàn khỏi môi trường production, nên các giao dịch test không bao giờ ảnh hưởng đến dữ liệu thực. - **Plans**: Chọn tối đa **3 Stripe plans**. Mỗi plan là một tổ hợp sản phẩm + giá. Paywall sẽ trình bày các plan này dưới dạng các ưu đãi tại checkout. Nếu bạn chọn ít hơn 3, paywall chỉ hiển thị các plan bạn đã chọn. Nhấp **Generate** để chạy quá trình tạo. Khi hoàn tất, mở editor trong builder để xem lại kết quả và đăng. Sau đó, nhấp **Save**. :::important Paywall phải được đăng trước khi có thể phục vụ lưu lượng checkout. Các paywall chưa được đăng sẽ trả về lỗi khi người dùng nhấp vào link checkout trong email. ::: Để biết chi tiết về nhà cung cấp thanh toán trong builder (tài khoản Stripe, chế độ test vs live, thiết lập sản phẩm), xem [Cấu hình web paywall](web-paywall-configuration). ### Use your own hosted paywall \{#use-your-own-hosted-paywall\} 1. Trên trang Create, chọn **Enter URL manually**. 2. Dán URL paywall bạn đã host. URL phải bao gồm các placeholder `{email}` và `{external_profile_id}` dưới dạng query parameter — Adapty Mail sẽ điền các giá trị này cho từng người nhận để trang biết ai đang truy cập. Ví dụ: ``` https://example.com/paywall?email={email}&profile={external_profile_id} ``` 3. Lưu. Lựa chọn này hoạt động với bất kỳ nhà cung cấp thanh toán nào — Adapty Mail chỉ xử lý việc chuyển hướng và thay thế tham số; việc thanh toán và cá nhân hóa xảy ra hoàn toàn ở phía bạn. ## Giao diện checkout trông như thế nào \{#what-the-checkout-looks-like\} Khi người dùng nhấp vào link checkout, họ sẽ đến **Main conversion page**. Sau khi họ thực hiện thanh toán, họ sẽ thấy **Payment success** hoặc **Payment failed** — chỉ hiển thị một trang cho mỗi lần thử. **Main conversion page** Một trang trình bày bán hàng toàn trang. AI tạo nội dung và hình ảnh cho từng phần: | Phần | Nội dung AI tạo | |---|---| | Headline | Tiêu đề đậm, hướng đến lợi ích | | Subheadline | Đề xuất giá trị bổ sung | | Offer badge | Badge tạo cảm giác cấp bách (không bịa đặt giá — sử dụng ngôn ngữ khuyến mãi chung) | | CTA button | Văn bản hướng đến hành động, 2–5 từ | | Benefits | 3–6 thẻ lợi ích dạng emoji + văn bản | | Features | 3–8 mô tả tính năng với tiêu đề và phụ đề | | Plans | Tiêu đề lựa chọn plan và văn bản đồng hồ đếm ngược ưu đãi | | Social proof | Văn bản chứng minh cộng đồng và 3–5 đánh giá người dùng thực tế | | FAQ | 3–6 câu hỏi và câu trả lời phổ biến | | Guarantee | Văn bản đảm bảo hoàn tiền hoặc đảm bảo sự hài lòng | **Payment success** Thông báo chúc mừng kèm các bước tiếp theo và hình ảnh do AI tạo. **Payment failed** Thông báo thân thiện nhắc người dùng thử lại. Trạng thái checkout được giữ nguyên. ## Cách cá nhân hóa hoạt động \{#how-personalization-works\} Mỗi email chứa một URL checkout duy nhất với `customer_user_id` và địa chỉ email của người nhận được nhúng dưới dạng tham số: ``` https://your-funnel.com/?cid={{customer_user_id}}&email={{email}} ``` Adapty tự động tạo các URL này khi gửi từng email — không cần cấu hình gì trong web paywall builder. Khi người dùng nhấp, builder đọc các tham số để nhận dạng họ. Khi hoàn tất mua hàng, Adapty kết nối doanh thu với email cụ thể đã dẫn đến chuyển đổi. Dữ liệu này xuất hiện trong [Phân tích chiến dịch](mail-analytics). ## Xử lý sự cố \{#troubleshooting\} | Vấn đề | Giải pháp | |---|---| | Link checkout không mở được | Xác minh paywall đã được đăng trong web paywall builder | | Người dùng không được nhận dạng tại checkout | Xác nhận `Adapty.identify()` đã được gọi với đúng user ID trước khi email được gửi | | Mua hàng không được gắn với email | Kiểm tra xem tham số `cid` có trong URL checkout không — liên hệ hỗ trợ nếu thiếu tham số | --- # File: mail-email-campaigns --- --- title: "Chiến dịch email trong Adapty Mail" description: "Thiết kế chuỗi email nhiều bước, chọn đúng giọng điệu và nhắm đúng đối tượng người dùng." --- Một chiến dịch trong Adapty Mail là một chuỗi email hoàn chỉnh — nội dung, thiết kế, hình ảnh nổi bật và thời gian trễ — được tạo ra cho ứng dụng của bạn chỉ trong một lần. Bản thân chiến dịch không tự gửi: nó được lưu dưới dạng **nháp** và chỉ bắt đầu gửi khi bạn gắn nó vào một [flow](mail-create-flow), vốn liên kết chiến dịch với một trigger và đối tượng cụ thể. Sử dụng các hướng dẫn dưới đây để tạo chiến dịch, chọn đúng giọng điệu và nhắm đúng đối tượng người dùng. <CustomDocCardList ids={['mail-create-campaign', 'mail-suppression']} /> --- # File: mail-create-campaign --- --- title: "Tạo chiến dịch trong Adapty Mail" description: "Tạo toàn bộ chuỗi email từ metadata cửa hàng của ứng dụng và tinh chỉnh trước khi đính kèm vào flow." --- Adapty Mail tạo ra một chuỗi email hoàn chỉnh — nội dung, thiết kế, hình ảnh chính, dòng tiêu đề và độ trễ — từ metadata cửa hàng của ứng dụng. Không cần viết nội dung hay thiết kế thủ công. Chiến dịch được lưu dưới dạng bản nháp; chỉ bắt đầu gửi khi bạn đính kèm vào một [flow](mail-create-flow). ## Trước khi bắt đầu \{#before-you-start\} - **Đã có web paywall**: Mỗi chiến dịch phải liên kết với một web paywall. Backend sẽ từ chối chiến dịch không có web paywall. Xem [Tạo web paywall](mail-get-started#3-create-a-web-paywall) nếu bạn chưa có. - **Hồ sơ thương hiệu**: AI sử dụng hồ sơ thương hiệu để định hình nội dung, giọng điệu và hình ảnh trong toàn bộ chuỗi. Hãy thiết lập trong Adapty Mail tại **Brand** nếu chưa làm — xem [Brand](mail-brand). ## 1. Tạo chuỗi email \{#1-generate-the-sequence\} 1. Trong Adapty Mail, vào **Campaigns** và nhấn **Create**. 2. Đặt tên chiến dịch. 3. Trong dropdown **Web paywall**, chọn web paywall mà bạn muốn email liên kết đến. 4. Nhấn **Generate emails**. 5. Điền vào hộp thoại tạo: - **Tone**: Chọn từ danh sách. Các tùy chọn được tạo ra riêng cho danh mục ứng dụng của bạn — ứng dụng khác sẽ thấy tùy chọn khác. Lựa chọn của bạn định hình dòng tiêu đề, tiêu đề, nội dung và CTA trong tất cả các email; không ảnh hưởng đến bố cục hay hình ảnh chính. - **Language**: Chọn ngôn ngữ email. - **Custom prompt** (tùy chọn): Hướng dẫn tự do tối đa 2.000 ký tự. Dùng để nêu bật một chương trình khuyến mãi, dịp đặc biệt, đặc điểm đối tượng, các điểm cần đưa vào, hoặc hướng dẫn giọng điệu bổ sung mà các tùy chọn sẵn có không thể đáp ứng. - **Number of emails**: Mặc định, AI tự chọn số lượng dựa trên các thực hành tốt nhất và ngữ cảnh ứng dụng. Để tự đặt, nhấn **Set number manually** và chọn giá trị (**1–15**, mặc định **4**). Sau khi nhấn **Generate**, giọng điệu sẽ được khóa với chiến dịch đó. Để thử giọng điệu khác, hãy tạo chiến dịch mới — mỗi lần tạo có thể cho ra tổ hợp tùy chọn khác nhau. 6. Nhấn **Generate**. Quá trình tạo thường mất vài phút. Backend sẽ hết thời gian chờ sau 5 phút nếu không hoàn thành được — hãy thử lại nếu điều đó xảy ra. ## 2. Xem lại và tinh chỉnh \{#2-review-and-refine\} Sau khi tạo xong, toàn bộ chuỗi được hiển thị trong phần xem trước. Với mỗi email bạn có thể thấy: - **Các biến thể dòng tiêu đề**: Ba tùy chọn tiêu đề cho mỗi email. Adapty Mail kiểm tra chúng khi gửi và tiếp tục gửi tùy chọn có hiệu suất cao nhất — xem [A/B testing](mail-ab-testing). - **Tiêu đề, nội dung và CTA**: Khối nội dung chính. - **Hình ảnh chính**: Hình ảnh được tạo ra phù hợp với nội dung email và thương hiệu của bạn. - **Bố cục và độ trễ**: Cách email được sắp xếp, và khoảng thời gian gửi sau email trước đó. Thanh tiêu đề xem trước có nút **Theme toggle** (Auto, Light, Dark) — các nút biểu tượng ở góc trên bên phải của khung xem trước. Nó chỉ kiểm soát cách xem trước hiển thị; nội dung được tạo ra giống nhau ở tất cả các chế độ. Dùng để kiểm tra mỗi email trông như thế nào trong từng bảng màu mà không cần tạo lại. Bạn có thể: - **Tạo lại từng email riêng lẻ**: AI viết lại nội dung và hình ảnh chính mới cho một email đơn. Vị trí, thời gian và hệ thống thiết kế tổng thể (màu sắc, kiểu chữ, chế độ tối) vẫn giữ nguyên — chỉ email được chọn thay đổi. - **Chỉnh sửa HTML trực tiếp**: Mở trình chỉnh sửa mã HTML để kiểm soát chi tiết bất kỳ điều gì AI chưa làm đúng. :::note Email tự động thích ứng. Bố cục nhiều cột sẽ thu gọn thành một cột trên màn hình dưới 620 px, và mọi bố cục đều được kiểm tra trên Gmail (web và di động), Apple Mail (macOS và iOS), Outlook desktop, Yahoo Mail và Samsung Mail — ở cả chế độ sáng và tối. ::: ## 3. Lưu dưới dạng bản nháp \{#3-save-as-a-draft\} Nhấn **Create** để lưu chiến dịch dưới dạng bản nháp. Chưa có email nào được gửi — trình soạn thảo chiến dịch không có hành động "publish" hay "launch" riêng biệt. Trạng thái của chiến dịch phản ánh việc nó có được đính kèm vào flow đang hoạt động hay không: - **draft**: Chưa đính kèm vào flow nào. - **live**: Đã đính kèm vào flow và đang định tuyến người dùng. - **inactive**: Đã từng đính kèm, nhưng wrapper A/B test của flow đã kết thúc. - **archived**: Đã xóa khỏi dashboard. :::important Một chiến dịch ở trạng thái nháp sẽ không bao giờ tự gửi. Để bắt đầu gửi email, bạn cần: - Đính kèm chiến dịch trực tiếp vào một [flow](mail-create-flow), hoặc - Đưa vào một [A/B test](mail-ab-testing) và đính kèm A/B test đó vào flow. Cho đến khi một trong hai điều đó xảy ra, chiến dịch sẽ ở trạng thái `draft` và không có người nhận nào được tiếp cận. ::: --- # File: mail-suppression --- --- title: "Hủy đăng ký và danh sách chặn trong Adapty Mail" description: "Cách Adapty Mail ngừng gửi email đến người dùng — thông qua hủy đăng ký, bounce từ SES, khiếu nại, và cơ chế điều kiện dừng." --- Adapty Mail ngừng gửi email đến người dùng trong hai trường hợp khác nhau: - **Danh sách chặn (Suppression)**: Người dùng bị loại khỏi tất cả các lần gửi trong tương lai của dự án này (đã hủy đăng ký, bị bounce, khiếu nại, bị từ chối, hoặc bị giới hạn tốc độ). - **Điều kiện dừng (Stop condition)**: Chuỗi email hiện tại của người dùng bị hủy vì họ đã chuyển đổi. Họ không bị đưa vào danh sách chặn và vẫn đủ điều kiện nhận các chiến dịch khác. Cả hai cơ chế đều hoạt động theo từng dự án. Danh sách chặn trong một dự án Adapty không ảnh hưởng đến dự án khác. ## Hủy đăng ký \{#unsubscribe\} Mỗi email do Adapty Mail gửi đều có link hủy đăng ký ở chân trang. 1. Người dùng nhấp vào link. Adapty Mail mở trang xác nhận. 2. Người dùng xác nhận. Backend đánh dấu hồ sơ người dùng với `suppression_reason = 'unsubscribe'`, hủy chuỗi email còn lại, và loại hồ sơ đó khỏi các lần gửi trong tương lai của dự án. Token trong URL hủy đăng ký mã hóa `profile_id` và `scheduled_email_id`, nên không cần đăng nhập. :::note Adapty Mail cũng gửi header `List-Unsubscribe: <URL>, <mailto:>` kèm theo `List-Unsubscribe-Post: List-Unsubscribe=One-Click`. Gmail và Yahoo yêu cầu điều này với những người gửi số lượng lớn (RFC 8058). Các email client hỗ trợ header này cung cấp nút hủy đăng ký một chạm ngay trong hộp thư — không cần trang xác nhận. ::: ## Tự động đưa vào danh sách chặn \{#automatic-suppression\} Adapty Mail lắng nghe các sự kiện giao nhận từ AWS SES qua SNS và đưa người dùng vào danh sách chặn ngay lập tức khi xảy ra bất kỳ trường hợp nào sau đây: | Sự kiện | Mã lý do | Ý nghĩa | | --------- | ----------- | ---------------------------------------------------------------------------------------- | | Bounce | `bounce` | Địa chỉ email không hợp lệ, hộp thư đầy, hoặc tên miền không tồn tại. | | Complaint | `complaint` | Người dùng đánh dấu email là spam. | | Reject | `reject` | SES từ chối tin nhắn trước khi gửi. | | Throttle | `throttle` | Tốc độ gửi vượt quá giới hạn an toàn của tên miền. | Với mỗi sự kiện, kết quả đều như nhau: người dùng được thêm vào danh sách chặn, chuỗi email còn lại bị hủy, và họ bị loại khỏi các lần gửi trong tương lai của dự án. :::important Adapty Mail **không** phân biệt giữa hard bounce và soft bounce. Bất kỳ bounce nào — kể cả các trường hợp tạm thời như hộp thư đầy — đều đưa người dùng vào danh sách chặn ngay lập tức. Không có khoảng thời gian thử lại. ::: ## Điều kiện dừng \{#stop-condition\} Khi người dùng chuyển đổi giữa chừng trong chuỗi email, Adapty Mail hủy các email còn lại của họ với lý do `stop_condition`. Chuyển đổi có nghĩa là trạng thái gói đăng ký của họ đạt **Subscribed**, hoặc trạng thái sản phẩm mua một lần đạt **Purchased**. Điều kiện dừng khác với danh sách chặn: - **Danh sách chặn (Suppression)**: Loại người dùng khỏi tất cả các lần gửi trong tương lai của dự án. - **Điều kiện dừng (Stop condition)**: Chỉ hủy chuỗi email hiện tại. Người dùng vẫn đủ điều kiện nhận các chiến dịch khác — ví dụ, một flow gia hạn hoặc thu hút khách hàng cũ dành cho người đăng ký đang hoạt động. Các lần hủy do điều kiện dừng sẽ hiển thị cùng với danh sách chặn trong phân tích chiến dịch. ## Quản lý danh sách chặn \{#managing-suppression\} Adapty Mail không có giao diện dashboard để xem hoặc xóa người dùng khỏi danh sách chặn. Để bỏ chặn một hồ sơ người dùng — chẳng hạn ai đó vô tình đánh dấu email thử nghiệm là spam — hãy liên hệ với bộ phận hỗ trợ của Adapty. ## Những gì Adapty Mail xử lý cho việc tuân thủ \{#what-adapty-mail-handles-for-compliance\} Adapty Mail được tích hợp sẵn: - **Link hủy đăng ký**: Có trong chân trang mỗi email, được xử lý ngay khi người dùng xác nhận. - **Header List-Unsubscribe**: Được gửi kèm mỗi email để hủy đăng ký một chạm từ hộp thư (RFC 8058). - **Tự động đưa vào danh sách chặn**: Được kích hoạt khi xảy ra sự kiện bounce, complaint, reject, và throttle từ SES. Những phần bạn cần tự xử lý: - **Địa chỉ gửi thư vật lý**: CAN-SPAM yêu cầu phải có trong chân trang email. Adapty Mail không tự thêm vào — bạn cần tự thêm trong thiết kế chiến dịch của mình. - **Sự đồng ý opt-in rõ ràng**: Thu thập trước khi truyền email của người dùng vào Adapty. Xem [Collect user emails](mail-collect-emails). - **Yêu cầu xóa dữ liệu theo GDPR**: Adapty Mail không cung cấp endpoint "xóa dữ liệu của tôi". Phối hợp với bộ phận hỗ trợ Adapty nếu người dùng thực hiện quyền xóa dữ liệu của họ. --- # File: mail-flows --- --- title: "Flow trong Adapty Mail" description: "Cách flow định tuyến các chiến dịch đến đúng người dùng vào đúng thời điểm — trigger, phân khúc và quy tắc ưu tiên." --- <CustomDocCardList ids={['mail-create-flow']} /> Một **flow** biến một chiến dịch đã lưu thành các lần gửi có lịch trình. Flow liên kết một sự kiện trigger (trạng thái gói đăng ký của người dùng) với một phân khúc (những người dùng đủ điều kiện) và chiến dịch họ nhận được. Adapty Mail đánh giá mọi flow mỗi khi có sự kiện phù hợp xảy ra — không cần polling, không cần cron, không cần khởi động thủ công. ## Trigger \{#triggers\} Adapty Mail đi kèm với năm trigger cố định, mỗi trigger có view flow riêng dưới mục **Flows**: - **Never purchased**: Người dùng đã đăng ký nhưng chưa thực hiện mua hàng. Mục tiêu: kích hoạt và chuyển đổi lần đầu. Người dùng dùng thử không thuộc nhóm này — bắt đầu dùng thử được tính là gói đăng ký đang hoạt động. - **Renewal cancelled**: Người dùng đã tắt tự động gia hạn nhưng vẫn còn gói đăng ký đang hoạt động. Bao gồm cả người đăng ký trả phí và người dùng thử đã hủy trước khi chuyển đổi. Đây là khoảng thời gian tốt nhất để giữ chân họ — họ vẫn còn quyền truy cập. Hãy chia đối tượng trả phí và dùng thử qua bộ lọc phân khúc nếu nội dung cần khác nhau. - **Billing issue**: Thanh toán thất bại — thẻ bị từ chối hoặc hết hạn, hoặc thời gian ân hạn sau khi bỏ lỡ gia hạn. Mục tiêu: khôi phục khẩn cấp và hữu ích, không phải thúc đẩy bán hàng. Hãy khôi phục họ nhanh chóng — họ đã muốn thanh toán rồi. - **Expired**: Gói đăng ký đã hết hạn và quyền truy cập không còn. Bao gồm cả gói trả phí hết hạn và gói dùng thử hết hạn mà không chuyển đổi. Mục tiêu: thu hút lại họ. Bộ lọc phân khúc có thể tùy chỉnh nội dung cho đối tượng hết dùng thử và hết gói trả phí. - **Refunded**: Người dùng đã yêu cầu hoàn tiền sau khi mua. Mục tiêu: hiểu điều gì đã xảy ra và đề xuất lựa chọn phù hợp hơn. Giọng điệu nên khiêm tốn và chân thành, không phải bán hàng cứng nhắc. Trigger không thể cấu hình — bạn không thể tạo trigger tùy chỉnh hoặc mở rộng danh sách. ## Phân khúc All Users \{#the-all-users-segment\} Adapty Mail đi kèm với phân khúc **All Users** tích hợp sẵn không có bộ lọc — mọi người dùng trong dự án đều đủ điều kiện. Phân khúc này hữu ích nhất trong các flow như một hàng bắt tất cả (catch-all), phục vụ những người không được khớp bởi phân khúc cụ thể hơn ở phía trên. All Users không thể chỉnh sửa hoặc xóa. Xem [Phân khúc](mail-segments) để biết toàn bộ thông tin. ## Ưu tiên \{#priority\} Mỗi view trigger chứa một danh sách các hàng **phân khúc → chiến dịch** (hoặc phân khúc → A/B test), được sắp xếp theo mức độ ưu tiên. Khi người dùng kích hoạt trigger, Adapty Mail: 1. Duyệt các hàng từ trên xuống dưới. 2. Gửi chiến dịch trong hàng đầu tiên mà phân khúc của nó khớp. 3. Dừng lại. Các hàng tiếp theo không được đánh giá cho người dùng đó. Thứ tự quan trọng. Một phân khúc rộng được đặt phía trên phân khúc hẹp hơn sẽ bao phủ mọi người dùng mà lẽ ra sẽ khớp với hàng hẹp hơn đó. Để sắp xếp lại, kéo tay cầm ở bên trái của bất kỳ hàng nào — phần backend sẽ gán lại số thứ tự ưu tiên 1, 2, 3… dựa trên thứ tự đã lưu. :::important Hàng **All Users**, nếu có, phải ở cuối cùng (ưu tiên thấp nhất). Backend sẽ từ chối lưu nếu All Users không nằm ở vị trí cuối — nếu không, nó sẽ bao phủ mọi người dùng trước khi các phân khúc cụ thể hơn có cơ hội khớp. ::: ## Loại nội dung \{#content-types\} Một hàng có thể gửi một chiến dịch đơn lẻ hoặc một A/B test: - **Campaign**: Gửi một chiến dịch đến tất cả mọi người khớp với phân khúc. - **A/B Test**: Bao gồm hai hoặc nhiều chiến dịch với tỷ lệ có thể cấu hình, phân phối người dùng đến các chiến dịch đó một cách ngẫu nhiên, và theo dõi chỉ số theo từng biến thể. Xem [A/B testing](mail-ab-testing). ## Vòng đời \{#lifecycle\} Các hàng flow không có trạng thái nháp. Một hàng hoạt động ngay khi bạn lưu — từ thời điểm đó, người dùng kích hoạt trigger và khớp phân khúc sẽ được định tuyến đến chiến dịch của hàng đó. - **Tạo hàng mới**: Bắt đầu gửi ngay khi lưu. - **Chỉnh sửa hàng**: Thay đổi áp dụng cho người dùng kích hoạt trigger từ thời điểm đó trở đi. Người dùng đang ở giữa chuỗi tiếp tục với cấu hình trước đó. - **Xóa hàng**: Người dùng mới sẽ không còn vào chuỗi nữa. Người dùng đang ở giữa chuỗi vẫn có thể tiếp tục nhận email đã lên lịch — không có hủy tự động. Các hàng A/B test có vòng đời riêng (**nháp → đang chạy → đã kết thúc**) được kiểm soát tách biệt khỏi bản thân hàng đó. Xem [A/B testing](mail-ab-testing). --- # File: mail-create-flow --- --- title: "Tạo và quản lý các hàng flow trong Adapty Mail" description: "Thêm, sắp xếp lại, chỉnh sửa và xóa các hàng trong một flow để định tuyến chiến dịch đến người dùng của bạn." --- Mỗi [flow](mail-flows) là một danh sách ưu tiên các hàng **phân khúc → chiến dịch** bên trong một chế độ xem trigger cố định. Hướng dẫn này đề cập đến cách thêm, sắp xếp lại, chỉnh sửa và xóa các hàng đó. Để hiểu các khái niệm về trigger, mức độ ưu tiên và loại nội dung, hãy xem [Flows](mail-flows). ## Thêm một hàng \{#add-a-row\} 1. Trong Adapty Mail, vào **Flows** và mở trigger bạn muốn cấu hình. 2. Nhấp **Create** để mở hộp thoại. 3. Trong hộp thoại: - **Segment**: Chọn một phân khúc, hoặc **All Users** để áp dụng cho tất cả. - **Content type**: **Campaign** cho một chiến dịch đơn lẻ, hoặc **A/B Test** để so sánh nhiều chiến dịch — xem [A/B testing](mail-ab-testing). - **Campaign**: Chọn chiến dịch cần gửi. 4. Nhấp **Save**. Hàng được kích hoạt ngay lập tức. Người dùng kích hoạt trigger và khớp với phân khúc sẽ bắt đầu nhận chiến dịch từ thời điểm đó. ## Sắp xếp lại các hàng \{#reorder-rows\} Kéo tay cầm ở bên trái của một hàng để thay đổi mức độ ưu tiên của nó. Adapty Mail tự động gán `priority: 1, 2, 3…` dựa trên thứ tự đã lưu. Hàng **All Users** phải ở vị trí cuối cùng — việc kéo nó lên trên một hàng khác sẽ bị chặn khi lưu. ## Chỉnh sửa một hàng \{#edit-a-row\} Nhấp **Change content** trên một hàng để mở lại hộp thoại với các giá trị hiện tại được điền sẵn. Bạn có thể thay đổi phân khúc, loại nội dung và chiến dịch, sau đó nhấp **Save** để áp dụng. Một hàng sử dụng A/B test chỉ có thể được chỉnh sửa khi test đang ở trạng thái **draft**. Sau khi test được khởi chạy, nội dung của nó sẽ bị khóa cho đến khi bạn kết thúc test. ## Xóa một hàng \{#delete-a-row\} Mở menu hành động của hàng và nhấp **Delete**. Không có hộp thoại xác nhận — hàng sẽ bị xóa ngay lập tức. - **Hàng Campaign**: Có thể xóa bất kỳ lúc nào. - **Hàng có A/B test đang chạy**: Không thể xóa. Hãy kết thúc test trước bằng **Finish A/B test**, rồi mới xóa hàng. :::note Xóa một hàng sẽ ngăn người dùng mới bước vào chuỗi. Những người dùng đang ở giữa chuỗi có thể vẫn tiếp tục nhận các email đã lên lịch — không có tính năng hủy tự động. ::: --- # File: mail-segments --- --- title: "Phân khúc trong Adapty Mail" description: "Tạo các phân khúc đối tượng có thể tái sử dụng dựa trên dữ liệu hồ sơ người dùng và lịch sử mua hàng để nhắm mục tiêu cho các flow và A/B test." --- **Phân khúc** là một lát đối tượng có thể tái sử dụng. Bạn định nghĩa nó một lần — trong **Segments** — và tham chiếu từ các flow và A/B test. Phân khúc là định nghĩa bộ lọc, không phải ảnh chụp tĩnh: chúng được đánh giá theo yêu cầu khi trigger của flow kích hoạt, vì vậy thành viên trong phân khúc luôn phản ánh dữ liệu hồ sơ người dùng mới nhất. ## Tạo phân khúc \{#create-a-segment\} 1. Trong Adapty Mail, vào **Segments** và nhấn **+ Create**. Trang tạo phân khúc sẽ mở ra với tiêu đề **New Segment**. 2. Đặt **Name** (bắt buộc) và **Description** (tùy chọn) cho phân khúc. 3. Trong phần **Filters**, nhấn **Add filter** cho mỗi [điều kiện](#available-filter-fields) bạn muốn thêm. Mỗi filter sẽ hiển thị dưới dạng thẻ thu gọn được đặt tên **Filter 1**, **Filter 2**, v.v. 4. Với mỗi filter, chọn trường, chọn toán tử và nhập giá trị so sánh. 5. Lưu phân khúc. :::important Các bộ lọc được kết hợp bằng **AND** — người dùng phải thỏa mãn tất cả các bộ lọc mới được tính vào phân khúc. Logic OR và nhóm lồng nhau không được hỗ trợ. Mỗi trường chỉ xuất hiện một lần trong một phân khúc; để so sánh cùng một trường với nhiều giá trị khác nhau, hãy tách logic thành các phân khúc riêng biệt. ::: ## Nhập phân khúc từ Adapty \{#import-a-segment-from-adapty\} Thay vì tự xây dựng phân khúc từ các bộ lọc, bạn có thể nhập phân khúc đối tượng có sẵn từ Adapty Dashboard chính. 1. Trên trang **Segments**, nhấp vào **Import**. 2. Xem lại danh sách. **Importable segments** hiển thị ô checkbox và các điều kiện lọc của chúng. Những phân khúc **can't be imported** sẽ bị làm mờ kèm lý do cụ thể, ví dụ như trường không được hỗ trợ (chẳng hạn dữ liệu Apple Ads attribution), toán tử không được hỗ trợ, hoặc trạng thái gói đăng ký không có tương đương trong Adapty Mail. 3. Chọn các phân khúc bạn muốn và nhấp vào **Import**. :::important Việc nhập tạo ra một bản sao độc lập của các bộ lọc phân khúc tại thời điểm nhập — bản sao này không được đồng bộ với phân khúc gốc trong Adapty, và việc nhập cùng một phân khúc nhiều lần sẽ tạo ra các bản sao riêng biệt, vì Adapty Mail không phát hiện trùng lặp. ::: Các phân khúc được nhập sẽ bắt đầu ở trạng thái **Draft**, giống như các phân khúc bạn tự tạo thủ công, vì vậy bạn có thể chỉnh sửa bộ lọc của chúng ngay lập tức. ## Các trường lọc có sẵn \{#available-filter-fields\} | Nhóm | Trường | Kiểu | | -------------- | ------------------------------ | ------- | | Hồ sơ người dùng | Email | String | | Hồ sơ người dùng | Age | Integer | | Hồ sơ người dùng | Country | String | | Hồ sơ người dùng | External profile ID | String | | Hồ sơ người dùng | Created at | Date | | Trạng thái mua | Total revenue (USD) | Decimal | | Trạng thái mua | Subscription state | Enum | | Trạng thái mua | Subscription purchased at | Date | | Trạng thái mua | Subscription expires at | Date | | Trạng thái mua | One-time purchase state | Enum | | Trạng thái mua | One-time purchased at | Date | **Giá trị trạng thái gói đăng ký**: Never purchased, Subscribed, Auto-renew off, Billing issue, Grace period, Expired, Refunded. **Giá trị trạng thái sản phẩm mua một lần**: Never purchased, Purchased, Refunded. Các toán tử có sẵn theo loại trường: - **String**: equals, not equals, is set, is not set. - **Number**: equals, not equals, less than, greater than, less than or equal, greater than or equal, between, is set, is not set. - **Date**: equals, not equals, before, after, on or before, on or after, between, is set, is not set. ## Phân khúc hệ thống All Users \{#the-all-users-system-segment\} Adapty Mail đi kèm với phân khúc **All Users** được tích hợp sẵn, không có bộ lọc nào — mọi người dùng trong dự án đều thuộc phân khúc này. Bạn không thể chỉnh sửa hay xóa nó. Khi dùng trong một flow, nó đóng vai trò là hàng bắt tất cả ở cuối cùng (xem [Flows](mail-flows) để hiểu quy tắc ưu tiên). ## Vòng đời \{#lifecycle\} Trạng thái của một phân khúc được tính dựa trên cách nó đang được sử dụng: - **Draft**: Đã tạo, chưa gắn với bất kỳ flow hoặc A/B test nào. - **Live**: Đã gắn với một flow hoặc A/B test đang hoạt động. - **Inactive**: Đã từng được gắn, nhưng A/B test đã kết thúc hoặc hàng flow đã bị xóa. - **Archived**: Đã bị xóa mềm và ẩn khỏi danh sách chính. Trang Segments có bộ lọc trạng thái trên thanh công cụ để bạn có thể thu hẹp danh sách theo bất kỳ trạng thái nào trong số này. ## Chỉnh sửa và xóa phân khúc \{#edit-and-delete-a-segment\} - **Tên và mô tả**: Luôn có thể chỉnh sửa. - **Bộ lọc trên phân khúc Draft**: Có thể chỉnh sửa hoàn toàn. - **Bộ lọc trên phân khúc Live**: Bị khóa. Khi một phân khúc đã được tham chiếu bởi một flow row đang hoạt động hoặc A/B test, các bộ lọc sẽ trở thành chỉ đọc. Bạn chỉ có thể đổi tên hoặc cập nhật mô tả. Để thay đổi mục tiêu, hãy tạo một phân khúc mới và hoán đổi flow row. - **Xóa**: Xóa mềm phân khúc. Các phân khúc Live không thể xóa — hãy xóa chúng khỏi flow (hoặc kết thúc A/B test) trước. ## Hạn chế \{#limitations\} - **Không có logic OR, không lồng nhau**: Các bộ lọc chỉ kết hợp với nhau bằng AND. - **Một trường mỗi phân khúc**: Một phân khúc không thể có hai bộ lọc trên cùng một trường (ví dụ: hai kiểm tra quốc gia). - **Không xem trước số lượng**: Trình chỉnh sửa không hiển thị số người dùng hiện tại khớp với các bộ lọc. - **Bộ lọc bị khóa khi đã hoạt động**: Các phân khúc đang hoạt động chỉ có thể chỉnh sửa tên và mô tả. --- # File: mail-profiles --- --- title: "Hồ sơ người dùng trong Adapty Mail" description: "Xem toàn bộ khách hàng trong dự án của bạn — thuộc tính, trạng thái mua hàng, mức độ tương tác email và toàn bộ lịch sử hoạt động." --- **Hồ sơ người dùng** là một khách hàng trong dự án của bạn. Trang **Profiles** liệt kê tất cả mọi người mà Adapty Mail biết đến, đồng thời hiển thị kết quả mua hàng, mức độ tương tác email và toàn bộ lịch sử hoạt động của từng người. Hồ sơ người dùng được thêm vào tự động: từ các email mà SDK thu thập, hoặc từ dữ liệu bạn gửi qua Adapty Mail API. :::tip Để nhóm các hồ sơ người dùng thành các đối tượng có thể tái sử dụng cho các flow và A/B test, hãy xem [Phân khúc](mail-segments). ::: ## Cách hồ sơ người dùng được đưa vào Adapty Mail \{#how-profiles-get-into-adapty-mail\} Adapty Mail tự động tạo hồ sơ người dùng từ hai nguồn: - **Adapty SDK**: SDK thu thập email và các giao dịch mua từ ứng dụng của bạn. Xem [Thu thập email người dùng](mail-collect-emails). - **Adapty Mail API**: Backend của bạn gửi hồ sơ người dùng và giao dịch theo hướng server-to-server. Xem [Gửi dữ liệu qua API](mail-send-data-via-api). Adapty Mail liên kết mọi email, lượt click và giao dịch mua hàng với hồ sơ người dùng thông qua `external_profile_id` ổn định của nó. Trang Profiles chỉ ở chế độ xem — bạn có thể xem hồ sơ người dùng và hủy đăng ký chúng, nhưng không thể tạo, chỉnh sửa hay xóa chúng. Dữ liệu đó thuộc về ứng dụng gốc hoặc API. ## Danh sách hồ sơ người dùng \{#the-profiles-list\} Danh sách hiển thị mỗi hồ sơ người dùng trên một hàng, mới nhất lên trước. Dùng ô tìm kiếm để tìm hồ sơ theo email, profile ID, hoặc external profile ID. | Cột | Hiển thị | | --- | --- | | Profile | Email của khách hàng và campaign đã gửi cho họ nhiều email nhất. | | Status | Trạng thái mua hàng của hồ sơ người dùng. Xem [Trạng thái hồ sơ người dùng](#profile-status). | | Country | Quốc gia của khách hàng. | | Open rate | Tỷ lệ đã mở chia cho tổng số đã gửi trên tất cả email. Dấu gạch ngang nghĩa là chưa có email nào được gửi. | | LTV | Giá trị trọn đời — tổng doanh thu từ hồ sơ người dùng này trên tất cả các nguồn. | | Joined | Thời điểm hồ sơ người dùng lần đầu tham gia Adapty Mail. | | Last activity | Thời điểm gần nhất hồ sơ người dùng tương tác với email, chẳng hạn như gửi, mở hoặc nhấp. | :::important **Joined** là ngày hồ sơ người dùng lần đầu được đưa vào Adapty Mail, dùng làm ngày "khách hàng kể từ". Đây là thời điểm dữ liệu được nhập vào, không phải ngày đăng ký gốc từ ứng dụng của bạn. ::: ### Trạng thái hồ sơ người dùng \{#profile-status\} Cột **Status** hiển thị trạng thái mua hàng của hồ sơ người dùng — tức là khách hàng đang ở đâu trong hành trình gói đăng ký và sản phẩm mua một lần. | Trạng thái | Ý nghĩa | | --- | --- | | Never purchased | Hồ sơ người dùng chưa mua gì. | | Purchased | Hồ sơ người dùng đã thực hiện sản phẩm mua một lần. | | Active subscriber | Hồ sơ người dùng có gói đăng ký đang hoạt động. | | Cancelling | Tự động gia hạn đã tắt; quyền truy cập kéo dài đến hết kỳ hiện tại. | | Billing issue | Thanh toán gia hạn thất bại. | | Grace period | Thanh toán thất bại, nhưng quyền truy cập vẫn tiếp tục trong thời gian ân hạn của cửa hàng. | | Churned | Gói đăng ký đã hết hạn và quyền truy cập kết thúc. | | Refunded | Một giao dịch mua đã được hoàn tiền. | :::note Trạng thái mua hàng hoàn toàn tách biệt với trạng thái đăng ký email. Một hồ sơ người dùng có thể là **Active subscriber** nhưng **Unsubscribed** khỏi email của bạn, hoặc **Churned** nhưng vẫn **Subscribed**. Trạng thái email hiển thị trên trang hồ sơ người dùng và kiểm soát việc Adapty Mail có thể gửi thư đến hồ sơ đó hay không. ::: ## Chi tiết hồ sơ người dùng \{#profile-details\} Nhấp vào một hồ sơ để mở trang chi tiết của hồ sơ đó. Phần đầu trang hiển thị email, quốc gia, nền tảng và ngày "customer since". Bên cạnh đó còn có ba huy hiệu trạng thái: trạng thái mua hàng, tỷ lệ mở và trạng thái **Subscribed** hoặc **Unsubscribed**. Năm chỉ số tương tác hiển thị ở đầu trang: - **Sent**: Số email đã gửi đến hồ sơ. - **Delivered**: Số email được nhà cung cấp hộp thư chấp nhận. - **Opened**: Số email mà hồ sơ đã mở. - **Clicked**: Số email mà hồ sơ đã nhấp vào liên kết. - **Revenue**: Hai con số — doanh thu được attribution và giá trị trọn đời. :::note **Doanh thu được phân bổ (Attributed revenue)** là doanh thu đến từ email của bạn: các giao dịch mua mà hồ sơ người dùng thực hiện sau khi tương tác với một chiến dịch. **Giá trị trọn đời (LTV)** là tổng doanh thu của hồ sơ người dùng từ tất cả các nguồn, bất kể email có tham gia hay không. Phần tiêu đề hiển thị doanh thu được phân bổ trước, sau đó là LTV. ::: Thẻ **Profile** liệt kê các thuộc tính của khách hàng: - **Platform**: Nền tảng thiết bị của khách hàng, chẳng hạn iOS hoặc Android. - **Country**: Quốc gia của khách hàng. - **Store country**: Quốc gia của tài khoản App Store hoặc Google Play của khách hàng. - **Gender**: Giới tính của khách hàng, nếu có. - **Age**: Tuổi của khách hàng, nếu đã cung cấp ngày sinh. - **Profile ID**: Mã định danh nội bộ của Adapty Mail cho hồ sơ người dùng. - **External ID**: `external_profile_id` từ ứng dụng hoặc backend của bạn. - **Custom attributes**: Các cặp key-value bạn đã gửi kèm theo hồ sơ người dùng. ### Trạng thái mua hàng \{#purchase-state\} Thẻ **Purchase state** hiển thị doanh thu và lịch sử mua hàng của hồ sơ người dùng. Giá trị trọn đời (lifetime value) được hiển thị ở trên cùng, tiếp theo là tối đa hai phần: - **Subscription**: Giá, cửa hàng, ngày bắt đầu, ngày gia hạn hoặc ngày hết hạn, và ID sản phẩm của gói đăng ký. - **One-time purchase**: Giá, cửa hàng, ngày mua, và ID sản phẩm của sản phẩm mua một lần gần nhất. Nếu hồ sơ người dùng chưa mua gì, thẻ sẽ hiển thị **No purchase yet**. Thẻ **Segments** liệt kê tất cả các phân khúc mà hồ sơ người dùng hiện đang thuộc về, hoặc hiển thị **Not in any segment** nếu không có phân khúc nào phù hợp. Việc xác định tư cách thành viên được đánh giá theo thời gian thực, vì vậy nó luôn phản ánh dữ liệu hồ sơ mới nhất. Xem [Phân khúc](mail-segments) để biết cách tạo chúng. ## Hành trình hoạt động \{#the-activity-journey\} Phần **Journey** là dòng thời gian ghi lại mọi sự kiện xảy ra với hồ sơ người dùng. Nó bắt đầu bằng **Profile created** và sau đó xen kẽ hai loại sự kiện: - **Sự kiện email**: Mỗi email được gửi đến hồ sơ người dùng, kèm theo hoạt động giao nhận, mở và nhấp. Mở rộng một email để xem các liên kết mà hồ sơ người dùng đã nhấp và bất kỳ giao dịch mua nào mà email đó tạo ra. - **Sự kiện giao dịch**: Các mốc quan trọng của gói đăng ký và sản phẩm mua một lần, chẳng hạn như bắt đầu, gia hạn, hủy, sự cố thanh toán, hết hạn và hoàn tiền. Các sự kiện giao dịch tương ứng với các nhãn hành trình sau: | `event_type` | Nhãn Journey | | --- | --- | | `subscription_started` | Subscription started | | `subscription_renewed` | Subscription renewed | | `subscription_renewal_cancelled` | Renewal cancelled | | `subscription_renewal_reactivated` | Renewal resumed | | `billing_issue_detected` | Billing issue | | `entered_grace_period` | Entered grace period | | `subscription_expired` | Subscription expired | | `subscription_refunded` | Subscription refunded | | `non_subscription_purchase` | One-time purchase | | `non_subscription_purchase_refunded` | Purchase refunded | Các sự kiện này được gửi đến Adapty Mail tự động thông qua SDK, hoặc bạn có thể tự gửi qua API. Xem [Gửi sự kiện giao dịch](mail-send-data-via-api#send-transaction-events) để tham khảo danh sách sự kiện. ## Hủy đăng ký nhận email cho một hồ sơ người dùng \{#unsubscribe-a-profile\} Để ngừng gửi email đến một hồ sơ người dùng, mở trang hồ sơ đó, nhấn **...**, rồi chọn **Unsubscribe**. Adapty Mail sẽ đánh dấu hồ sơ là đã hủy đăng ký và thêm vào danh sách chặn, khiến các chiến dịch và flow bỏ qua hồ sơ này. Thao tác này có tính idempotent: hồ sơ đã hủy đăng ký trước đó sẽ vẫn giữ nguyên trạng thái đó. Để hiểu đầy đủ về cơ chế chặn và cách hồ sơ tự hủy đăng ký, xem [Hủy đăng ký và danh sách chặn](mail-suppression). --- # File: mail-ab-testing --- --- title: "A/B testing trong Adapty Mail" description: "So sánh các chiến dịch email với nhau bằng cách gắn A/B test vào một flow." --- A/B test trong Adapty Mail so sánh hai hoặc nhiều chiến dịch email với nhau. Mỗi biến thể là một chiến dịch hoàn chỉnh, độc lập. Khi người dùng khớp với phân khúc của test trong một [flow](mail-flows), Adapty Mail sẽ định tuyến họ đến một trong các biến thể theo trọng số đã cấu hình và theo dõi lượt gửi, tương tác cũng như doanh thu theo từng biến thể. ## Biến thể là gì \{#what-a-variation-is\} Mỗi biến thể là một chiến dịch đầy đủ. Các biến thể có thể khác nhau ở bất kỳ yếu tố nào — nội dung, hình ảnh nổi bật, giọng văn, độ dài chuỗi email, hoặc khoảng thời gian chờ giữa các email. Bản thân A/B test không cho phép điều chỉnh những yếu tố đó trực tiếp; bạn tạo các chiến dịch riêng lẻ rồi thêm vào làm biến thể. ## Tạo A/B test \{#create-an-ab-test\} 1. Tạo các chiến dịch trước trong **Campaigns**. Mỗi biến thể cần một chiến dịch riêng. 2. Trong Adapty Mail, vào **A/B Tests** và nhấn **Create**. 3. Thêm từng chiến dịch làm biến thể và đặt trọng số. Tổng trọng số phải bằng **100%**. 4. Gán một phân khúc để kiểm soát nhóm người dùng nào được áp dụng test. 5. Lưu lại. Test được lưu ở trạng thái **draft** và chưa gửi bất kỳ thứ gì. Để chạy thực tế, test phải được gắn vào một flow. ## Khởi chạy từ flow \{#launch-from-a-flow\} A/B test không thể khởi chạy từ trang A/B Tests — cả việc khởi chạy lẫn kết thúc đều diễn ra bên trong một flow row. 1. Trong Adapty Mail, vào **Flows** và mở trigger mà bạn muốn chạy test. 2. Nhấn **Create** trên một row mới. Trong hộp thoại, đặt **Content type** thành **A/B Test**, chọn test bạn đã lưu, rồi nhấn **Save**. 3. Trên row đó, nhấn **Launch A/B test**. Trạng thái test chuyển từ **draft** sang **live** và người dùng mới vào khớp với phân khúc sẽ bắt đầu được định tuyến đến các biến thể. Xem [Tạo flow](mail-create-flow) để biết thêm về flow row. ## Cách định tuyến hoạt động \{#how-routing-works\} Khi người dùng kích hoạt trigger của flow và khớp với phân khúc của A/B test, Adapty Mail chọn biến thể theo phương pháp **chọn ngẫu nhiên có trọng số** — trọng số của mỗi biến thể quyết định tỷ lệ được chọn. Việc định tuyến không cố định theo từng người dùng. ## Xem kết quả \{#read-results\} Trên trang A/B Tests, mỗi biến thể hiển thị số liệu thô và tỷ lệ được tính toán: - **Delivery**: Sends, Deliveries, Bounces. - **Engagement**: Opens, Clicks, Unsubs. - **Revenue**: Purchases, Revenue. Xem [Phân tích chiến dịch](mail-analytics) để biết cách tính từng chỉ số và cách quy kết doanh thu. ## Kết thúc test \{#finish-the-test\} Giống như khởi chạy, việc kết thúc cũng diễn ra từ flow row, không phải trang A/B Tests. 1. Mở flow row nơi test đang chạy. 2. Nhấn **Finish A/B test**. 3. Trong hộp thoại **Finish A/B test**, chọn chiến dịch thắng từ dropdown **Replace with campaign** — hoặc để trống để xóa phân khúc khỏi flow hoàn toàn. 4. Xác nhận. :::note Người dùng đang ở giữa chuỗi email của bất kỳ biến thể nào — dù thắng hay thua — vẫn tiếp tục nhận email đã lên lịch. Họ không bị chuyển sang biến thể thắng. ::: ## Vòng đời \{#lifecycle\} A/B test trải qua bốn trạng thái: - **Draft**: Đã tạo, chưa được gắn vào flow row đang chạy. - **Live**: Đã gắn và khởi chạy; đang định tuyến người dùng mới vào. - **Finished**: Đã dừng qua **Finish A/B test**. - **Archived**: Đã xóa mềm khỏi danh sách. --- # File: mail-analytics --- --- title: "Phân tích trong Adapty Mail" description: "Phân tích hiệu suất chiến dịch theo chiến dịch, phân khúc, biến thể A/B test, tin nhắn hoặc điều kiện kích hoạt — và xem số liệu giao hàng, tương tác và doanh thu song song." --- Trang **Analytics** hiển thị hiệu suất chiến dịch của bạn theo năm chiều: chiến dịch, phân khúc, biến thể A/B, tin nhắn và trigger. Trang này kết hợp các chỉ số giao hàng với doanh thu được gán cho từng email, giúp bạn so sánh các biến thể, tìm phân khúc hoạt động tốt nhất và xác định nơi tập trung doanh thu. Trang có một biểu đồ ở trên và bảng phân tích chi tiết bên dưới. Nhấp vào bất kỳ hàng nào để xem chi tiết của một thực thể. ## Chọn khoảng thời gian \{#pick-a-time-range\} Thanh công cụ ở đầu trang kiểm soát cửa sổ thời gian và cách phân nhóm: - **Date range**: Các tùy chọn có sẵn (7 / 14 / 30 / 90 ngày gần đây, Tháng này, Tháng trước, 12 tháng gần đây, Từ đầu năm) hoặc bộ chọn **Custom range**. Mặc định là 30 ngày gần đây. - **Granularity**: Phân nhóm theo **Daily**, **Weekly** hoặc **Monthly**. Độ chi tiết tự động giảm khi phạm vi tăng lên — **Daily** chuyển sang **Weekly** khi vượt quá 92 ngày, và cả hai chuyển sang **Monthly** khi vượt quá 366 ngày. - **Chart style**: **Line**, **Area** hoặc **Bar**. Nếu trang hiển thị cảnh báo "range too wide", hãy thu hẹp phạm vi ngày, giảm độ chi tiết hoặc áp dụng bộ lọc. ## Nhóm, phân tích và lọc \{#group-break-down-filter\} Ba điều khiển bên dưới thanh công cụ quyết định những gì biểu đồ và bảng hiển thị: - **Group by**: Chiều dữ liệu dùng để chia tập dữ liệu thành các hàng. Các tùy chọn gồm **Campaigns**, **Segments**, **A/B variants** và **Triggers**. Với **No grouping**, trang gộp tất cả vào một hàng tổng hợp **All** duy nhất. - **Breakdown**: Chiều thứ hai dùng để chia mỗi hàng thành các hàng con. Khi cả **Group by** và **Breakdown** được đặt, mỗi hàng trong bảng có thể được mở rộng để hiển thị các nhóm con. Breakdown có thể là bất kỳ chiều nào — bao gồm **Messages** — ngoại trừ chiều đã được dùng làm **Group by**. - **Add filter**: Giới hạn tập dữ liệu theo các chiến dịch, phân khúc, biến thể A/B hoặc trigger cụ thể. Bộ lọc áp dụng cho cả biểu đồ và bảng. :::note **Messages** có thể dùng làm breakdown nhưng không thể dùng làm **Group by** cấp cao nhất hoặc bộ lọc. Để phân tích từng tin nhắn, hãy nhóm theo **Campaigns** với breakdown **Messages**, sau đó mở rộng hàng chiến dịch hoặc mở tin nhắn từ chế độ xem chi tiết. ::: ## Đọc biểu đồ \{#read-the-chart\} Biểu đồ hiển thị các chỉ số bạn chọn theo khoảng thời gian đã chọn. - **Metric category**: Chuyển đổi giữa **Email actions** (Sent, Delivered, Opened, Clicked, Bounced, Unsubscribed, Converted) và **Revenue**. - **Metric pills**: Chọn chỉ số nào cần vẽ. Ở chế độ tổng hợp (không nhóm), bạn có thể vẽ nhiều chỉ số trên cùng một biểu đồ. Khi bật nhóm, biểu đồ vẽ một chỉ số duy nhất — một đường cho mỗi nhóm — để các nhóm dễ phân biệt về mặt thị giác. - **Legend**: Khi bật nhóm, chú giải ở bên phải liệt kê tất cả các nhóm và cho phép bạn bật/tắt từng chuỗi. Các ô kiểm tra hiển thị trong bảng chỉ số bên dưới cũng kiểm soát hàng nào xuất hiện trên biểu đồ. ## Đọc bảng chỉ số \{#read-the-metrics-table\} Bên dưới biểu đồ, bảng chỉ số liệt kê một hàng cho mỗi nhóm. Một hàng tóm tắt ở đầu tổng hợp tất cả các hàng còn lại trong bảng. - **Sortable columns**: Nhấp vào tiêu đề cột bất kỳ để sắp xếp theo Tên, Sent, Delivered, Delivery rate, Opened, Open rate, Clicked, Click rate, Converted, Revenue, Bounced hoặc Unsubscribed. - **Visibility checkbox**: Bật/tắt hiển thị hàng trên biểu đồ. - **Expand row**: Khi đặt **Breakdown**, mũi tên ở bên trái mỗi hàng cho phép mở rộng thành các nhóm con. - **Open the drilldown**: Nhấp vào tên hàng để mở chế độ xem tập trung cho thực thể đó. Chế độ xem chi tiết hiển thị: - Biểu đồ và bộ chọn chỉ số tương tự trang chính, nhưng giới hạn trong một thực thể. - Tám thẻ tóm tắt ở dưới cùng: **Sent**, **Delivered** (kèm delivery rate), **Opened** (kèm open rate), **Clicked** (kèm click rate), **Bounced**, **Unsubscribed**, **Converted** và **Revenue**. Phạm vi ngày và độ chi tiết được kế thừa từ trang chính. Dùng **Back** trong breadcrumb để quay lại. ## Những gì được theo dõi \{#whats-tracked\} :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: Mỗi hàng — trên trang Analytics, trong drilldown, và trong các chế độ xem inline được mô tả bên dưới — đều hiển thị cùng một tập hợp các số liệu thô: - **Sent**: Email được gửi đến SES. - **Delivered**: Xác nhận giao hàng vào hộp thư đến từ SES. - **Bounced**: Bounce do SES báo cáo. Bounce cứng và bounce mềm không được phân biệt — cả hai đều tính là một **Bounced**. - **Opened**: Lượt tải pixel. Apple Mail Privacy Protection tải trước hình ảnh trên iOS 15+ và làm tăng số này — hãy dựa vào lượt nhấp và doanh thu để có tín hiệu chính xác hơn. - **Clicked**: Lượt nhấp vào liên kết trong nội dung email. - **Unsubscribed**: Hủy đăng ký qua liên kết ở chân trang hoặc header `List-Unsubscribe`. - **Converted**: Số hồ sơ người dùng duy nhất trong nhóm có lượt mua được gán trong khoảng thời gian. Chuyển đổi được phân nhóm theo ngày mua — nhấp vào tháng 3 nhưng mua vào tháng 4 sẽ được tính vào tháng 4. Một hồ sơ người dùng mua nhiều lần vẫn chỉ được tính một lần. - **Revenue**: Tổng doanh thu được gán (USD) từ các lần bắt đầu gói đăng ký, gia hạn và sản phẩm mua một lần. ## Tỷ lệ dẫn xuất \{#derived-rates\} Mỗi tỷ lệ được tính từ các số đếm thô ở trên: | Tỷ lệ | Công thức | | ------------- | -------------------- | | Delivery rate | Delivered / Sent | | Open rate | Opened / Delivered | | Click rate | Clicked / Delivered | Chế độ xem chi tiết hiển thị ba tỷ lệ tương tự bên cạnh các thẻ tóm tắt. ## Gán attribution doanh thu \{#revenue-attribution\} Doanh thu được gán theo cơ chế **last-click** trên liên kết được theo dõi: 1. Khi người nhận nhấp vào bất kỳ liên kết nào trong email, Adapty Mail lưu `scheduled_email_id` vào hồ sơ người dùng đó trong một kho lưu trữ ngắn hạn. 2. Nếu một sự kiện mua hàng đến sau đó mà chưa có attribution, Adapty Mail gán `scheduled_email_id` đã lưu vào giao dịch — với điều kiện thời điểm mua hàng là sau khi nhấp. 3. Các lượt mua không có lượt nhấp liên kết được theo dõi trước đó sẽ không được gán attribution. Tham số được theo dõi là `scheduled_email_id`. URL thanh toán cũng mang thông tin nhận dạng của người nhận qua các placeholder `{email}` và `{external_profile_id}` để paywall web có thể cá nhân hóa flow — đây là cơ chế riêng biệt với attribution. Xem [Thiết lập checkout](mail-checkout). ## Analytics nội tuyến trong Flows và A/B Tests \{#inline-analytics-in-flows-and-ab-tests\} Các chỉ số tương tự cũng xuất hiện nội tuyến bên cạnh các hàng đang được đo lường: - **Flows page**: Mỗi hàng phân khúc trong chế độ xem trigger hiển thị số liệu giao hàng, tương tác và doanh thu. - **A/B Tests page**: Các biến thể được liệt kê cạnh nhau với cùng một tập chỉ số, thuận tiện để so sánh trực tiếp các biến thể. Dùng trang Analytics khi so sánh giữa các chiến dịch hoặc xem chi tiết một thực thể, và dùng các chế độ xem nội tuyến khi bạn đang làm việc trong một hàng flow hoặc A/B test cụ thể. Định nghĩa chỉ số, tỷ lệ dẫn xuất và quy tắc attribution ở trên áp dụng như nhau cho cả ba chế độ xem. ## Giới hạn \{#limitations\} - **Không phân biệt bounce mềm và bounce cứng**: Mọi bounce — tạm thời hay vĩnh viễn — đều được gộp vào một số đếm **Bounced** duy nhất. - **Nhất quán theo thời gian, không theo thời gian thực**: Số liệu được tổng hợp từ các bảng sự kiện. Các sự kiện mới thường xuất hiện trong vài phút, nhưng không có đảm bảo streaming. - **Phạm vi kích thước bị giới hạn**: Phạm vi ngày rất rộng kết hợp với độ chi tiết cao có thể vượt quá giới hạn ô của biểu đồ. Trang sẽ hiển thị cảnh báo "range too wide" — hãy thu hẹp phạm vi, giảm độ chi tiết hoặc áp dụng bộ lọc. --- # File: configuration --- --- title: "Cấu hình tích hợp bên thứ ba" description: "Tìm hiểu cách cấu hình cài đặt Adapty để tối ưu hóa quản lý gói đăng ký." --- Với các tích hợp của Adapty, bạn có thể dễ dàng truyền các sự kiện gói đăng ký và dữ liệu mua hàng tới nền tảng hoặc quy trình làm việc mà bạn muốn. Dù bạn đang tìm kiếm thông tin chi tiết về hành vi người dùng, chiến lược tương tác khách hàng, hay phân tích sản phẩm nâng cao cho nhóm marketing, Adapty đều có thể dễ dàng chuyển tiếp các sự kiện in-app purchase tới tích hợp bạn chọn. Adapty tự động theo dõi các in-app purchase và các sự kiện gói đăng ký như dùng thử, chuyển đổi, gia hạn và hủy đăng ký. Các [sự kiện](events) này được tự động truyền tới các tích hợp bạn đã chọn, giúp bạn tương tác với khách hàng dựa trên giai đoạn hiện tại của họ và phân tích các hoạt động liên quan đến doanh thu trong ứng dụng. ## Cài đặt tích hợp \{#integration-settings\} <img src="/assets/shared/img/20bf659-CleanShot_2023-08-22_at_13.26.562x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Các tích hợp cung cấp các tùy chọn cấu hình sau đây, ảnh hưởng đến tất cả các sự kiện được gửi qua tích hợp này: | Cài đặt | Mô tả | |:--------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Reporting Proceeds** | Chọn cách hiển thị giá trị doanh thu: thuần sau khi trừ hoa hồng của App Store và Play Store, hoặc tổng (trước khi khấu trừ). Bật checkbox "Send sales as proceeds" để hiển thị doanh thu sau khi đã trừ hoa hồng của App Store / Play Store. | | **Send Trial Price** | Nếu được chọn, Adapty sẽ truyền giá gói đăng ký cho sự kiện Trial Started. | | **Exclude Historical Events** | Chọn để loại trừ các sự kiện xảy ra trước khi người dùng cài đặt ứng dụng có tích hợp Adapty SDK. Điều này giúp tránh trùng lặp sự kiện và đảm bảo báo cáo chính xác. Ví dụ: nếu người dùng kích hoạt gói đăng ký hàng tháng vào ngày 10 tháng 1 và cập nhật ứng dụng có Adapty SDK vào ngày 6 tháng 3, Adapty sẽ bỏ qua các sự kiện trước ngày 6 tháng 3 và giữ lại các sự kiện sau đó. | | **Report User's Currency** | Chọn xem doanh thu được báo cáo theo đơn vị tiền tệ của người dùng hay USD. | | **Send User Attributes** | Nếu bạn muốn gửi các thuộc tính riêng của người dùng như tùy chọn ngôn ngữ, và gói OneSignal của bạn hỗ trợ hơn 10 tag, hãy chọn tùy chọn này. Bật tùy chọn này cho phép bao gồm thông tin bổ sung ngoài 10 tag mặc định. Lưu ý rằng vượt quá giới hạn tag có thể gây ra lỗi. | | **Send Attributions** | Bật tùy chọn này để truyền thông tin attribution (ví dụ: attribution từ AppsFlyer) và nhận các thông tin liên quan. | | **Send Play Store purchase token** | Bật tùy chọn này để nhận token Play Store cần thiết để xác thực lại giao dịch mua nếu cần. Tùy chọn này sẽ thêm tham số `play_store_purchase_token` vào sự kiện. | | **Delay events with future datetime** | **Chỉ dành cho AppsFlyer và custom webhook**: Khi bật, các sự kiện gia hạn và chuyển đổi dùng thử sẽ được gửi vào ngày chúng thực sự xảy ra. Khi tắt (mặc định), các sự kiện này được gửi ngay khi phát hiện, ngay cả khi ngày đó là trong tương lai. | | **Data residency** | **Chỉ dành cho Mixpanel và Amplitude**: Chọn data residency để xác định nơi các sự kiện của bạn được xử lý và lưu trữ. | ## Cấu hình sự kiện \{#configure-the-events\} Bên dưới phần thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi tới nền tảng tích hợp đã chọn từ Adapty. Bạn nên bật những sự kiện mà bạn cần. Lưu ý rằng việc tùy chỉnh tên sự kiện chỉ khả dụng với một số tích hợp nhất định, trong khi với các tích hợp khác, tên sự kiện đã được đặt sẵn và không thể thay đổi. Ngoài ra, với một số tích hợp như [Airbridge](airbridge#configure-events-and-tags) chẳng hạn, bạn có thể linh hoạt gán nhiều tên sự kiện cho một sự kiện Adapty duy nhất. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/c79f5cd-screencapture-app-adapty-io-integrations-pushwoosh-2023-08-22-13_31_07.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Mặc dù chúng tôi khuyến nghị sử dụng tên sự kiện mặc định của Adapty, bạn vẫn hoàn toàn có thể tùy chỉnh tên sự kiện theo yêu cầu cụ thể của mình. --- # File: events --- --- title: "Sự kiện gửi đến tích hợp bên thứ ba" description: "Theo dõi các sự kiện gói đăng ký quan trọng bằng các công cụ phân tích của Adapty." --- Apple và Google gửi các sự kiện gói đăng ký trực tiếp đến máy chủ thông qua [App Store Server Notifications](enable-app-store-server-notifications) và [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). Do đó, ứng dụng di động không thể gửi sự kiện đến các hệ thống phân tích theo thời gian thực một cách đáng tin cậy. Chẳng hạn, nếu người dùng đăng ký nhưng không bao giờ mở lại ứng dụng, nhà phát triển sẽ không nhận được bất kỳ cập nhật trạng thái gói đăng ký nào nếu không có máy chủ. Adapty lấp đầy khoảng trống này bằng cách thu thập dữ liệu gói đăng ký và chuyển đổi thành các sự kiện dễ đọc. Các sự kiện tích hợp này được gửi ở định dạng JSON. Mặc dù tất cả sự kiện có cùng cấu trúc, các trường của chúng khác nhau tùy theo loại sự kiện, cửa hàng và cấu hình cụ thể. Bạn có thể tìm thấy các trường chính xác trong từng sự kiện trên các trang tích hợp tương ứng. Để hiểu cách xác định xem một sự kiện đã được xử lý thành công hay có sự cố gì đó, hãy xem trang [Trạng thái sự kiện](event-statuses). ## Các loại sự kiện \{#event-types\} Hầu hết các sự kiện đều được tạo và gửi đến tất cả các tích hợp đã cấu hình nếu chúng được bật. Tuy nhiên, sự kiện **Access level updated** chỉ kích hoạt nếu [tích hợp webhook](webhook) được cấu hình và sự kiện này được bật. Sự kiện này sẽ xuất hiện trong [Event Feed](https://app.adapty.io/event-feed) và cũng sẽ được gửi đến webhook, nhưng sẽ không được chia sẻ với các tích hợp khác. Nếu tích hợp webhook chưa được cấu hình hoặc loại sự kiện này chưa được bật, sự kiện **Access level updated** sẽ không được tạo và sẽ không xuất hiện trong [Event Feed](https://app.adapty.io/event-feed). | Tên sự kiện | Mô tả | |:-----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | subscription_started | Kích hoạt khi người dùng bắt đầu gói đăng ký trả phí không có thời gian dùng thử, tức là bị tính phí ngay lập tức. | | subscription_renewed | Xảy ra khi gói đăng ký được gia hạn và người dùng bị tính phí. Sự kiện này bắt đầu từ lần thanh toán thứ hai, dù là gói đăng ký có hay không có dùng thử. | | subscription_renewal_cancelled | Người dùng đã tắt tính năng tự động gia hạn gói đăng ký. Người dùng vẫn có thể sử dụng các tính năng cao cấp cho đến khi kết thúc chu kỳ đăng ký đã thanh toán. | | subscription_renewal_reactivated | Kích hoạt khi người dùng bật lại tính năng tự động gia hạn gói đăng ký. | | subscription_expired | Kích hoạt khi gói đăng ký hết hạn hoàn toàn sau khi bị hủy. Ví dụ: nếu người dùng hủy gói đăng ký vào ngày 12 tháng 12 nhưng vẫn còn hiệu lực đến ngày 31 tháng 12, sự kiện sẽ được ghi nhận vào ngày 31 tháng 12 khi gói đăng ký hết hạn. | | subscription_paused | Xảy ra khi người dùng kích hoạt tính năng [tạm dừng gói đăng ký](https://developer.android.com/google/play/billing/lifecycle/subscriptions#pause) (chỉ dành cho Android). | | subscription_deferred | Kích hoạt khi giao dịch mua gói đăng ký được [hoãn lại](https://adapty.io/glossary/subscription-purchase-deferral/), cho phép người dùng trì hoãn việc thanh toán trong khi vẫn duy trì quyền truy cập các tính năng cao cấp. Tính năng này có sẵn thông qua Google Play Developer API và có thể dùng cho các bản dùng thử miễn phí hoặc hỗ trợ người dùng gặp khó khăn về tài chính. | | non_subscription_purchase | Bất kỳ sản phẩm mua một lần nào, chẳng hạn như quyền truy cập trọn đời hoặc các sản phẩm consumable như tiền trong game. | | trial_started | Kích hoạt khi người dùng bắt đầu gói đăng ký dùng thử. | | trial_converted | Xảy ra khi thời gian dùng thử kết thúc và người dùng bị tính phí (lần mua đầu tiên). Ví dụ: nếu người dùng có thời gian dùng thử đến ngày 14 tháng 1 nhưng bị tính phí vào ngày 7 tháng 1, sự kiện sẽ được ghi nhận vào ngày 7 tháng 1. | | trial_renewal_cancelled | Người dùng đã tắt tính năng tự động gia hạn trong thời gian dùng thử. Người dùng vẫn có thể sử dụng các tính năng cao cấp cho đến khi thời gian dùng thử kết thúc nhưng sẽ không bị tính phí hay chuyển sang gói đăng ký. | | trial_renewal_reactivated | Xảy ra khi người dùng bật lại tính năng tự động gia hạn trong thời gian dùng thử. | | trial_expired | Kích hoạt khi thời gian dùng thử kết thúc mà không chuyển sang gói đăng ký. | | entered_grace_period | Xảy ra khi một lần thanh toán thất bại và người dùng bước vào thời gian ân hạn (nếu được bật). Người dùng vẫn có thể truy cập các tính năng cao cấp trong thời gian này. | | billing_issue_detected | Kích hoạt khi xảy ra sự cố thanh toán trong quá trình thực hiện giao dịch (ví dụ: số dư thẻ không đủ). | | subscription_refunded | Kích hoạt khi gói đăng ký được hoàn tiền (ví dụ: do Apple Support xử lý). | | non_subscription_purchase_refunded | Kích hoạt khi một sản phẩm mua một lần được hoàn tiền. | | access_level_updated | Xảy ra khi mức độ truy cập của người dùng được cập nhật. | Các sự kiện trên bao quát đầy đủ trạng thái của người dùng trong quá trình mua hàng. Hãy cùng xem một số ví dụ. ### Ví dụ 1 \{#example-1\} _Người dùng kích hoạt gói đăng ký hàng tháng vào ngày 1 tháng 4 với thời gian dùng thử 7 ngày. Vào ngày thứ 4, anh ta hủy đăng ký._ Trong trường hợp đó, các sự kiện sau sẽ được gửi: 1. `trial_started` vào ngày 1 tháng 4 2. `trial_renewal_cancelled` vào ngày 4 tháng 4 3. `trial_expired` vào ngày 7 tháng 4 ### Ví dụ 2 \{#example-2\} _Người dùng kích hoạt gói đăng ký hàng tháng vào ngày 1 tháng 4 với thời gian dùng thử 7 ngày. Vào ngày thứ 10, anh ta hủy đăng ký._ Trong trường hợp đó, các sự kiện sau sẽ được gửi: 1. `trial_started` vào ngày 1 tháng 4 2. `trial_converted` vào ngày 7 tháng 4 3. `subscription_renewal_cancelled` vào ngày 10 tháng 4 4. `subscription_expired` vào ngày 1 tháng 5 Để xem chi tiết các sự kiện nào được kích hoạt trong từng tình huống, hãy xem [Luồng sự kiện](event-flows). --- # File: event-flows --- --- title: "Luồng sự kiện" description: "Khám phá các sơ đồ chi tiết về luồng sự kiện gói đăng ký trong Adapty. Tìm hiểu cách các sự kiện gói đăng ký được tạo ra và gửi đến các tích hợp, giúp bạn theo dõi các thời điểm quan trọng trong hành trình của khách hàng." --- Trong Adapty, bạn sẽ nhận được nhiều sự kiện gói đăng ký khác nhau trong suốt hành trình của khách hàng trong ứng dụng. Các luồng gói đăng ký này phác thảo các kịch bản phổ biến để giúp bạn hiểu các sự kiện mà Adapty tạo ra khi người dùng đăng ký, hủy hoặc kích hoạt lại gói đăng ký. Lưu ý rằng Apple xử lý thanh toán gói đăng ký vài giờ trước thời điểm bắt đầu/gia hạn thực tế. Trong các luồng dưới đây, chúng tôi hiển thị cả việc bắt đầu/gia hạn gói đăng ký và việc tính phí xảy ra cùng một lúc để sơ đồ dễ đọc hơn. Ngoài ra, các sự kiện liên quan đến cùng một hành động xảy ra đồng thời và có thể xuất hiện trong **Event Feed** của bạn theo bất kỳ thứ tự nào, có thể khác với trình tự được hiển thị trong sơ đồ của chúng tôi. ## Vòng đời gói đăng ký \{#subscription-lifecycle\} ### Luồng mua lần đầu \{#initial-purchase-flow\} Luồng này xảy ra khi khách hàng mua gói đăng ký lần đầu tiên mà không có dùng thử. Trong trường hợp này, các sự kiện sau được tạo ra: - **Subscription started** - **Access level updated** để cấp quyền truy cập cho người dùng Khi đến ngày gia hạn gói đăng ký, gói đăng ký sẽ được gia hạn. Trong trường hợp này, các sự kiện sau được tạo ra: - **Subscription renewal** để bắt đầu một kỳ mới của gói đăng ký - **Access level updated** để cập nhật ngày hết hạn gói đăng ký, kéo dài quyền truy cập thêm một kỳ nữa Các tình huống khi thanh toán không thành công hoặc khi người dùng hủy gia hạn được mô tả trong [Luồng kết quả sự cố thanh toán](event-flows#billing-issue-outcome-flow) và [Luồng hủy gói đăng ký](event-flows#subscription-cancellation-flow) tương ứng. <img src="/assets/shared/img_webhook_flows/Initial_Purchase_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Luồng hủy gói đăng ký \{#subscription-cancellation-flow\} Khi người dùng hủy gói đăng ký của họ, các sự kiện sau được tạo ra: - **Subscription renewal canceled** để chỉ ra rằng gói đăng ký vẫn còn hiệu lực đến cuối kỳ hiện tại, sau đó người dùng sẽ mất quyền truy cập - Sự kiện **Access level updated** được tạo ra để tắt tự động gia hạn cho mức độ truy cập Khi gói đăng ký kết thúc, sự kiện **Subscription expired (churned)** được kích hoạt để đánh dấu kết thúc gói đăng ký. <img src="/assets/shared/img_webhook_flows/Subscription_Cancellation_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Nếu yêu cầu hoàn tiền được chấp thuận, sự kiện sau sẽ thay thế **Subscription expired (churned)**: - **Subscription refunded** để kết thúc gói đăng ký và cung cấp thông tin chi tiết về việc hoàn tiền <img src="/assets/shared/img_webhook_flows/Subscription_Cancellation_Flow_with_a_Refund.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đối với Stripe, gói đăng ký có thể bị hủy ngay lập tức, bỏ qua khoảng thời gian còn lại. Trong trường hợp này, tất cả các sự kiện được tạo ra đồng thời: - **Subscription renewal cancelled** - **Subscription expired (churned)** - **Access Level updated** để xóa quyền truy cập của người dùng Nếu yêu cầu hoàn tiền được chấp thuận, sự kiện **Subscription refunded** cũng được kích hoạt khi được phê duyệt. <img src="/assets/shared/img_webhook_flows/Subscription_Immediate_Cancellation_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Luồng kích hoạt lại gói đăng ký \{#subscription-reactivation-flow\} Nếu người dùng hủy gói đăng ký, gói đó hết hạn và sau đó họ mua lại cùng gói đăng ký đó, sự kiện **Subscription renewed** sẽ được tạo ra. Ngay cả khi có khoảng thời gian gián đoạn quyền truy cập, Adapty xử lý đây là một chuỗi giao dịch duy nhất, được liên kết bởi `vendor_original_transaction_id`. Vì vậy, việc mua lại được coi là một lần gia hạn. Các sự kiện **Access level updated** sẽ được tạo ra hai lần: - vào lúc kết thúc gói đăng ký để thu hồi quyền truy cập của người dùng - vào lúc mua lại gói đăng ký để cấp quyền truy cập <img src="/assets/shared/img_webhook_flows/Subscription_Rejoin_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Luồng tạm dừng gói đăng ký (chỉ Android) \{#subscription-pause-flow-android-only\} Luồng này áp dụng khi người dùng tạm dừng và sau đó tiếp tục gói đăng ký trên Android. Việc tạm dừng gói đăng ký có hiệu lực trì hoãn. Nếu người dùng tạm dừng gói đăng ký trước khi đến ngày gia hạn, gói đăng ký vẫn còn hiệu lực và người dùng vẫn được truy cập trong phần còn lại của kỳ thanh toán. 1. Khi người dùng tạm dừng gói đăng ký, họ kích hoạt sự kiện **Subscription paused (Android only)**. 2. Vào cuối kỳ gói đăng ký, Adapty kích hoạt sự kiện **Access level updated** để thu hồi quyền truy cập của người dùng. 3. Khi người dùng tiếp tục gói đăng ký, các sự kiện sau được kích hoạt: - **Subscription renewed** - **Access level updated** để khôi phục quyền truy cập của người dùng Các gói đăng ký này sẽ thuộc cùng một chuỗi giao dịch, được liên kết với cùng **vendor_original_transaction_id**. <img src="/assets/shared/img_webhook_flows/Subscription_Paused_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Luồng dùng thử \{#trial-flows\} Nếu bạn sử dụng tính năng dùng thử trong ứng dụng, bạn sẽ nhận được các sự kiện bổ sung liên quan đến dùng thử. ### Luồng dùng thử với chuyển đổi thành công \{#trial-with-successful-conversion-flow\} Luồng phổ biến nhất xảy ra khi người dùng bắt đầu dùng thử, cung cấp thẻ tín dụng và chuyển đổi thành công sang gói đăng ký tiêu chuẩn vào cuối kỳ dùng thử. Trong trường hợp này, các sự kiện sau được tạo ra tại thời điểm bắt đầu dùng thử: - **Trial started** để đánh dấu thời điểm bắt đầu dùng thử - **Access level updated** để cấp quyền truy cập Sự kiện **Trial converted** được tạo ra khi gói đăng ký tiêu chuẩn bắt đầu. <img src="/assets/shared/img_webhook_flows/Trial_Flow_with_Successful_Conversion.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Luồng dùng thử không chuyển đổi thành công \{#trial-without-successful-conversion-flow\} Nếu người dùng hủy dùng thử trước khi chuyển đổi sang gói đăng ký, các sự kiện sau được tạo ra tại thời điểm hủy: - **Trial renewal cancelled** để tắt chuyển đổi tự động từ dùng thử sang gói đăng ký - **Access level updated** để tắt gia hạn quyền truy cập Người dùng sẽ có quyền truy cập cho đến khi kết thúc kỳ dùng thử, khi đó sự kiện **Trial expired** được tạo ra để đánh dấu kết thúc dùng thử. <img src="/assets/shared/img_webhook_flows/Trial_Flow_without_Successful_Conversion.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Luồng kích hoạt lại gói đăng ký sau khi dùng thử hết hạn \{#subscription-reactivation-after-expired-trial-flow\} Nếu dùng thử hết hạn (do sự cố thanh toán hoặc hủy) và người dùng sau đó mua gói đăng ký, các sự kiện sau được tạo ra: - **Access level updated** để cấp quyền truy cập cho người dùng - **Trial converted** Ngay cả khi có khoảng thời gian gián đoạn giữa dùng thử và gói đăng ký, Adapty liên kết hai sự kiện này bằng `vendor_original_transaction_id`. Việc chuyển đổi này được coi là một phần của chuỗi giao dịch liên tục, bắt đầu bằng dùng thử miễn phí. Đó là lý do tại sao sự kiện **Trial converted** được tạo ra thay vì **Subscription started**. <img src="/assets/shared/img_webhook_flows/Subscription_Reactivation_Flow_after_Expired_Trial.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Thay đổi sản phẩm \{#product-changes\} Phần này đề cập đến mọi điều chỉnh đối với gói đăng ký đang hoạt động, chẳng hạn như nâng cấp, hạ cấp hoặc mua sản phẩm từ nhóm khác. ### Luồng thay đổi sản phẩm ngay lập tức \{#immediate-product-change-flow\} Sau khi người dùng thay đổi sản phẩm, nó có thể được thay đổi trong hệ thống ngay lập tức trước khi gói đăng ký kết thúc (chủ yếu trong trường hợp nâng cấp hoặc thay thế sản phẩm). Trong trường hợp này, tại thời điểm thay đổi sản phẩm: - Mức độ truy cập được thay đổi và hai sự kiện **Access level updated** được tạo ra: 1. để xóa quyền truy cập vào sản phẩm đầu tiên. 2. để cấp quyền truy cập vào sản phẩm thứ hai. - Gói đăng ký cũ kết thúc và hoàn tiền được thực hiện (sự kiện **Subscription refunded** được tạo ra với `cancellation_reason` = `upgraded`). Lưu ý rằng không có sự kiện **Subscription expired (churned)** nào được tạo ra; sự kiện **Subscription refunded** thay thế nó. - Gói đăng ký mới bắt đầu (sự kiện **Subscription started** được tạo ra cho sản phẩm mới). <img src="/assets/shared/img_webhook_flows/Immediate_Product_Change_Flow_Upgrade.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Nếu người dùng hạ cấp gói đăng ký, gói đăng ký đầu tiên sẽ kéo dài đến cuối kỳ đã thanh toán, và khi gói đăng ký kết thúc, nó sẽ được thay thế bằng gói đăng ký mới ở cấp thấp hơn. Trong trường hợp này, chỉ có sự kiện **Access level updated** để tắt tự động gia hạn quyền truy cập được tạo ra ngay lập tức. Tất cả các sự kiện khác sẽ được tạo ra tại thời điểm thực tế thay thế gói đăng ký: - Một sự kiện **Access level updated** khác được tạo ra để cấp quyền truy cập vào sản phẩm thứ hai. - Sự kiện **Subscription expired (churned)** được tạo ra để kết thúc gói đăng ký cho sản phẩm đầu tiên. - Sự kiện **Subscription started** được tạo ra để bắt đầu gói đăng ký mới cho sản phẩm mới. <img src="/assets/shared/img_webhook_flows/Delayed_Product_Change_Downgrade.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Luồng thay đổi sản phẩm trì hoãn \{#delayed-product-change-flow\} Cũng có một biến thể khi người dùng thay đổi sản phẩm vào thời điểm gia hạn gói đăng ký. Biến thể này rất giống với biến thể trước: một sự kiện **Access level updated** sẽ được tạo ra ngay lập tức để tắt tự động gia hạn quyền truy cập cho sản phẩm cũ. Tất cả các sự kiện khác sẽ được tạo ra tại thời điểm người dùng thay đổi gói đăng ký và nó được thay đổi trong hệ thống: - Một sự kiện **Access level updated** khác được tạo ra để cấp quyền truy cập vào sản phẩm thứ hai. - Sự kiện **Subscription expired (churned)** được tạo ra để kết thúc gói đăng ký cho sản phẩm đầu tiên. - Sự kiện **Subscription started** được tạo ra để bắt đầu gói đăng ký mới cho sản phẩm mới. <img src="/assets/shared/img_webhook_flows/Product_Change_on_Renewal_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Luồng kết quả sự cố thanh toán \{#billing-issue-outcome-flow\} Nếu các lần thử chuyển đổi dùng thử hoặc gia hạn gói đăng ký thất bại do sự cố thanh toán, những gì xảy ra tiếp theo phụ thuộc vào việc thời gian ân hạn có được bật hay không. Với thời gian ân hạn, nếu thanh toán thành công, dùng thử sẽ được chuyển đổi hoặc gói đăng ký được gia hạn. Nếu thất bại, cửa hàng ứng dụng sẽ tiếp tục cố gắng tính phí người dùng cho gói đăng ký và nếu vẫn thất bại, cửa hàng ứng dụng sẽ tự kết thúc dùng thử hoặc gói đăng ký. Do đó, tại thời điểm xảy ra sự cố thanh toán, các sự kiện sau được tạo ra trong Adapty: - **Billing issue detected** - **Entered grace period** (nếu thời gian ân hạn được bật) - **Access level updated** để cấp quyền truy cập đến cuối thời gian ân hạn Nếu thanh toán thành công sau đó, Adapty ghi lại sự kiện **Trial converted** hoặc **Subscription renewed**, và người dùng không mất quyền truy cập. Nếu thanh toán cuối cùng thất bại và cửa hàng ứng dụng hủy gói đăng ký, Adapty tạo ra các sự kiện sau: - **Trial expired** hoặc **Subscription expired (churned)** với `cancellation_reason: billing_error` - **Access level updated** để thu hồi quyền truy cập của người dùng <img src="/assets/shared/img_webhook_flows/Billing_Issue_Outcome_Flow_with_Grace_Period.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Không có thời gian ân hạn, Thời gian thử lại thanh toán (khoảng thời gian cửa hàng ứng dụng tiếp tục cố gắng tính phí người dùng) bắt đầu ngay lập tức. Nếu thanh toán không bao giờ thành công đến cuối thời gian ân hạn, luồng vẫn giống nhau: các sự kiện tương tự được tạo ra khi cửa hàng ứng dụng tự động kết thúc gói đăng ký: - Sự kiện **Trial expired** hoặc **Subscription expired (churned)** với `cancellation_reason` là `billing_error` - **Access level updated** để thu hồi quyền truy cập của người dùng <img src="/assets/shared/img_webhook_flows/Billing_Issue_Outcome_Flow_without_Grace_Period.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Luồng chia sẻ giao dịch mua giữa các tài khoản người dùng \{#sharing-purchases-across-user-accounts-flows\} Khi một <InlineTooltip tooltip="Customer User ID">[iOS](identifying-users#set-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), và [Unity](unity-identifying-users#setting-customer-user-id-on-configuration)</InlineTooltip> cố gắng khôi phục hoặc mở rộng gói đăng ký đã được liên kết với một <InlineTooltip tooltip="Customer User ID">[iOS](identifying-users#set-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), và [Unity](unity-identifying-users#setting-customer-user-id-on-configuration)</InlineTooltip> khác, cài đặt **Sharing paid access between user accounts** của Adapty sẽ kiểm soát cách quản lý quyền truy cập. Luồng sẽ thay đổi tùy thuộc vào tùy chọn được chọn. :::note Đối với các giao dịch Apple Family Sharing (`in_app_ownership_type=FAMILY_SHARED`), chỉ có sự kiện **Access level updated** được kích hoạt — các sự kiện gói đăng ký theo từng sản phẩm bên dưới sẽ không được kích hoạt. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. ::: :::note Nếu người dùng nhấn **Restore Purchases** nhưng đã có quyền truy cập trên cùng hồ sơ người dùng, thao tác khôi phục không có tác dụng và không có sự kiện webhook nào được kích hoạt. Các sự kiện trong phần này chỉ được kích hoạt khi quyền truy cập thực sự được chuyển giữa các hồ sơ. ::: Để xem nhanh các sự kiện nào được kích hoạt khi hồ sơ thứ hai yêu cầu một gói đăng ký hiện có, hãy sử dụng ma trận này. Các phần tiếp theo hiển thị toàn bộ JSON payload cho mỗi luồng. | Sự kiện | Enabled (mặc định) | Transfer access to new user | Disabled | | --- | --- | --- | --- | | Hồ sơ mới: **Access level updated** (`is_active=true`) | Được kích hoạt | Được kích hoạt | Không được kích hoạt | | Hồ sơ cũ: **Access level updated** (`is_active=false`) | Không được kích hoạt — cả hai hồ sơ đều giữ quyền truy cập | Được kích hoạt khi thiết bị mới được nhận dạng truyền giao dịch | Không được kích hoạt — hồ sơ gốc giữ quyền truy cập | | Trường `profiles_sharing_access_level` trong sự kiện mới | Liệt kê các hồ sơ khác chia sẻ mức độ truy cập | `null` | Không áp dụng — không có sự kiện nào được kích hoạt | Các lần gia hạn, hoàn tiền và hết hạn trên gói đăng ký đã được chuyển tiếp tục kích hoạt các sự kiện `subscription_renewed`, `subscription_refunded` và `subscription_expired` trên hồ sơ hiện đang nắm giữ mức độ truy cập. Sự kiện chuyển nhượng bản thân không phát ra sự kiện `subscription_started`, vì không có giao dịch mới nào được ghi lại — chỉ có attribution thay đổi. Để biết chi tiết hợp đồng theo từng chế độ, xem [Tài liệu tham khảo thực tế](sharing-paid-access-between-user-accounts#practical-reference). ### Luồng chuyển quyền truy cập sang người dùng mới \{#transfer-access-to-new-user-flow\} Tùy chọn được khuyến nghị là chuyển mức độ truy cập sang người dùng mới. Điều này giúp bảo toàn lịch sử giao dịch của người dùng gốc để phân tích nhất quán. Chỉ có 2 sự kiện **Access level updated** sẽ được tạo ra: 1. để xóa quyền truy cập của người dùng đầu tiên 2. để cấp quyền truy cập cho người dùng thứ hai <img src="/assets/shared/img_webhook_flows/Transfer_Access_to_New_User_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đây là phân tích chi tiết các trường liên quan đến gán mức độ truy cập và chuyển nhượng trong các sự kiện được tạo ra trong kịch bản này: - **User A: Access level updated (được gửi khi User A mua gói đăng ký trong ứng dụng)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": UserA, "event_properties": { "profile_has_access_level": true, }, "profiles_sharing_access_level": null } ``` - **User A: Access level updated (được gửi khi ứng dụng được cài đặt lại và User B đăng nhập, thu hồi quyền truy cập của User A)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": UserA, "event_properties": { "profile_has_access_level": false, }, "profiles_sharing_access_level": null } ``` - **User B: Access level updated (được gửi khi User B đăng nhập và quyền truy cập được cấp)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000001", "customer_user_id": UserB, "event_properties": { "profile_has_access_level": true, }, "profiles_sharing_access_level": null } ``` ### Luồng chia sẻ quyền truy cập giữa các người dùng \{#shared-access-between-users-flow\} Tùy chọn này cho phép nhiều người dùng chia sẻ cùng mức độ truy cập nếu thiết bị của họ được đăng nhập vào cùng Apple/Google ID. Điều này hữu ích khi người dùng cài đặt lại ứng dụng và đăng nhập bằng email khác — họ vẫn sẽ có quyền truy cập vào giao dịch mua trước đó. Với tùy chọn này, nhiều người dùng được xác định có thể chia sẻ cùng mức độ truy cập. Trong khi mức độ truy cập được chia sẻ, tất cả các giao dịch được ghi lại dưới <InlineTooltip tooltip="Customer User ID">[iOS](identifying-users#set-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), và [Unity](unity-identifying-users#setting-customer-user-id-on-configuration)</InlineTooltip> gốc để duy trì lịch sử giao dịch và phân tích đầy đủ. Do đó, chỉ có 1 sự kiện sẽ được tạo ra: **Access level updated** để cấp quyền truy cập cho người dùng thứ hai. <img src="/assets/shared/img_webhook_flows/Share_Access_Between_Users_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đây là phân tích chi tiết các trường liên quan đến gán mức độ truy cập và chia sẻ trong các sự kiện được tạo ra trong kịch bản này: **User B: Access level updated (được gửi khi User B đăng nhập và quyền truy cập được cấp)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": UserA, "event_properties": { "profile_has_access_level": true, }, "profiles_sharing_access_level": [ { "profile_id": "00000000-0000-0000-0000-000000000001, "customer_user_id": UserB } ] } ``` ### Luồng không chia sẻ quyền truy cập giữa các người dùng \{#access-not-shared-between-users-flow\} Với tùy chọn này, chỉ hồ sơ người dùng đầu tiên nhận được mức độ truy cập mới giữ nó vĩnh viễn. Điều này lý tưởng khi các giao dịch mua cần được gắn với một <InlineTooltip tooltip="Customer User ID">[iOS](identifying-users#set-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), và [Unity](unity-identifying-users#setting-customer-user-id-on-configuration)</InlineTooltip> duy nhất. <img src="/assets/shared/img_webhook_flows/Share_Access_Between_Users_Disabled_Flow.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: event-statuses --- --- title: "Trạng thái sự kiện tích hợp" description: "" --- Adapty xác định khả năng gửi thành công dựa trên mã trạng thái HTTP, coi bất kỳ phản hồi nào ngoài phạm vi `200-399` là thất bại. Bạn có thể theo dõi trạng thái của các sự kiện tích hợp trong **Event List** trên Adapty Dashboard. Hệ thống hiển thị trạng thái cho tất cả các tích hợp đã bật, bất kể loại sự kiện cụ thể có được bật cho tích hợp đó hay không. - Đen: Sự kiện đã được gửi thành công. - <span style={{ color: 'grey' }}>Xám:</span> Loại sự kiện bị tắt cho tích hợp này. - <span style={{ color: 'red' }}>Đỏ:</span> Có sự cố với tích hợp cần được xử lý. Để biết thêm chi tiết về các sự kiện thất bại, hãy di chuột qua tên tích hợp để xem tooltip với thông tin lỗi cụ thể. <img src="/assets/shared/img/event-status.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> **Event Feed** chỉ hiển thị dữ liệu trong hai tuần gần nhất để tối ưu hóa hiệu suất. Giới hạn này giúp cải thiện tốc độ tải trang, giúp người dùng dễ dàng điều hướng và phân tích các sự kiện một cách hiệu quả hơn. --- # File: adjust --- --- title: "Adjust" description: "Kết nối Adjust với Adapty để theo dõi gói đăng ký và phân tích tốt hơn." --- [Adjust](https://www.adjust.com/) là một trong những nền tảng đo lường di động (MMP) hàng đầu, thu thập và trình bày dữ liệu từ các chiến dịch marketing, giúp các công ty theo dõi hiệu quả chiến dịch của mình. Adapty cung cấp bộ dữ liệu đầy đủ để bạn theo dõi [sự kiện gói đăng ký](events) từ các cửa hàng ở một nơi duy nhất. Với Adapty, bạn có thể dễ dàng nắm bắt hành vi của người dùng, hiểu sở thích của họ, và sử dụng thông tin đó để giao tiếp một cách có mục tiêu và hiệu quả. Vì vậy, tích hợp này cho phép bạn theo dõi sự kiện gói đăng ký trong Adjust và phân tích chính xác doanh thu mà các chiến dịch của bạn tạo ra. Tích hợp giữa Adapty và Adjust hoạt động theo hai hướng chính. 1. **Adapty nhận dữ liệu attribution từ Adjust** Sau khi thiết lập tích hợp Adjust, Adapty sẽ bắt đầu nhận dữ liệu attribution từ Adjust. Bạn có thể dễ dàng truy cập và xem dữ liệu này trên trang hồ sơ người dùng. <img src="/assets/shared/img/98769d9-CleanShot_2023-08-11_at_14.39.182x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. **Adapty gửi sự kiện gói đăng ký đến Adjust** Adapty có thể gửi tất cả sự kiện gói đăng ký đã được cấu hình trong tích hợp của bạn đến Adjust. Nhờ đó, bạn có thể theo dõi các sự kiện này trong Adjust dashboard. Tích hợp này rất hữu ích để đánh giá hiệu quả của các chiến dịch quảng cáo. ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với Adjust \{#connect-adapty-to-adjust\} 1. Mở Adapty Dashboard và điều hướng đến [Integrations > Adjust](https://app.adapty.io/integrations/adjust). 2. Bật toggle ở đầu trang. 3. Điền vào các trường và thiết lập thông tin xác thực. <img src="/assets/shared/img/5064125-CleanShot_2023-08-11_at_14.43.382x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nếu bạn đã bật xác thực OAuth trên nền tảng Adjust, bạn bắt buộc phải cung cấp **OAuth Token** trong quá trình tích hợp cho ứng dụng iOS và Android của mình. 4. Tiếp theo, cung cấp **app tokens** cho ứng dụng iOS và Android. Mở Adjust dashboard và bạn sẽ thấy danh sách ứng dụng của mình. <img src="/assets/shared/img/adjust-apps.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::note Bạn có thể có các ứng dụng Adjust khác nhau cho iOS và Android, vì vậy trong Adapty có hai mục riêng biệt cho chúng. Nếu bạn chỉ có một ứng dụng Adjust, hãy điền cùng một thông tin. ::: 5. Chọn ứng dụng từ danh sách và sao chép **App Token**. Dán token vào trường tương ứng trên Adapty dashboard. <img src="/assets/shared/img/adjust-token.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Cấu hình sự kiện và tags \{#configure-events-and-tags\} Adjust hoạt động hơi khác so với các nền tảng khác. Bạn cần tạo sự kiện thủ công trong Adjust dashboard, lấy token sự kiện, rồi sao chép-dán chúng vào các sự kiện tương ứng trong Adapty. Vì vậy, bước đầu tiên là tìm token sự kiện cho tất cả các sự kiện bạn muốn Adapty gửi. Để làm điều đó: 1. Trong Adjust dashboard, mở ứng dụng của bạn và chuyển sang tab **Events**. <img src="/assets/shared/img/adjust-events.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 1. Sao chép token sự kiện và dán vào Adapty. Phía dưới thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi đến Adjust từ Adapty. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/adjust-event-token.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Adapty sẽ gửi sự kiện gói đăng ký đến Adjust thông qua tích hợp server-to-server, cho phép bạn xem tất cả sự kiện gói đăng ký trong Adjust dashboard và liên kết chúng với các chiến dịch thu hút người dùng. :::important Lưu ý những điều sau: - Adjust không hỗ trợ sự kiện cũ hơn 58 ngày. Vì vậy, nếu bạn có sự kiện cũ hơn 58 ngày, Adapty vẫn sẽ gửi đến Adjust nhưng thời gian của sự kiện sẽ được thay thế bằng timestamp hiện tại. - Adjust không hỗ trợ IPv6. Nếu bạn tắt tính năng thu thập IP trong SDK trong **App settings** hoặc khi kích hoạt SDK, chỉ có IPv6 phía backend được gửi đi và việc theo dõi có thể thất bại — hãy giữ tính năng thu thập IP của SDK được bật để đảm bảo sử dụng IPv4. ::: ### Kết nối ứng dụng với Adjust \{#connect-your-app-to-adjust\} Sau khi hoàn thành các bước trên, hãy thêm hai phương thức sau vào ứng dụng. Chúng sẽ thiết lập kết nối giữa ứng dụng và Adjust: 1. **Để gửi dữ liệu gói đăng ký đến Adjust**: Truyền Adjust device ID vào phương thức SDK `setIntegrationIdentifier()` 2. **Để nhận dữ liệu attribution từ Adjust**: Cập nhật dữ liệu attribution bằng phương thức SDK `updateAttribution()` Đối với Adjust phiên bản 5.0 trở lên, hãy sử dụng ví dụ sau: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers class AdjustModuleImplementation { func updateAdjustAdid() { Adjust.adid { adid in guard let adid else { return } // Adapty SDK 4.x Adapty.setIntegrationIdentifier(.adjustDeviceId(adid)) // Adapty SDK 3.x Adapty.setIntegrationIdentifier(key: "adjust_device_id", value: adid) } } func updateAdjustAttribution() { Adjust.attribution { attribution in guard let attribution = attribution?.dictionary() else { return } // Adapty SDK 4.x Adapty.updateAttribution(attribution, source: .adjust) // Adapty SDK 3.x Adapty.updateAttribution(attribution, source: "adjust") } } } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adjust.getAdid { adid -> if (adid == null) return@getAdid Adapty.setIntegrationIdentifier("adjust_device_id", adid) { error -> if (error != null) { // handle the error } } } Adjust.getAttribution { attribution -> if (attribution == null) return@getAttribution Adapty.updateAttribution(attribution, "adjust") { error -> // handle the error } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers Adjust.getAdid(adid -> { if (adid == null) return; Adapty.setIntegrationIdentifier("adjust_device_id", adid, error -> { if (error != null) { // handle the error } }); }); Adjust.getAttribution(attribution -> { if (attribution == null) return; Adapty.updateAttribution(attribution, "adjust", error -> { // handle the error }); }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers var adjustConfig = new AdjustConfig(appToken, environment); // Before submiting Adjust config... adjustConfig.setAttributionCallbackListener(attribution => { // Make sure Adapty SDK is activated at this point // You may want to lock this thread awaiting of `activate` adapty.updateAttribution(attribution, "adjust"); }); // ... Adjust.create(adjustConfig); Adjust.getAdid((adid) => { if (adid) adapty.setIntegrationIdentifier("adjust_device_id", adid); }); ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```javascript showLineNumbers 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<String, String>(); 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!; await Adapty().updateAttribution(attribution, source: "adjust"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="unity" label="Unity" default> ```csharp showLineNumbers // 1. To update ADID Adjust.GetAdid((adid) => { if (adid == null) { // handle the error return; } Adapty.SetIntegrationIdentifier("adjust_device_id", adid, (error) => { if (error != null) { // handle the error return; } }); }); // 2. To update Attribution // in your adjust configuration scope: adjustConfig.AttributionChangedDelegate = AttributionChangedCallback; public void AttributionChangedCallback(AdjustAttribution attributionData) { var attribution = new Dictionary<string, string>(); 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; // you will probably need to install Newtonsoft.Json package, if not yet var attributionJsonString = Newtonsoft.Json.JsonConvert.SerializeObject(attribution); Adapty.UpdateAttribution(attributionJsonString, "adjust", (error) => { if (error != null) { // handle the error } }); } ``` </TabItem> </Tabs> ## Cấu trúc sự kiện \{#event-structure\} Adapty gửi các sự kiện đã chọn đến Adjust theo cấu hình trong mục **Events names** trên [**trang tích hợp Adjust**](https://app.adapty.io/integrations/adjust). Mỗi sự kiện có cấu trúc như sau: ```json { "event_token": "EVENT_TOKEN_FROM_CONFIG", "app_token": "APP_TOKEN_FROM_CONFIG", "s2s": 1, "environment": "production", "created_at_unix": 1709294400, "currency": "USD", "revenue": 9.99, "customer_user_id": "user_12345", "external_device_id": "user_12345", "ip_address": "192.168.100.1", "user_agent": "Mozilla/5.0 (Linux; Android 14; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36", "android_id": "875646c2-4a56-4211-8931-168532479006", "gps_adid": "875646c2-4a56-4211-8931-168532479006", "callback_params": "{\"integration_event_id\":\"550e8400-e29b-41d4-a716-446655440000\",\"customer_user_id\":\"user_12345\",\"vendor_product_id\":\"com.example.app.yearly.premium\",\"transaction_id\":\"GPA.3312-4512-1100-55923\",\"original_transaction_id\":\"GPA.3312-4512-1100-55923\",\"store\":\"play_store\",\"store_country\":\"US\",\"price_usd\":9.99,\"proceeds_usd\":8.49,\"price_local\":9.99,\"proceeds_local\":8.49,\"net_revenue_usd\":8.49,\"net_revenue_local\":8.49,\"tax_amount_usd\":0.0,\"tax_amount_local\":0.0,\"consecutive_payments\":3,\"rate_after_first_year\":false}", "partner_params": "{\"integration_event_id\":\"550e8400-e29b-41d4-a716-446655440000\",\"customer_user_id\":\"user_12345\",\"vendor_product_id\":\"com.example.app.yearly.premium\",\"transaction_id\":\"GPA.3312-4512-1100-55923\",\"original_transaction_id\":\"GPA.3312-4512-1100-55923\",\"store\":\"play_store\",\"store_country\":\"US\",\"price_usd\":9.99,\"proceeds_usd\":8.49,\"price_local\":9.99,\"proceeds_local\":8.49,\"net_revenue_usd\":8.49,\"net_revenue_local\":8.49,\"tax_amount_usd\":0.0,\"tax_amount_local\":0.0,\"consecutive_payments\":3,\"rate_after_first_year\":false}" } ``` Trong đó | Tham số | Kiểu | Mô tả | |:---------------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `app_token` | String | App Token của Adjust từ cài đặt tích hợp. | | `event_token` | String | Event Token của Adjust được ánh xạ đến sự kiện Adapty cụ thể. | | `s2s` | Integer | Cờ sự kiện Server-to-Server. | | `environment` | String | `sandbox` hoặc `production`. | | `created_at_unix` | Integer | Timestamp của sự kiện tính bằng giây. | | `currency` | String | Mã tiền tệ (ví dụ: "USD") cho giao dịch. Chỉ được đưa vào khi doanh thu vượt quá 0.001, vì Adjust yêu cầu cả doanh thu và tiền tệ phải được gửi cùng nhau. | | `revenue` | Float | Doanh thu giao dịch. Chỉ được đưa vào khi giá trị vượt quá 0.001. Lưu ý rằng sự kiện hoàn tiền được gửi không kèm thuộc tính doanh thu vì Adjust không hỗ trợ giá trị âm. | | `customer_user_id` | String | Customer User ID của người dùng. | | `external_device_id` | String | Giống với `customer_user_id`. | | `ip_address` | String | Địa chỉ IP của người dùng (chỉ IPv4). | | `user_agent` | String | Chuỗi User Agent của thiết bị. | | `adid` | String | Adjust Device ID (nếu có). | | `android_id` | String | **Chỉ Android**. Google Advertising ID. | | `gps_adid` | String | **Chỉ Android**. Google Advertising ID. | | `idfa` | String | **Chỉ iOS**. ID for Advertisers. | | `idfv` | String | **Chỉ iOS**. ID for Vendors. | | `callback_params` | String | Chuỗi JSON chứa tất cả [các trường sự kiện](webhook-event-types-and-fields#for-most-event-types) có sẵn. Chỉ bao gồm các trường không null. | | `partner_params` | String | Giống với `callback_params`. | ## Khắc phục sự cố \{#troubleshooting\} ### Chênh lệch doanh thu \{#revenue-discrepancy\} Nếu có sự chênh lệch doanh thu giữa Adapty và Adjust, nguyên nhân có thể là không phải tất cả người dùng của bạn đều sử dụng phiên bản ứng dụng có Adapty SDK. Để đảm bảo tính nhất quán của dữ liệu, bạn có thể yêu cầu người dùng cập nhật ứng dụng lên phiên bản có Adapty SDK. --- # File: airbridge --- --- title: "Airbridge" description: "Kết nối Adapty với Airbridge để theo dõi thông tin marketing và attribution." --- [Airbridge](https://www.airbridge.io/) cung cấp phân tích hiệu suất marketing tích hợp cho website và ứng dụng di động bằng cách tổng hợp dữ liệu thu thập từ nhiều thiết bị, nền tảng và kênh khác nhau. Sử dụng Identity Resolution Engine của Airbridge, bạn có thể kết hợp dữ liệu định danh khách hàng phân tán từ các tương tác trên web và ứng dụng thành một định danh thống nhất dựa trên người dùng, giúp attribution chính xác hơn. Adapty cung cấp đầy đủ dữ liệu để bạn theo dõi [các sự kiện gói đăng ký](events) từ các cửa hàng tại một nơi. Với Adapty, bạn dễ dàng nắm bắt hành vi của người dùng, hiểu sở thích của họ và dùng thông tin đó để giao tiếp một cách có mục tiêu và hiệu quả. Tích hợp giữa Adapty và Airbridge hoạt động theo hai hướng chính. 1. **Nhận dữ liệu attribution từ Airbridge** Sau khi thiết lập tích hợp Airbridge, Adapty sẽ bắt đầu nhận dữ liệu attribution từ Airbridge. Bạn có thể dễ dàng xem dữ liệu này trên trang của người dùng. 2. **Gửi sự kiện gói đăng ký đến Airbridge** Adapty có thể gửi tất cả các sự kiện gói đăng ký đã được cấu hình trong tích hợp của bạn đến Airbridge. Nhờ đó, bạn có thể theo dõi các sự kiện này trong Airbridge dashboard, giúp đánh giá hiệu quả các chiến dịch quảng cáo. ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với Airbridge \{#connect-adapty-to-airbridge\} Để tích hợp Airbridge, vào [Integrations > Airbridge](https://app.adapty.io/integrations/airbridge), bật toggle từ tắt sang bật và điền thông tin vào các trường. Trước tiên, hãy thiết lập thông tin xác thực để tạo kết nối giữa hồ sơ Airbridge và Adapty của bạn. Cần có tên ứng dụng Airbridge và API token Airbridge. <img src="/assets/shared/img/2b31d90-Untitled-1_1.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Cả hai thông tin đều có thể tìm thấy trong Airbridge dashboard của bạn tại mục [Third-party Integrations > Adapty](https://app.airbridge.io/app/testad/integrations/third-party/adapty). <img src="/assets/shared/img/5a2f627-Screenshot_2023-02-21_at_11.19.29_AM.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trường Adapty API token được tạo sẵn từ phía backend của Adapty. Bạn cần sao chép giá trị Adapty API token và dán vào Airbridge Dashboard trong trường Adapty Authorization Token. <img src="/assets/shared/img/ff422d1-CleanShot_2023-03-01_at_17.11.412x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Cấu hình sự kiện và tags \{#configure-events-and-tags\} Bên dưới phần thông tin xác thực là ba nhóm sự kiện bạn có thể gửi từ Adapty đến Airbridge. <img src="/assets/shared/img/eb4e3a9-CleanShot_2023-08-22_at_13.58.472x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chỉ cần bật những sự kiện bạn cần. ### Kết nối ứng dụng với Airbridge \{#connect-your-app-to-airbridge\} Để tích hợp, bạn cần truyền `airbridge_device_id` vào profile builder và gọi `setIntegrationIdentifier` như ví dụ dưới đây: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "airbridge_device_id", value: AirBridge.deviceUUID() ) } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers Airbridge.getDeviceInfo().getUUID(object: AirbridgeCallback.SimpleCallback<String>() { override fun onSuccess(result: String) { Adapty.setIntegrationIdentifier("airbridge_device_id", result) { error -> if (error != null) { // handle the error } } } override fun onFailure(throwable: Throwable) { } }) ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```javascript showLineNumbers final deviceUUID = await Airbridge.state.deviceUUID; try { await Adapty().setIntegrationIdentifier( key: "airbridge_device_id", value: deviceUUID, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers try { const deviceId = await Airbridge.state.deviceUUID(); await adapty.setIntegrationIdentifier("airbridge_device_id", deviceId); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> Đọc thêm về airbridgeDeviceId trong [tài liệu Airbridge.](https://help.airbridge.io/en/developers/airbridge-device-id-faq) Adapty có thể mất tới 24 giờ để nhận dữ liệu attribution từ Airbridge sau một sự kiện gói đăng ký. Adapty sẽ hiển thị ngay trên dashboard sau khi nhận được. ## Cấu trúc sự kiện \{#event-structure\} Adapty gửi các sự kiện đã chọn đến Airbridge theo cấu hình trong phần **Events names** trên [**trang tích hợp Airbridge**](https://app.adapty.io/integrations/airbridge). Mỗi sự kiện có cấu trúc như sau: ```json { "user": { "externalUserID": "user_12345", "externalUserEmail": "user@example.com", "attributes": { "is_premium": true } }, "device": { "deviceUUID": "550e8400-e29b-41d4-a716-446655440000", "deviceModel": "iPhone 14 Pro", "osName": "iOS", "osVersion": "17.0.1", "locale": "en-US", "timezone": "America/New_York", "ifa": "00000000-0000-0000-0000-000000000000", "ifv": "00000000-0000-0000-0000-000000000000" }, "app": { "packageName": "com.example.app", "version": "1.2.3" }, "eventUUID": "d4f6f1f4-96fb-4a31-bafd-599fef77be90", "eventTimestamp": 1709294400000, "eventData": { "goal": { "category": "airbridge.subscribe", "customAttributes": { "isTrialConverted": true }, "semanticAttributes": { "transactionID": "GPA.3383-4699-1373-07113", "totalValue": 9.99, "currency": "USD", "period": "P1M", "isRenewal": true, "renewalCount": 2, "products": [ { "productID": "yearly.premium.6999", "name": "yearly.premium.6999", "position": 1 } ] } } } } ``` Trong đó: | Tham số | Kiểu | Mô tả | |:---------------------------------------------|:--------|:--------------------------------------------------------------------------------------------| | `user` | Object | Thông tin người dùng. | | `user.externalUserID` | String | Customer User ID của người dùng. | | `user.externalUserEmail` | String | Địa chỉ email của người dùng (nếu có). | | `user.attributes` | Object | Các thuộc tính tùy chỉnh của người dùng. | | `device` | Object | Thông tin thiết bị. | | `device.deviceUUID` | String | UUID thiết bị Airbridge. | | `device.deviceModel` | String | Model thiết bị (ví dụ: "iPhone 14 Pro"). | | `device.osName` | String | Tên hệ điều hành (ví dụ: "iOS", "Android"). | | `device.osVersion` | String | Phiên bản hệ điều hành. | | `device.ifa` | String | **Chỉ iOS**. ID for Advertisers. | | `device.ifv` | String | **Chỉ iOS**. ID for Vendors. | | `device.gaid` | String | **Chỉ Android**. Google Advertising ID. | | `app` | Object | Thông tin ứng dụng. | | `app.packageName` | String | Package name / bundle ID của ứng dụng. | | `app.version` | String | Phiên bản ứng dụng. | | `eventUUID` | String | ID duy nhất của sự kiện trong Adapty. | | `eventTimestamp` | Long | Timestamp của sự kiện tính bằng mili giây. | | `eventData` | Object | Chi tiết sự kiện. | | `eventData.goal.category` | String | Danh mục sự kiện Airbridge (ánh xạ từ sự kiện Adapty). | | `eventData.goal.semanticAttributes` | Object | Các thuộc tính sự kiện chuẩn. | | `...semanticAttributes.transactionID` | String | ID giao dịch từ cửa hàng. | | `...semanticAttributes.totalValue` | Float | Giá trị doanh thu. | | `...semanticAttributes.currency` | String | Mã tiền tệ (ví dụ: "USD"). | | `...semanticAttributes.period` | String | Chu kỳ gói đăng ký theo định dạng ISO 8601 (ví dụ: "P1M"). | | `...semanticAttributes.isRenewal` | Boolean | `true` nếu đây là giao dịch gia hạn. | | `...semanticAttributes.renewalCount` | Integer | Số lần gia hạn thành công. | | `...semanticAttributes.products` | Array | Danh sách sản phẩm liên quan đến sự kiện. | | `...semanticAttributes.products[].productID` | String | ID sản phẩm từ cửa hàng (ví dụ: "yearly.premium.6999"). | | `...semanticAttributes.products[].name` | String | Giống với `productID`. | | `...semanticAttributes.products[].position` | Integer | Vị trí của sản phẩm trong danh sách (luôn là 1). | --- # File: apple-search-ads --- --- title: "Apple Ads" description: "Tích hợp Apple Ads với Adapty để tối ưu hóa chuyển đổi gói đăng ký." --- :::important Tích hợp Apple Ads trong **App settings** chỉ dùng cho phân tích cơ bản và cho các tích hợp SplitMetrics Acquire và Asapty. [Adapty Ads Manager](adapty-ads-manager) sử dụng một kết nối riêng. Kết nối tài khoản Apple Ads của bạn trong [Adapty Ads Manager](adapty-ads-manager-get-started). ::: Adapty có thể giúp bạn lấy dữ liệu attribution từ Apple Ads và phân tích các chỉ số của bạn theo phân khúc chiến dịch và từ khóa. Adapty tự động thu thập dữ liệu attribution cho Apple Ads thông qua SDK và AdServices Framework. Sau khi thiết lập tích hợp Apple Ads, Adapty sẽ bắt đầu nhận dữ liệu attribution từ Apple Ads. Bạn có thể dễ dàng truy cập và xem dữ liệu này trên trang hồ sơ người dùng. <img src="/assets/shared/img/ba4a3e9-CleanShot_2023-08-21_at_15.14.592x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với framework AdServices \{#connect-adapty-to-the-adservices-framework\} Apple Ads thông qua [AdServices](https://developer.apple.com/documentation/adservices) yêu cầu một số cấu hình trong Adapty Dashboard, và bạn cũng cần bật tính năng này ở phía ứng dụng. Để thiết lập Apple Ads sử dụng framework AdServices qua Adapty, hãy làm theo các bước sau: #### Bước 1: Lấy public key \{#step-1-obtain-public-key\} Trong Adapty Dashboard, điều hướng đến [Settings -> Apple Ads.](https://app.adapty.io/settings/apple-search-ads) Tìm public key đã được tạo sẵn (Adapty cung cấp một cặp key cho bạn) và sao chép nó. <img src="/assets/shared/img/baa5998-CleanShot_2023-08-21_at_14.55.542x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::note Nếu bạn đang sử dụng dịch vụ thay thế hoặc giải pháp riêng của mình cho attribution trên Apple Ads, bạn có thể tải lên private key của riêng bạn. ::: #### Bước 2: Cấu hình quản lý người dùng trên Apple Ads \{#step-2-configure-user-management-on-apple-ads\} Trong [tài khoản Apple Ads](https://ads.apple.com/app-store) của bạn, hãy vào trang **Settings > User Management**. Để Adapty có thể lấy dữ liệu attribution, bạn cần mời một tài khoản Apple ID khác và cấp cho nó quyền truy cập API Account Manager. Bạn có thể dùng bất kỳ tài khoản nào bạn có quyền truy cập hoặc tạo một tài khoản mới chỉ dành cho mục đích này. Điều quan trọng là bạn phải có thể đăng nhập vào Apple Ads bằng Apple ID đó. <img src="/assets/shared/img/ec183b2-kdjsfldsfjkdsfdfd.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> #### Bước 3: Tạo thông tin xác thực API \{#step-3-generate-api-credentials\} Tiếp theo, đăng nhập vào tài khoản vừa thêm trong Apple Ads. Truy cập Settings -> API trong giao diện Apple Ads. Dán khóa công khai đã sao chép trước đó vào trường được chỉ định. Tạo thông tin xác thực API mới. #### Bước 4: Cấu hình Adapty với thông tin xác thực Apple Ads \{#step-4-configure-adapty-with-apple-ads-credentials\} Sao chép các trường Client ID, Team ID và Key ID từ cài đặt Apple Ads. Trong Adapty Dashboard, dán các thông tin xác thực này vào các trường tương ứng. <img src="/assets/shared/img/7356113-CleanShot_2023-08-21_at_15.08.512x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Kết nối ứng dụng với mạng AdServices \{#connect-your-app-to-the-adservices-network\} Sau khi hoàn tất [thiết lập AdServices framework](#connect-the-adservices-framework), Adapty sẽ tự động bắt đầu thu thập dữ liệu attribution Apple Search Ad. Bạn không cần thêm bất kỳ đoạn code SDK nào. Đối với ứng dụng iOS, dữ liệu attribution này sẽ **luôn** được ưu tiên hơn dữ liệu từ các nguồn khác. Nếu không muốn hành vi này, hãy *tắt* attribution ASA theo hướng dẫn bên dưới. ## Tắt tích hợp \{#disable-integration\} Để tắt attribution Apple Search Ads, mở tab [**App Settings** -> **Apple Search Ads**](https://app.adapty.io/settings/apple-search-ads), và gạt tắt công tắc **Receive Apple Search Ads attribution**. <img src="/assets/shared/img/asa-disable.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::warning Xin lưu ý rằng việc tắt tính năng này sẽ hoàn toàn ngừng nhận dữ liệu analytics từ ASA. Kết quả là, ASA sẽ không còn được sử dụng trong analytics hoặc gửi đến các tích hợp. Ngoài ra, SplitMetrics Acquire và Asapty sẽ ngừng hoạt động vì chúng dựa vào attribution của ASA để hoạt động đúng. Attribution đã nhận trước khi thay đổi này sẽ không bị ảnh hưởng. ::: ## Tải lên key của riêng bạn \{#uploading-your-own-keys\} :::note Tùy chọn Các bước này không bắt buộc cho attribution Apple Ads, chỉ cần thiết khi làm việc với các dịch vụ khác như Asapty hoặc giải pháp của riêng bạn. ::: Bạn có thể dùng cặp khóa public-private của riêng mình nếu đang sử dụng dịch vụ khác hoặc giải pháp riêng cho attribution ASA. ### Bước 1 \{#step-1\} Tạo private key trong Terminal ```text showLineNumbers title="Text" openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem ``` Tải lên trong Adapty Settings -> Apple Ads (nút Upload private key) ### Bước 2 \{#step-2\} Tạo public key trong Terminal ```text showLineNumbers title="Text" openssl ec -in private-key.pem -pubout -out public-key.pem ``` Bạn có thể dùng public key này trong cài đặt Apple Ads của tài khoản có vai trò API Account Manager. Vì vậy, bạn có thể sử dụng các giá trị Client ID, Team ID và Key ID đã tạo cho Adapty và các dịch vụ khác. --- # File: switch-from-appsflyer-s2s-api-2-to-3 --- --- title: "Chuyển từ AppsFlyer S2S API 2 sang 3" description: "Nâng cấp từ AppsFlyer S2S API 2 lên API 3 trong Adapty." --- Theo [thông báo chính thức từ AppsFlyer](https://support.appsflyer.com/hc/en-us/articles/20509378973457-Bulletin-Upgrading-the-AppsFlyer-S2S-API), để tăng cường bảo mật cho việc sử dụng API và giảm thiểu gian lận, AppsFlyer đã nâng cấp API server-to-server (S2S) dành cho các sự kiện trong ứng dụng. Endpoint hiện tại sẽ bị ngừng hỗ trợ trong tương lai, vì vậy chúng tôi khuyến nghị bạn bắt đầu lên kế hoạch chuyển đổi. Adapty hỗ trợ AppsFlyer S2S API 3 và cung cấp cho bạn trải nghiệm chuyển đổi liền mạch từ API 2. Lưu ý rằng quá trình chuyển đổi này là một chiều — bạn sẽ không thể quay lại API 2 sau khi đã thực hiện thay đổi. Để chuyển từ AppsFlyer S2S API 2 sang 3: 1. Truy cập [trang AppsFlyer](https://www.appsflyer.com/home) và đăng nhập. 2. Nhấp vào **Tên tài khoản của bạn** -> **Security Center** ở góc trên bên trái của dashboard. <img src="/assets/shared/img/be299ea-appsflyer_security_center.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Manage your account security**, nhấp vào nút **Manage your AppsFlyer API and S2S tokens**. 4. Nếu bạn chưa có S2S token, nhấp vào nút **New token**. Nếu đã có, hãy chuyển sang bước 8. <img src="/assets/shared/img/7934920-appsflyer_new_token.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Trong cửa sổ **New token**, nhập tên cho token. Tên này chỉ dùng để bạn tham khảo. 6. Chọn **S2S** trong danh sách **Choose type**. 7. Đừng quên nhấp vào nút **Create new token** để lưu token mới. 8. Trong cửa sổ **Tokens**, sao chép S2S token. <img src="/assets/shared/img/d014c25-appsflyer_tokens.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 9. Mở [**Integrations** -> **AppsFlyer**](https://app.adapty.io/integrations/appsflyer) trong Adapty Dashboard. 10. Trong trường **AppsFlyer S2S API**, chọn **API 3**. <img src="/assets/shared/img/c0b3e72-appsflyer_switch_API.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 11. Dán S2S key vừa sao chép vào các trường **Dev key for iOS** và **Dev key for Android**. 12. Nhấp vào nút **Save** để xác nhận việc chuyển đổi. Ngay lúc này, tích hợp của bạn sẽ chuyển ngay sang AppsFlyer S2S API 3 và các sự kiện mới sẽ được gửi đến URL mới: `https://api3.appsflyer.com/inappevent`. --- # File: asapty --- --- title: "Asapty" description: "Khám phá Asapty và vai trò của nó trong hệ sinh thái gói đăng ký của Adapty." --- Sử dụng tích hợp [Asapty](https://asapty.com/), bạn có thể tối ưu hóa các chiến dịch Search Ads. Adapty gửi các sự kiện gói đăng ký tới Asapty, giúp bạn xây dựng các dashboard tùy chỉnh dựa trên attribution từ Apple Search Ads. Tích hợp này không bổ sung dữ liệu attribution vào Adapty, vì chúng tôi đã có đầy đủ thông tin cần thiết trực tiếp từ [ASA](apple-search-ads). ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với Asapty \{#connect-adapty-to-asapty\} Để tích hợp Asapty, hãy vào [Integrations > Asapty](https://app.adapty.io/integrations/asapty) trong Adapty dashboard và điền giá trị cho trường Asapty ID. <img src="/assets/shared/img/895de2b-CleanShot_2023-08-14_at_18.57.462x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Asapty ID có thể tìm thấy trong mục Settings > General trong tài khoản Asapty của bạn. ### Cấu hình sự kiện và thẻ \{#configure-events-and-tags\} Bên dưới phần thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi tới Asapty từ Adapty. Chỉ cần bật những sự kiện bạn cần. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/58ddf41-CleanShot_2023-08-15_at_15.11.072x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chúng tôi khuyến nghị sử dụng tên sự kiện mặc định do Asapty cung cấp. Tuy nhiên, bạn có thể tùy chỉnh tên sự kiện theo nhu cầu của mình. ### Kết nối ứng dụng của bạn với Asapty \{#connect-your-app-to-asapty\} Sau khi hoàn thành các bước trên, Adapty sẽ tự động nhận dữ liệu attribution từ Asapty. Bạn không cần phải yêu cầu dữ liệu attribution một cách tường minh trong code ứng dụng. Để đảm bảo độ chính xác của dữ liệu attribution, hãy cấu hình Asapty để chia sẻ `customerUserId` cùng với dữ liệu của mỗi sự kiện. ## Cấu trúc sự kiện Asapty \{#asapty-event-structure\} Adapty gửi các sự kiện tới Asapty qua GET request sử dụng query parameter. Mỗi URL sự kiện có dạng như sau: ``` https://asapty.com/_api/mmpEvents/?source=adapty&asaptyid=a1b2c3d4&keywordid=12345&adgroupid=67890&campaignid=11223&conversiondate=1709294400000&event_name=subscription_renewed&install_time=1709100000&app_name=MyApp&json=%7B%22af_revenue%22%3A%229.99%22%2C%22af_currency%22%3A%22USD%22...%7D ``` Các query parameter: | Parameter | Type | Description | |:-----------------|:-------|:-----------------------------------------------------| | `source` | String | Luôn là "adapty". | | `asaptyid` | String | Asapty ID từ thông tin xác thực của bạn. | | `keywordid` | String | Apple Search Ads Keyword ID (nếu có). | | `adgroupid` | String | Apple Search Ads Ad Group ID (nếu có). | | `campaignid` | String | Apple Search Ads Campaign ID (nếu có). | | `conversiondate` | Long | Timestamp của sự kiện tính bằng **mili giây**. | | `event_name` | String | Tên sự kiện (được ánh xạ từ sự kiện Adapty). | | `install_time` | Long | Timestamp của lần cài đặt tính bằng giây. | | `app_name` | String | Tiêu đề ứng dụng từ Adapty (nếu có). | | `json` | String | Chuỗi JSON đã được URL-encode chứa chi tiết sự kiện (xem bên dưới). | Tham số `json` là một chuỗi JSON đã được URL-encode chứa các trường sau: | Parameter | Type | Description | |:--------------------------|:-------|:---------------------------------------------| | `af_revenue` | String | Số tiền doanh thu dưới dạng chuỗi. | | `af_currency` | String | Mã tiền tệ (ví dụ: "USD"). | | `transaction_id` | String | Transaction ID từ cửa hàng. | | `original_transaction_id` | String | Transaction ID gốc từ cửa hàng. | | `purchase_date` | Long | Timestamp mua hàng tính bằng mili giây. | | `original_purchase_date` | Long | Timestamp mua hàng gốc tính bằng mili giây. | | `environment` | String | `Production` hoặc `Sandbox`. | | `vendor_product_id` | String | Product ID từ cửa hàng. | | `profile_country` | String | Mã quốc gia dựa theo IP của người dùng. | | `store_country` | String | Mã quốc gia của cửa hàng người dùng. | ## Xử lý sự cố \{#troubleshooting\} - Đảm bảo bạn đã cấu hình [Apple Search Ads](apple-search-ads) trong Adapty và [tải lên thông tin xác thực](https://app.adapty.io/settings/apple-search-ads); nếu không có, Asapty sẽ không hoạt động. - Chỉ những hồ sơ người dùng có dữ liệu attribution ASA chi tiết (không phải organic) mới gửi sự kiện tới Asapty. Bạn sẽ thấy thông báo "The user profile is missing the required integration data." nếu dữ liệu attribution không đầy đủ. - Các hồ sơ người dùng được tạo trước khi cấu hình tích hợp sẽ không thể gửi sự kiện tới Asapty. - Nếu tích hợp với Adapty không hoạt động dù đã thiết lập đúng, hãy kiểm tra xem toggle **Receive Apple Search Ads attribution in Adapty** đã được bật trong tab [**App Settings** -> **Apple Search Ads**](https://app.adapty.io/settings/apple-search-ads) chưa. --- # File: branch --- --- title: "Branch" description: "Tích hợp Branch với Adapty để theo dõi deep link và lượt chuyển đổi trong ứng dụng." --- [Branch](https://www.branch.io/) giúp doanh nghiệp tiếp cận, tương tác và đánh giá kết quả trên nhiều thiết bị, kênh và nền tảng khác nhau. Đây là nền tảng thân thiện với người dùng, được thiết kế để tăng doanh thu di động thông qua các liên kết chuyên biệt hoạt động liền mạch trên mọi thiết bị, kênh và nền tảng. Adapty cung cấp bộ dữ liệu đầy đủ giúp bạn theo dõi [các sự kiện gói đăng ký](events) từ các cửa hàng tại một nơi. Với Adapty, bạn có thể dễ dàng xem hành vi của người dùng, tìm hiểu sở thích của họ và sử dụng thông tin đó để giao tiếp theo cách có mục tiêu và hiệu quả. Tích hợp giữa Adapty và Branch hoạt động theo hai cách chính. 1. **Nhận dữ liệu attribution từ Branch** Sau khi thiết lập tích hợp Branch, Adapty sẽ bắt đầu nhận dữ liệu attribution từ Branch. Bạn có thể dễ dàng xem dữ liệu này trên trang hồ sơ người dùng. <img src="/assets/shared/img/49f4aa7-CleanShot_2023-08-11_at_17.36.072x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. **Gửi sự kiện gói đăng ký đến Branch** Adapty có thể gửi tất cả các sự kiện gói đăng ký được cấu hình trong phần tích hợp của bạn đến Branch. Nhờ đó, bạn có thể theo dõi các sự kiện này trong Branch dashboard. ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với Branch \{#connect-adapty-to-branch\} Để tích hợp Branch, vào [Integrations > Branch](https://app.adapty.io/integrations/branch) trong Adapty Dashboard, bật toggle từ tắt sang bật và điền thông tin vào các trường. <img src="/assets/shared/img/817a051-CleanShot_2023-08-11_at_15.54.372x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Để lấy giá trị cho trường **Branch Key**, mở [Account Settings](https://dashboard.branch.io/account-settings/profile) trên Branch và tìm trường **Branch Key**. Sử dụng giá trị này cho trường **Key test** (cho Sandbox) hoặc **Key live** (cho Production) trong Adapty Dashboard. Trong Branch, chuyển đổi giữa môi trường Live và Test để lấy key tương ứng. <img src="/assets/shared/img/130e58b-CleanShot_2023-08-11_at_15.24.162x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Cấu hình sự kiện và tag \{#configure-events-and-tags\} Phía dưới phần thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi đến Branch từ Adapty. Chỉ cần bật những sự kiện bạn cần. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). Bạn có thể gửi sự kiện kèm theo Proceeds \(sau khi Apple/Google khấu trừ\) hoặc chỉ doanh thu. Ngoài ra, bạn có thể chọn tùy chọn báo cáo theo tiền tệ của người dùng. <img src="/assets/shared/img/a645cf8-CleanShot_2023-08-11_at_15.18.282x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chúng tôi khuyên bạn nên dùng tên sự kiện mặc định do Adapty cung cấp. Tuy nhiên, bạn có thể thay đổi tên sự kiện tùy theo nhu cầu. Adapty sẽ gửi các sự kiện gói đăng ký đến Branch thông qua tích hợp server-to-server, cho phép bạn xem tất cả sự kiện gói đăng ký trong Branch dashboard và liên kết chúng với các chiến dịch acquisition của bạn. ### Kết nối ứng dụng với Branch \{#connect-your-app-to-branch\} 1. Gọi phương thức SDK `.setIntegrationIdentifier()` để khởi tạo kết nối. Bạn có thể truyền Branch Identity ID vào tham số `customerUserId`. :::note Các SDK bên thứ ba tạo user ID theo cách bất đồng bộ. ID có thể chưa sẵn sàng khi `Adapty.activate()` chạy. Nếu **Customer User ID** của bạn đến từ một trong các SDK này, hãy gọi `Adapty.activate()` mà không truyền ID đó. Khi ID đã có, gọi `setIntegrationIdentifier()`, sau đó gọi `identify()` với CUID. ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers do { // Adapty SDK 4.x try await Adapty.setIntegrationIdentifier(.branchId(<BRANCH_IDENTITY_ID>)) // Adapty SDK 3.x try await Adapty.setIntegrationIdentifier( key: "branch_id", value: <BRANCH_IDENTITY_ID> ) } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers // login and update attribution and identifier Branch.getAutoInstance(this) .setIdentity("YOUR_USER_ID") { referringParams, error -> referringParams?.let { data -> Adapty.updateAttribution(data, "branch") { error -> if (error != null) { //handle the error } } } } // logout Branch.getAutoInstance(context).logout() ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```javascript showLineNumbers import 'package:flutter_branch_sdk/flutter_branch_sdk.dart'; FlutterBranchSdk.setIdentity('YOUR_USER_ID'); ``` </TabItem> <TabItem value="unity" label="Unity (C#)" default> ```csharp showLineNumbers Branch.setIdentity("your user id"); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers import branch from 'react-native-branch'; branch.setIdentity('YOUR_USER_ID'); ``` </TabItem> </Tabs> 2. Sử dụng phương thức `.updateAttribution()` để lưu dữ liệu attribution. Nếu bạn chưa chỉ định Branch user ID ở bước trước, hãy truyền nó vào tham số `networkUserId` tại đây. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers class YourBranchImplementation { func initializeBranch() { // Pass the attribution you receive from the initializing method of Branch iOS SDK to Adapty. Branch.getInstance().initSession(launchOptions: launchOptions) { (data, error) in if let data { // Adapty SDK 4.x Adapty.updateAttribution(data, source: .branch) // Adapty SDK 3.x Adapty.updateAttribution(data, source: "branch") } } } } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers //everything is in the above snippet for Android ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```javascript showLineNumbers try { await Adapty().setIntegrationIdentifier( key: "branch_id", value: <BRANCH_IDENTITY_ID>, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="unity" label="Unity (C#)" default> ```csharp showLineNumbers using AdaptySDK; Branch.initSession(delegate(Dictionary<string, object> parameters, string error) { string attributionString = JsonUtility.ToJson(parameters); Adapty.UpdateAttribution( attributionString, "branch", (error) => { // handle the error }); }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers import { adapty, AttributionSource } from 'react-native-adapty'; import branch from 'react-native-branch'; branch.subscribe({ enComplete: ({ params, }) => { adapty.updateAttribution(params, "branch"); }, }); ``` </TabItem> </Tabs> ## Cấu trúc sự kiện \{#event-structure\} Adapty gửi các sự kiện đã chọn đến Branch theo cấu hình trong phần **Events names** trên [**trang Branch Integration**](https://app.adapty.io/integrations/branch). Mỗi sự kiện có cấu trúc như sau: ```json { "branch_key": "key_live_kaFuWw8WvY7n1ss7...", "name": "PURCHASE", "user_data": { "os": "iOS", "developer_identity": "user_12345", "country": "US", "ip": "192.168.100.1", "idfa": "00000000-0000-0000-0000-000000000000", "idfv": "00000000-0000-0000-0000-000000000000", "aaid": "00000000-0000-0000-0000-000000000000" }, "event_data": { "transaction_id": "GPA.3383-4699-1373-07113", "revenue": 9.99, "currency": "USD" }, "custom_data": { "vendor_product_id": "yearly.premium.6999", "original_transaction_id": "GPA.3383-4699-1373-07113", "store": "play_store", "environment": "production" } } ``` Trong đó: | Tham số | Kiểu | Mô tả | |:-------------------------------|:-------|:---------------------------------------------------------------------------------------------------------------------------------------------------| | `branch_key` | String | Branch Key của bạn. | | `name` | String | Tên sự kiện Branch (ánh xạ từ sự kiện Adapty, ví dụ: "PURCHASE"). | | `user_data` | Object | Thông tin người dùng. | | `user_data.os` | String | "Android" hoặc "iOS". | | `user_data.developer_identity` | String | Customer User ID của người dùng. | | `user_data.country` | String | Mã quốc gia dựa trên IP của người dùng. | | `user_data.ip` | String | Địa chỉ IP của người dùng. | | `user_data.idfa` | String | **Chỉ iOS**. ID cho Nhà quảng cáo. | | `user_data.idfv` | String | **Chỉ iOS**. ID cho Nhà cung cấp. | | `user_data.aaid` | String | **Chỉ Android**. Google Advertising ID. | | `event_data` | Object | Các chỉ số sự kiện tiêu chuẩn (chỉ xuất hiện với sự kiện PURCHASE và các sự kiện tương tự). | | `event_data.transaction_id` | String | Store Transaction ID. | | `event_data.revenue` | Float | Giá trị doanh thu. | | `event_data.currency` | String | Mã tiền tệ (ví dụ: "USD"). | | `custom_data` | Object | Thuộc tính chi tiết của sự kiện (chứa tất cả các [trường sự kiện](webhook-event-types-and-fields#for-most-event-types) hiện có). | --- # File: facebook-ads --- --- title: "Facebook Ads" description: "Tích hợp Facebook Ads với Adapty để tiếp thị gói đăng ký hiệu quả." --- Với tích hợp Facebook Ads, bạn có thể dễ dàng kiểm tra số liệu thống kê ứng dụng trong Meta Analytics. Adapty gửi sự kiện đến Meta Ads Manager, giúp bạn tạo các đối tượng tương tự dựa trên gói đăng ký để đạt hiệu quả cao hơn. Nhờ đó, bạn có thể xem chính xác quảng cáo của mình đang tạo ra bao nhiêu doanh thu từ gói đăng ký. Tích hợp giữa Adapty và Facebook Ads hoạt động như sau: Adapty gửi tất cả các sự kiện gói đăng ký được cấu hình trong tích hợp của bạn đến Facebook Ads. Tích hợp này hữu ích để đánh giá hiệu quả của các chiến dịch quảng cáo. ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với Facebook Ads \{#connect-adapty-to-facebook-ads\} Để tích hợp Facebook Ads và phân tích các chỉ số ứng dụng, bạn có thể thiết lập tích hợp với Meta Analytics. Bằng cách gửi sự kiện đến Meta Ads Manager, bạn có thể tạo đối tượng tương tự dựa trên các sự kiện gói đăng ký như gia hạn. Để cấu hình tích hợp này, hãy điều hướng đến [Integrations > Facebook Ads](https://app.adapty.io/integrations/facebookanalytics) trong Adapty Dashboard và cung cấp thông tin xác thực cần thiết. :::note Lưu ý rằng tích hợp Facebook Ads chỉ hoạt động trên iOS 14.5+ với người dùng đã cấp phép ATT. ::: <img src="/assets/shared/img/fd84ddf-CleanShot_2023-08-15_at_15.45.442x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 1. Để tìm App ID, mở trang ứng dụng của bạn trong [App Store Connect](https://appstoreconnect.apple.com/), vào trang **App Information** trong mục **General**, và tìm **Apple ID** ở góc dưới bên trái màn hình. 2. Bạn cần có ứng dụng trên nền tảng [Meta for Developers](https://developers.facebook.com/). Đăng nhập vào ứng dụng của bạn rồi vào phần cài đặt nâng cao. Bạn có thể tìm thấy **App ID** ở phần tiêu đề. <img src="/assets/shared/img/4b326c4-001563-August-23-4tO3JVso.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Tắt tính năng theo dõi phía client trong cấu hình Meta SDK để tránh tính doanh thu hai lần trong Meta Ads Manager. Bạn có thể tìm thấy cài đặt này trong Meta Developer Console tại **App Settings > Advanced Settings**. Đặt **Log in-app events automatically** thành "No". Điều này đảm bảo rằng sự kiện doanh thu chỉ được theo dõi thông qua tích hợp của Adapty. Để theo dõi sự kiện cài đặt và sử dụng, bạn cần kích hoạt Meta SDK trong mã nguồn của mình. Bạn có thể tìm thấy hướng dẫn triển khai trong tài liệu Meta SDK cho nền tảng của bạn: - [iOS SDK](https://developers.facebook.com/docs/ios/getting-started) - [Android SDK](https://developers.facebook.com/docs/android/getting-started) - [Unity SDK](https://developers.facebook.com/docs/unity/getting-started/canvas) <img src="/assets/shared/img/c4eb8eb-001565-August-23-483KKBbC.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bạn cũng có thể sử dụng tích hợp này với ứng dụng Android. Nếu bạn đã thiết lập cấu hình Android SDK trong **App Settings**, chỉ cần thiết lập **Facebook App ID** là đủ. ### Cấu hình sự kiện và thẻ \{#configure-events-and-tags\} Lưu ý rằng tích hợp Facebook Ads được thiết kế đặc biệt cho các công ty sử dụng Meta cho chiến dịch quảng cáo và tối ưu hóa dựa trên hành vi khách hàng. Tích hợp này hỗ trợ các sự kiện chuẩn của Meta cho mục đích tối ưu hóa. Do đó, không thể chỉnh sửa tên sự kiện trong tích hợp Meta Ads. Adapty ánh xạ các sự kiện khách hàng của bạn sang các sự kiện Meta tương ứng để phân tích chính xác. | Sự kiện Adapty | Sự kiện Meta Ads | | :---------------------------- | :-------------------------- | | Subscription initial purchase | Subscribe | | Subscription renewed | Subscribe | | Subscription cancelled | CancelSubscription | | Trial started | StartTrial | | Trial converted | Subscribe | | Trial cancelled | CancelTrial | | Non subscription purchase | fb_mobile_purchase | | Billing issue detected | billing_issue_detected | | Entered grace period | entered_grace_period | | Auto renew off | auto_renew_off | | Auto renew on | auto_renew_on | | Auto renew off subscription | auto_renew_off_subscription | | Auto renew on subscription | auto_renew_on_subscription | StartTrial, Subscribe, CancelSubscription là các sự kiện chuẩn. <img src="/assets/shared/img/8a5df9d-CleanShot_2023-07-04_at_12.47.312x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Để bật các sự kiện cụ thể, chỉ cần bật những sự kiện bạn cần. Nếu nhiều tên sự kiện được chọn, Adapty sẽ hợp nhất dữ liệu từ tất cả các sự kiện đã chọn thành một tên sự kiện Adapty duy nhất. ### Kết nối ứng dụng của bạn với Facebook Ads \{#connect-your-app-to-facebook-ads\} Nếu bạn thực hiện các bước trên, Facebook sẽ tự động nhận dữ liệu gói đăng ký từ Adapty. Theo những thay đổi về IDFA trong iOS 14.5, chúng tôi khuyến nghị bạn yêu cầu `facebookAnonymousId` của người dùng từ Facebook. Như vậy, nếu IDFA của người dùng không khả dụng, tích hợp vẫn sẽ tiếp tục hoạt động. Hãy làm theo <InlineTooltip tooltip="hướng dẫn cài đặt thuộc tính người dùng">[iOS](setting-user-attributes), [Android](android-setting-user-attributes), [React Native](react-native-setting-user-attributes), [Flutter](flutter-setting-user-attributes) và [Unity](unity-setting-user-attributes)</InlineTooltip> để thiết lập thông số này. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "facebook_anonymous_id", value: AppEvents.shared.anonymousID ) } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.setIntegrationIdentifier( "facebook_anonymous_id", AppEventsLogger.getAnonymousAppDeviceGUID(context) ) { error -> if (error != null) { // handle the error } } ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers try { const anonymousId = await AppEventsLogger.getAnonymousID(); await adapty.setIntegrationIdentifier("facebook_anonymous_id", anonymousId); } catch (error) { // handle `AdaptyError` } ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```text There is no official SDK for Flutter ``` </TabItem> <TabItem value="unity" label="Unity (C#)" default> ```csharp anonymousID is not available in the official SDK https://github.com/facebook/facebook-sdk-for-unity/issues/676 ``` </TabItem> </Tabs> ## Cấu trúc sự kiện \{#event-structure\} Adapty gửi sự kiện đến Facebook Ads (Meta) qua Graph API. Mỗi sự kiện có cấu trúc như sau: ```json { "event": "CUSTOM_APP_EVENTS", "app_user_id": "user_12345", "advertiser_id": "00000000-0000-0000-0000-000000000000", "advertiser_tracking_enabled": 1, "application_tracking_enabled": 1, "custom_events": "[{\"_eventName\":\"Subscribe\",\"_logTime\":1709294400,\"fb_num_items\":1,\"fb_content_type\":\"in_app\",\"fb_content_id\":\"yearly.premium.6999\",\"fb_currency\":\"USD\",\"fb_order_id\":\"GPA.3383...\",\"fb_transaction_id\":\"GPA.3383...\",\"_valueToSum\":9.99}]", "extinfo": "[\"i2\",\"com.example.app\",\"1.0.0\",\"100\",\"17.0.1\",\"iPhone14,3\",\"en_US\",\"GMT+3\",\"\",0,0,0,0,0,0,\"GMT+3\"]", "anon_id": "facebook_anon_id_123" } ``` Trong đó: | Thông số | Kiểu | Mô tả | |:---|:---|:---| | `event` | String | Luôn là "CUSTOM_APP_EVENTS". | | `app_user_id` | String | Customer User ID của người dùng. | | `advertiser_id` | String | IDFA (iOS) hoặc Advertising ID (Android). | | `advertiser_tracking_enabled` | Integer | `1` nếu bật theo dõi (ATT được cấp phép), `0` nếu không. | | `application_tracking_enabled` | Integer | Luôn là `1`. | | `custom_events` | String | Chuỗi JSON được mã hóa chứa các đối tượng sự kiện (xem bên dưới). | | `extinfo` | String | Chuỗi JSON được mã hóa chứa thông tin ứng dụng/thiết bị (ví dụ: phiên bản, hệ điều hành, ngôn ngữ). | | `anon_id` | String | Facebook Anonymous ID (nếu có). | Thông số `custom_events` là một mảng các đối tượng được mã hóa JSON, chứa: | Thông số | Kiểu | Mô tả | |:---|:---|:---| | `_eventName` | String | Tên sự kiện Meta Ads (ví dụ: "Subscribe"). | | `_logTime` | Long | Dấu thời gian của sự kiện tính bằng giây. | | `_valueToSum` | Float | Số tiền doanh thu. | | `fb_content_id` | String | Product ID từ cửa hàng. | | `fb_currency` | String | Mã tiền tệ (ví dụ: "USD"). | | `fb_order_id` | String | ID giao dịch gốc. | | `fb_transaction_id` | String | ID giao dịch gốc. | | `fb_content_type` | String | Luôn là "in_app". | | `fb_num_items` | Integer | Luôn là 1 cho sự kiện mua hàng. | --- # File: singular --- --- title: "Singular" description: "Tích hợp Singular với Adapty để phân tích dữ liệu marketing và gói đăng ký." --- [Singular](https://www.singular.net/) là một trong những nền tảng Mobile Measurement Partner (MMP) hàng đầu, thu thập và trình bày dữ liệu từ các chiến dịch marketing. Điều này giúp các công ty theo dõi hiệu quả chiến dịch của mình. Adapty cung cấp đầy đủ dữ liệu giúp bạn theo dõi [các sự kiện gói đăng ký](events) từ các cửa hàng tại một nơi. Với Adapty, bạn có thể dễ dàng nắm bắt hành vi của người dùng, hiểu họ thích gì, và sử dụng thông tin đó để giao tiếp với họ một cách có mục tiêu và hiệu quả. Vì vậy, tích hợp này cho phép bạn theo dõi các sự kiện gói đăng ký trong Singular và phân tích chính xác doanh thu mà các chiến dịch của bạn tạo ra. Adapty có thể gửi tất cả các sự kiện gói đăng ký được cấu hình trong tích hợp của bạn đến Singular. Kết quả là bạn có thể theo dõi những sự kiện này trong dashboard Singular. Tích hợp này rất hữu ích để đánh giá hiệu quả của các chiến dịch quảng cáo. ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với Singular \{#connect-adapty-to-singular\} Để thiết lập tích hợp với Singular, vào [Integrations > Singular](https://app.adapty.io/integrations/singular) trong Adapty Dashboard, bật toggle và điền thông tin vào các trường. Các thông tin xác thực sau đây có sẵn: - **Singular SDK Key**: Bắt buộc. SDK key production cho ứng dụng Singular của bạn. - **Singular SDK Key (Sandbox)**: Không bắt buộc. SDK key cho ứng dụng Singular sandbox. Nếu không được thiết lập, các sự kiện sandbox sẽ không được gửi đến Singular. Cả hai key đều có thể tìm thấy trong dashboard Singular tại **Developer tools -> SDK Keys -> SDK Key (**không phải** SDK Secret)**: <img src="/assets/shared/img/4bc50d1-singular_sdk_key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bên dưới phần thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi đến Singular từ Adapty. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/e67de0c-singular_events.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chúng tôi khuyến nghị sử dụng tên sự kiện mặc định do Adapty cung cấp. Tuy nhiên, bạn có thể thay đổi tên sự kiện theo nhu cầu của mình. Adapty sẽ gửi các sự kiện gói đăng ký đến Singular thông qua tích hợp server-to-server, cho phép bạn xem tất cả các sự kiện gói đăng ký trong dashboard Singular và liên kết chúng với các chiến dịch quảng cáo của bạn. :::warning Các hồ sơ người dùng được tạo trước khi cấu hình tích hợp sẽ không thể gửi sự kiện đến Singular. ::: ### Kết nối ứng dụng của bạn với Singular \{#connect-your-app-to-singular\} Tích hợp giữa Adapty và Singular là server-to-server. Do đó, bạn không cần thêm bất kỳ đoạn code nào vào ứng dụng của mình. ## Cấu trúc sự kiện \{#event-structure\} Adapty gửi sự kiện đến Singular thông qua GET request sử dụng query parameters. Mỗi sự kiện có cấu trúc như sau: ```json { "n": "subscription_renewed", "a": "singular_sdk_key_123", "p": "iOS", "i": "com.example.app", "ip": "192.168.100.1", "idfa": "00000000-0000-0000-0000-000000000000", "idfv": "00000000-0000-0000-0000-000000000000", "ve": "17.0.1", "att_authorization_status": 3, "custom_user_id": "user_12345", "utime": 1709294400, "amt": 9.99, "cur": "USD", "purchase_product_id": "yearly.premium.6999", "purchase_transaction_id": "GPA.3383...", "e": "{\"is_revenue_event\":true,\"amt\":9.99,\"cur\":\"USD\",\"purchase_product_id\":\"yearly.premium.6999\",\"purchase_transaction_id\":\"GPA.3383...\"}" } ``` Trong đó: | Tham số | Kiểu | Mô tả | |:---------------------------|:--------|:---------------------------------------------------------------| | `n` | String | Tên sự kiện (được ánh xạ từ sự kiện Adapty). | | `a` | String | Singular SDK Key của bạn. | | `p` | String | Nền tảng ("iOS" hoặc "Android"). | | `i` | String | Store App ID (Bundle ID). | | `ip` | String | Địa chỉ IP của người dùng. | | `idfa` | String | **Chỉ iOS**. ID for Advertisers (chữ hoa). | | `idfv` | String | **Chỉ iOS**. ID for Vendors (chữ hoa). | | `aifa` | String | **Chỉ Android**. Google Advertising ID (chữ thường). | | `andi` | String | **Chỉ Android**. Android ID (chữ thường). | | `asid` | String | **Chỉ Android**. App Set ID (chữ thường). | | `ve` | String | Phiên bản hệ điều hành. | | `att_authorization_status` | Integer | **Chỉ iOS**. Trạng thái ATT (ví dụ: `3` là đã được phép). | | `custom_user_id` | String | Customer User ID của người dùng. | | `utime` | Long | Timestamp UNIX của sự kiện tính bằng giây. | | `amt` | Float | Số tiền doanh thu. | | `cur` | String | Mã tiền tệ (ví dụ: "USD"). | | `purchase_product_id` | String | Product ID từ cửa hàng. | | `purchase_transaction_id` | String | Transaction ID gốc. | | `e` | String | Chuỗi JSON chứa thông tin chi tiết sự kiện (xem bên dưới). | Tham số `e` (dữ liệu sự kiện tùy chỉnh) là một chuỗi được mã hóa JSON chứa: | Tham số | Kiểu | Mô tả | |:--------------------------|:--------|:--------------------------------------------| | `is_revenue_event` | Boolean | `true` nếu sự kiện có chứa doanh thu. | | `amt` | Float | Số tiền doanh thu. | | `cur` | String | Mã tiền tệ. | | `purchase_product_id` | String | Product ID từ cửa hàng. | | `purchase_transaction_id` | String | Transaction ID gốc. | --- # File: tenjin --- --- title: "Tenjin integration" description: "" --- Tenjin là nền tảng attribution và phân tích dành cho nhà phát triển ứng dụng và marketer. Nền tảng này cung cấp các công cụ để đo lường và tối ưu hóa chiến dịch thu hút người dùng thông qua các thông tin chi tiết về hiệu suất ứng dụng và hành vi người dùng. Với cách tiếp cận minh bạch và linh hoạt, Tenjin tổng hợp dữ liệu từ các mạng quảng cáo và cửa hàng ứng dụng, giúp các nhóm phân tích ROI, theo dõi chuyển đổi và giám sát các chỉ số hiệu suất quan trọng. Bằng cách chuyển tiếp [các sự kiện gói đăng ký](events) tới Tenjin, bạn có thể thấy chính xác nguồn gốc của từng chuyển đổi và chiến dịch nào mang lại giá trị cao nhất trên tất cả các kênh, nền tảng và thiết bị. Về cơ bản, dashboard Tenjin cung cấp phân tích nâng cao cho các chiến dịch marketing. Bằng cách chuyển tiếp attribution từ Tenjin sang Adapty, bạn làm giàu thêm dữ liệu phân tích của Adapty với các tiêu chí lọc bổ sung để sử dụng trong phân tích cohort và chuyển đổi. Tích hợp này hoạt động theo hai hướng chính: 1. **Nhận dữ liệu attribution từ Tenjin** Sau khi tích hợp, Adapty thu thập dữ liệu attribution từ Tenjin. Bạn có thể xem thông tin này trên trang hồ sơ người dùng trong Adapty Dashboard. 2. **Gửi sự kiện gói đăng ký đến Tenjin** Adapty gửi các sự kiện mua hàng đến Tenjin theo thời gian thực. Các sự kiện này giúp đánh giá hiệu quả của các chiến dịch quảng cáo trực tiếp trong dashboard của Tenjin. | Đặc điểm tích hợp | Mô tả | | ------------------ | ------------------------------------------------------------ | | Lịch trình | Thời gian thực | | Chiều truyền dữ liệu | <p>Truyền hai chiều:</p><ul><li> **Sự kiện Adapty**: Từ máy chủ Adapty đến máy chủ Tenjin</li><li> **Attribution Tenjin**: Từ SDK Tenjin đến máy chủ Adapty</li></ul> | | Điểm tích hợp Adapty | <ul><li> SDK Tenjin và Adapty trong mã nguồn ứng dụng di động</li><li> Máy chủ Adapty</li></ul> | ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với Tenjin \{#connect-adapty-to-tenjin\} 1. Mở trang [**Integrations** -> **Tenjin**](https://app.adapty.io/integrations/tenjin) trong Adapty Dashboard. 2. Bật toggle để kích hoạt tích hợp. <img src="/assets/shared/img/tenjin-toggle.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Đăng nhập vào [Tenjin Dashboard](https://tenjin.com/). 4. Vào **Configuration** -> **Apps** trong menu điều hướng. <img src="/assets/shared/img/tenjin-apps.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Chọn ứng dụng cho nền tảng của bạn (iOS hoặc Android) và chuyển đến tab **App and SDK**. 6. Trong tab **App and SDK**, nhấn **Copy** ở cột **SDK Key**. Nếu bạn chưa có SDK key, nhấn nút **Generate SDK Key** để tạo một cái. <img src="/assets/shared/img/tenjin-copy-sdk-key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Quay lại Adapty Dashboard và dán SDK Key vừa sao chép vào trường tương ứng: - Với ứng dụng iOS: dán vào trường **iOS SDK Key** hoặc **iOS Sandbox SDK Key** - Với ứng dụng Android: dán vào trường **Android SDK Key** hoặc **Android Sandbox SDK Key** :::info Tenjin không có chế độ Sandbox riêng cho tích hợp server-to-server. Hãy dùng một ứng dụng Tenjin riêng biệt hoặc dùng cùng một key cho cả sự kiện production lẫn sandbox. ::: <img src="/assets/shared/img/tenjin-keys.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 8. Nếu bạn có ứng dụng trên cả hai nền tảng, lặp lại bước 5-7 cho nền tảng còn lại. 9. (Tùy chọn) Điều chỉnh phần **How the revenue data should be sent** nếu cần. Để biết giải thích chi tiết về các cài đặt, tham khảo [Integration settings](configuration#integration-settings). 10. Nhấn **Save** để hoàn tất thiết lập. Adapty sẽ bắt đầu gửi sự kiện mua hàng đến Tenjin và nhận dữ liệu attribution. Bạn có thể điều chỉnh việc chia sẻ sự kiện trong phần **Events names**. ### Cấu hình sự kiện và tag \{#configure-events-and-tags\} Tenjin chỉ chấp nhận các sự kiện mua hàng và **Trial started**. Trong phần **Events names**, chọn những sự kiện cần chia sẻ với Tenjin để phù hợp với mục tiêu theo dõi của bạn. <img src="/assets/shared/img/tenjin-events.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Kết nối ứng dụng với Tenjin \{#connect-your-app-to-tenjin\} Dùng phương thức SDK `Adapty.updateAttribution()` để lấy dữ liệu attribution từ Tenjin và truyền nó sang Adapty. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers func updateTenjinId() { guard let tenjinId = TenjinSDK.getAnalyticsInstallationId() else { return } do { // Adapty SDK 4.x try await Adapty.setIntegrationIdentifier(.tenjinAnalyticsInstallationId(tenjinId)) // Adapty SDK 3.x try await Adapty.setIntegrationIdentifier( key: "tenjin_analytics_installation_id", value: tenjinId ) } catch { // handle the error } } func updateTenjinAttribution() { let instance = TenjinSDK.getInstance("<YOUR_TENJIN_API_TOKEN>") instance?.getAttributionInfo { info, _ in guard let info else { return } Task { do { // Adapty SDK 4.x try await Adapty.updateAttribution(info, source: .tenjin) // Adapty SDK 3.x try await Adapty.updateAttribution(info, source: "tenjin") } catch { // handle the error } } } } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.setIntegrationIdentifier("tenjin_analytics_installation_id", tenjinSdk.analyticsInstallationId) { error -> if (error != null) { // handle the error } } tenjinSdk.getAttributionInfo { attribution -> if (attribution == null) return@getAttributionInfo Adapty.updateAttribution(attribution, "tenjin") { error -> if (error != null) { // handle the error } } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers Adapty.setIntegrationIdentifier("tenjin_analytics_installation_id", tenjinSdk.getAnalyticsInstallationId(), error -> { if (error != null) { // handle the error } }); tenjinSdk.getAttributionInfo(attribution -> { if (attribution == null) return; Adapty.updateAttribution(attribution, "tenjin", error -> { // handle the error }); }); ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```javascript showLineNumbers try { final tenjinId = await TenjinSDK.instance.getAnalyticsInstallationId(); if (tenjinId != null) { await Adapty().setIntegrationIdentifier( key: 'tenjin_analytics_installation_id', value: tenjinId, ); } final attribution = await TenjinSDK.instance.getAttributionInfo(); if (attribution != null) { await Adapty().updateAttribution(attribution, source: 'tenjin'); } } catch (e) { // handle the error } ``` </TabItem> <TabItem value="unity" label="Unity (C#)" default> ```csharp showLineNumbers using AdaptySDK; using System.Linq; BaseTenjin instance = Tenjin.getInstance("<SDK_KEY>"); var tenjinId = instance.GetAnalyticsInstallationId(); Adapty.SetIntegrationIdentifier( "tenjin_analytics_installation_id", tenjinId, (error) => { // handle the error }); instance.GetAttributionInfo((attribution) => { var dynamicAttribution = attribution.ToDictionary( kvp => kvp.Key, kvp => (dynamic)kvp.Value ); Adapty.UpdateAttribution( dynamicAttribution, "tenjin", (error) => { // handle the error }); }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers // ... const posthog = usePostHog() // ... try { await adapty.setIntegrationIdentifier("tenjin_analytics_installation_id", await Tenjin.getAnalyticsInstallationId()); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> ## Cấu trúc sự kiện \{#event-structure\} Adapty gửi các sự kiện đã chọn đến Tenjin theo cấu hình trong phần **Events names** trên [**trang Tenjin Integration**](https://app.adapty.io/integrations/tenjin). Mỗi sự kiện có cấu trúc như sau: ```json showLineNumbers title="Json" { "price": 99.0, "locale": "en-US", "country": "ME", "postcut": "false", "currency": "USD", "platform": "ios", "quantity": 1, "bundle_id": "com.adapty.adaptydemoapp", "ip_address": "127.0.0.1", "os_version": "18.1.1", "product_id": "month.premium.99", "app_version": "3.2.0", "sdk_version": "server", "device_model": "iPhone 13 Mini", "advertising_id": "00000000-0000-0000-0000-000000000000", "os_version_release": "18.1.1", "developer_device_id": "00000000-0000-0000-0000-000000000000", "analytics_installation_id": "00000000-0000-0000-0000-000000000000" } ``` Trong đó: | **Tham số** | **Kiểu dữ liệu** | **Mô tả** | | ----------------------------- | ---------------- | ------------------------------------------------------------ | | **price** | Float | Đơn giá của sản phẩm được mua theo đơn vị tiền tệ chuẩn (ví dụ: USD được tính bằng đô la). | | **locale** | String | Ngôn ngữ/vùng của thiết bị. Với Android: `Locale.getDefault().toString()`. Với iOS: `[[NSLocale currentLocale] localeIdentifier]`. | | **country** | String | Mã quốc gia theo tiêu chuẩn ISO (ví dụ: US cho Hoa Kỳ). | | **postcut** | String (Boolean) | Cho biết giao dịch mua có được gửi sau khi trừ phí nền tảng không. 1 là có, 0 là không. | | **currency** | String | Mã tiền tệ theo ISO (ví dụ: USD cho đô la Mỹ). | | **platform** | String | Nền tảng của thiết bị (ví dụ: ios, android, windows, amazon). | | **quantity** | Integer | Số lượng đơn vị được mua. | | **bundle_id** | String | Bundle identifier của ứng dụng (ví dụ: `com.example.app`). | | **ip_address** | String (IPv4) | Địa chỉ IP của người dùng. Dùng để tra cứu quốc gia. | | **os_version** | String | Phiên bản hệ điều hành của thiết bị. Với Android: `String.valueOf(Build.VERSION.SDK_INT)`. Với iOS: `[[UIDevice currentDevice] systemVersion]`. | | **product_id** | String | Mã định danh duy nhất của sản phẩm được mua. | | **app_version** | Float, Decimal | Phiên bản ứng dụng. Với Android: `context.getPackageManager().getPackageInfo()`. Với iOS: `[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]`. | | **sdk_version** | String | Phiên bản SDK đang sử dụng, luôn được đặt là `server`. | | **device_model** | String | Model của thiết bị. Với Android: `Build.MODEL`. Với iOS: `sysctl("hw.machine")`. | | **advertising_id** | UUID | ID quảng cáo của thiết bị. Bắt buộc với Android. Với iOS, có thể để trống hoặc toàn số 0. | | **os_version_release** | String | Phiên bản phát hành hệ điều hành. Với Android: `String.valueOf(Build.VERSION.RELEASE)`. Với iOS: `[[UIDevice currentDevice] systemVersion]`. | | **developer_device_id** | UUID | Mã định danh nhà cung cấp (chỉ dành cho iOS). | | **analytics_installation_id** | UUID | ID cài đặt analytics. Để biết thêm chi tiết, tham khảo tài liệu tại `https://docs.tenjin.com`. | --- # File: amplitude --- --- title: "Amplitude" description: "Tích hợp Amplitude với Adapty để hiểu rõ hơn về hành vi người dùng." --- [Amplitude](https://amplitude.com/) là một dịch vụ phân tích di động mạnh mẽ. Với Adapty, bạn có thể dễ dàng gửi sự kiện đến Amplitude, xem cách người dùng tương tác, và đưa ra các quyết định thông minh. Adapty cung cấp một bộ dữ liệu đầy đủ cho phép bạn theo dõi [các sự kiện gói đăng ký](events) từ các cửa hàng ở một nơi và gửi chúng đến tài khoản Amplitude của bạn. Điều này giúp bạn kết hợp hành vi người dùng với lịch sử thanh toán trong Amplitude, từ đó đưa ra các quyết định sản phẩm tốt hơn. ### Cách thiết lập tích hợp Amplitude \{#how-to-set-up-amplitude-integration\} Trong Adapty, bạn có thể thiết lập các flow riêng biệt cho **sự kiện production** và **sự kiện test** từ môi trường sandbox của Apple hoặc Stripe, hoặc tài khoản test của Google. - Đối với sự kiện production, nhập các API key **Production** từ dashboard Amplitude, với một API key riêng cho từng nền tảng: iOS, Android và Stripe. - Đối với sự kiện test, sử dụng các trường **Sandbox** khi cần. Để thiết lập tích hợp Amplitude: 1. Mở [**Integrations** -> **Amplitude**](https://app.adapty.io/integrations/amplitude) trong Adapty Dashboard của bạn. <img src="/assets/shared/img/3b50552-CleanShot_2023-08-15_at_16.47.102x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Bật **Amplitude integration** để kích hoạt. 3. Điền vào các trường tích hợp: | Trường | Mô tả | | ------------------------------------------ | ------------------------------------------------------------ | | **Amplitude iOS/ Android/ Stripe API key** | Nhập **API Key** của Amplitude cho iOS/ Android/ Stripe vào Adapty. Tìm thấy nó trong **Project settings** trên Amplitude. Để được hỗ trợ, xem [tài liệu Amplitude](https://amplitude.com/docs/apis/authentication). Bắt đầu với các key **Sandbox** để thử nghiệm, sau đó chuyển sang key **Production** sau khi thử nghiệm thành công. | <img src="/assets/shared/img/2297782-CleanShot_2023-08-15_at_16.53.512x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Các cài đặt tùy chọn để tùy chỉnh thêm: | Tham số | Mô tả | | --------------------------------------- | ------------------------------------------------------------ | | **How the revenue data should be sent** | Chọn gửi doanh thu gộp hay doanh thu sau thuế và hoa hồng. Xem [Hoa hồng cửa hàng và thuế](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue) để biết thêm chi tiết. | | **Exclude historical events** | Chọn để loại trừ các sự kiện trước khi cài đặt Adapty SDK, tránh dữ liệu bị trùng lặp. Ví dụ: nếu người dùng đăng ký vào ngày 10 tháng 1 nhưng cài đặt Adapty SDK vào ngày 6 tháng 3, Adapty chỉ gửi các sự kiện từ ngày 6 tháng 3 trở đi. | | **Send User Attributes** | Chọn tùy chọn này để gửi các thuộc tính người dùng như tùy chọn ngôn ngữ. | | **Always populate user_id** | Adapty tự động gửi `device_id` dưới dạng `amplitudeDeviceId`. Đối với `user_id`, cài đặt này xác định hành vi: <ul><li>**ON**: Gửi `profile_id` của Adapty nếu `amplitudeUserId` hoặc `customer_user_id` không có sẵn.</li><li>**OFF**: Để trống `user_id` nếu không có ID nào khả dụng.</li></ul> | 5. Chọn các sự kiện bạn muốn nhận và [ánh xạ tên của chúng](amplitude#events-and-tags). 6. Nhấn **Save** để xác nhận thay đổi. Sau khi nhấn **Save**, Adapty sẽ bắt đầu gửi sự kiện đến Amplitude. Ngoài các sự kiện, Adapty còn gửi [trạng thái gói đăng ký](subscription-status) và ID sản phẩm gói đăng ký đến [thuộc tính người dùng Amplitude](https://amplitude.com/docs/data/user-properties-and-events). ### Sự kiện và tags \{#events-and-tags\} Bên dưới phần thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi đến Amplitude từ Adapty. Chỉ cần bật những sự kiện bạn cần. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/da67694-CleanShot_2023-08-15_at_16.52.352x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chúng tôi khuyến nghị sử dụng tên sự kiện mặc định do Adapty cung cấp. Tuy nhiên, bạn có thể thay đổi tên sự kiện theo nhu cầu. Adapty sẽ gửi các sự kiện gói đăng ký đến Amplitude thông qua tích hợp server-to-server, cho phép bạn xem tất cả sự kiện gói đăng ký trong dashboard Amplitude của mình. ### Cấu hình SDK \{#sdk-configuration\} Sử dụng phương thức `setIntegrationIdentifier()` để thiết lập tham số `amplitude_device_id`. Đây là bước bắt buộc để thiết lập tích hợp. Nếu bạn có đăng ký người dùng, bạn cũng có thể truyền `amplitude_user_id`. :::note Các SDK bên thứ ba tạo user ID theo cách bất đồng bộ. ID có thể chưa sẵn sàng khi `Adapty.activate()` chạy. Nếu **Customer User ID** của bạn đến từ một trong các SDK này, hãy gọi `Adapty.activate()` mà không truyền ID đó. Khi ID đã có, gọi `setIntegrationIdentifier()`, sau đó gọi `identify()` với CUID. ::: <Tabs groupId="current-os" queryString> <TabItem value="Swift" label="iOS (Swift)" default> **Setting amplitudeDeviceId** ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "amplitude_device_id", value: Amplitude.instance().deviceId ) } catch { // handle the error } ``` **Setting amplitudeUserId** ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "amplitude_user_id", value: "YOUR_AMPLITUDE_USER_ID" ) } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> **Setting amplitudeDeviceId** ```kotlin showLineNumbers //for Amplitude maintenance SDK (obsolete) val amplitude = Amplitude.getInstance() val amplitudeDeviceId = amplitude.getDeviceId() val amplitudeUserId = amplitude.getUserId() //for actual Amplitude Kotlin SDK val amplitude = Amplitude( Configuration( apiKey = AMPLITUDE_API_KEY, context = applicationContext ) ) val amplitudeDeviceId = amplitude.store.deviceId // Adapty.setIntegrationIdentifier("amplitude_device_id", amplitudeDeviceId) { error -> if (error != null) { // handle the error } } ``` **Setting amplitudeUserId** ```kotlin showLineNumbers //for Amplitude maintenance SDK (obsolete) val amplitude = Amplitude.getInstance() val amplitudeDeviceId = amplitude.getDeviceId() val amplitudeUserId = amplitude.getUserId() //for actual Amplitude Kotlin SDK val amplitude = Amplitude( Configuration( apiKey = AMPLITUDE_API_KEY, context = applicationContext ) ) val amplitudeUserId = amplitude.store.userId // Adapty.setIntegrationIdentifier("amplitude_user_id", amplitudeUserId) { error -> if (error != null) { // handle the error } } ``` </TabItem> <TabItem value="Flutter" label="Flutter (Dart)" default> **Setting amplitudeDeviceId** ```javascript showLineNumbers final Amplitude amplitude = Amplitude.getInstance(instanceName: "YOUR_INSTANCE_NAME"); try { await Adapty().setIntegrationIdentifier( key: "amplitude_device_id", value: amplitude.getDeviceId(), ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` **Setting amplitudeUserId** ```javascript showLineNumbers final Amplitude amplitude = Amplitude.getInstance(instanceName: "YOUR_INSTANCE_NAME"); try { await Adapty().setIntegrationIdentifier( key: "amplitude_user_id", value: "YOUR_AMPLITUDE_USER_ID", ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="Unity" label="Unity (C#)" default> **Setting amplitudeDeviceId** ```csharp showLineNumbers using AdaptySDK; Adapty.SetIntegrationIdentifier( "amplitude_device_id", amplitude.getDeviceId(), (error) => { // handle the error }); ``` **Setting amplitudeUserId** ```csharp showLineNumbers using AdaptySDK; Adapty.SetIntegrationIdentifier( "amplitude_user_id", "YOUR_AMPLITUDE_USER_ID", (error) => { // handle the error }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> **Setting amplitudeDeviceId** ```typescript showLineNumbers try { await adapty.setIntegrationIdentifier("amplitude_device_id", deviceId); } catch (error) { // handle `AdaptyError` } ``` **Setting amplitudeUserId** ```typescript showLineNumbers try { await adapty.setIntegrationIdentifier("amplitude_user_id", userId); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> ## Cấu trúc sự kiện Amplitude \{#amplitude-event-structure\} Adapty gửi sự kiện đến Amplitude qua HTTP API v2. Mỗi sự kiện có cấu trúc như sau: ```json { "api_key": "your_amplitude_api_key", "events": [ { "partner_id": "adapty", "event_type": "subscription_renewed", "time": 1709294400000, "insert_id": "123e4567-e89b-12d3-a456-426614174000", "user_id": "user_12345", "device_id": "device_12345", "platform": "iOS", "os_name": "iOS", "productId": "yearly.premium.6999", "revenue": 9.99, "event_properties": { "vendor_product_id": "yearly.premium.6999", "original_transaction_id": "GPA.3383...", "currency": "USD", "environment": "Production", "store": "app_store" }, "user_properties": { "subscription_state": "subscribed", "subscription_product": "yearly.premium.6999" } } ] } ``` Trong đó: | Tham số | Kiểu | Mô tả | |:----------------------------|:-------|:---------------------------------------------------------------------| | `api_key` | String | API Key Amplitude của bạn. | | `events` | Array | Danh sách các đối tượng sự kiện (Adapty gửi từng cái một). | | `events[].partner_id` | String | Luôn là "adapty". | | `events[].event_type` | String | Tên sự kiện (được ánh xạ từ sự kiện Adapty). | | `events[].time` | Long | Dấu thời gian của sự kiện tính bằng mili giây. | | `events[].insert_id` | String | ID sự kiện duy nhất (UUID). | | `events[].user_id` | String | Amplitude User ID hoặc Customer User ID. | | `events[].device_id` | String | Amplitude Device ID. | | `events[].platform` | String | Nền tảng (ví dụ: "iOS", "Android"). | | `events[].os_name` | String | Tên hệ điều hành. | | `events[].productId` | String | ID sản phẩm từ cửa hàng. | | `events[].revenue` | Float | Số tiền doanh thu. | | `events[].event_properties` | Object | Thuộc tính sự kiện chi tiết (chứa tất cả [trường sự kiện](webhook-event-types-and-fields#for-most-event-types) hiện có). | | `events[].user_properties` | Object | Thuộc tính người dùng như trạng thái gói đăng ký. | --- # File: appmetrica --- --- title: "AppMetrica" description: "Tích hợp AppMetrica với Adapty để phân tích gói đăng ký chuyên sâu." --- [AppMetrica](https://appmetrica.yandex.com/about) là công cụ phân tích miễn phí giúp bạn theo dõi hành vi người dùng và phân tích hiệu suất ứng dụng di động theo thời gian thực. Bằng cách tích hợp AppMetrica với Adapty, bạn có thể hiểu sâu hơn về các chỉ số gói đăng ký và mức độ tương tác của người dùng. ## Cách thiết lập tích hợp AppMetrica \{#how-to-set-up-appmetrica-integration\} Thiết lập tích hợp AppMetrica gồm hai bước chính: 1. Cấu hình tích hợp trong Adapty Dashboard 2. Thiết lập tích hợp trong code của ứng dụng ### Cấu hình trên dashboard \{#dashboard-configuration\} Để thiết lập tích hợp AppMetrica: 1. Mở [danh sách ứng dụng AppMetrica](https://appmetrica.yandex.ru/application/list) 2. Chọn ứng dụng bạn muốn theo dõi 3. Vào **Settings > Main** và sao chép **Application ID** và **Post API key** <img src="/assets/shared/img/appmetrica.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Vào [Integrations > AppMetrica](https://app.adapty.io/integrations/appmetrica) trong Adapty Dashboard 5. Dán thông tin xác thực AppMetrica của bạn vào. <img src="/assets/shared/img/appmetrica_creds.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Sự kiện và thẻ \{#events-and-tags\} Adapty cho phép bạn gửi ba nhóm sự kiện tới AppMetrica. Bạn có thể bật các sự kiện cần theo dõi hiệu suất ứng dụng. Xem danh sách đầy đủ các sự kiện có sẵn trong [tài liệu về sự kiện](events). :::note AppMetrica đồng bộ sự kiện mỗi 4 giờ, vì vậy có thể có độ trễ trước khi sự kiện xuất hiện trên dashboard của bạn. ::: <img src="/assets/shared/img/6ed2d88-CleanShot_2023-08-18_at_14.59.042x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::tip Chúng tôi khuyến nghị sử dụng tên sự kiện mặc định của Adapty để đảm bảo tính nhất quán, nhưng bạn có thể tùy chỉnh chúng để phù hợp với thiết lập phân tích hiện có của mình. ::: ### Cài đặt doanh thu \{#revenue-settings\} Mặc định, Adapty gửi dữ liệu doanh thu dưới dạng thuộc tính trong các sự kiện, hiển thị trong báo cáo Events của AppMetrica. Bạn có thể cấu hình cách tính toán và hiển thị dữ liệu doanh thu này: - **Tính toán doanh thu**: Chọn cách tính giá trị doanh thu để phù hợp với nhu cầu báo cáo tài chính của bạn: - **Doanh thu gộp**: Hiển thị tổng doanh thu trước khi trừ bất kỳ khoản nào, hữu ích để theo dõi tổng số tiền khách hàng thanh toán - **Doanh thu sau khi trừ hoa hồng cửa hàng**: Hiển thị doanh thu sau khi đã trừ phí App Store/Play Store, giúp bạn theo dõi thu nhập thực tế - **Doanh thu sau khi trừ hoa hồng cửa hàng và thuế**: Hiển thị doanh thu ròng sau khi trừ cả phí cửa hàng và thuế áp dụng, cho bức tranh chính xác nhất về thu nhập của bạn - **Báo cáo theo tiền tệ của người dùng**: Khi bật, doanh số được báo cáo theo tiền tệ địa phương của người dùng, giúp phân tích doanh thu theo khu vực dễ dàng hơn. Khi tắt, tất cả doanh số được quy đổi sang USD để báo cáo nhất quán trên các thị trường khác nhau. - **Gửi sự kiện doanh thu**: Bật tùy chọn này để dữ liệu doanh thu xuất hiện không chỉ trong báo cáo Events mà còn trong báo cáo [In-app and ad revenue](https://appmetrica.yandex.com/docs/en/mobile-reports/revenue-report) của AppMetrica. Đảm bảo bạn không gửi doanh thu từ bất kỳ nguồn nào khác, vì điều này có thể dẫn đến trùng lặp dữ liệu. - **Loại trừ sự kiện lịch sử**: Khi bật, Adapty sẽ không gửi các sự kiện xảy ra trước khi người dùng cài đặt ứng dụng có tích hợp Adapty SDK. Điều này giúp tránh trùng lặp dữ liệu nếu bạn đã gửi sự kiện tới analytics trước khi tích hợp Adapty. <img src="/assets/shared/img/appmetrica_revenue.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Cấu hình SDK \{#sdk-configuration\} Để bật tích hợp AppMetrica trong ứng dụng, bạn cần thiết lập hai mã định danh: 1. `appmetrica_device_id`: Bắt buộc để tích hợp cơ bản 2. `appmetrica_profile_id`: Tùy chọn, nhưng được khuyến nghị nếu ứng dụng có chức năng đăng ký tài khoản người dùng Sử dụng phương thức `setIntegrationIdentifier()` để thiết lập các giá trị này. Cách triển khai cho từng nền tảng: :::note Các SDK bên thứ ba tạo user ID theo cách bất đồng bộ. ID có thể chưa sẵn sàng khi `Adapty.activate()` chạy. Nếu **Customer User ID** của bạn đến từ một trong các SDK này, hãy gọi `Adapty.activate()` mà không truyền ID đó. Khi ID đã có, gọi `setIntegrationIdentifier()`, sau đó gọi `identify()` với CUID. ::: <Tabs groupId="current-os" queryString> <TabItem value="Swift" label="iOS (Swift)" default> **Thiết lập appmetrica_device_id** ```swift showLineNumbers AppMetrica.requestStartupIdentifiers(on: nil) { ids, error in if let error { // handle AppMetrica error return } guard let deviceIDHash = ids?[.deviceIDHashKey] as? String else { // handle AppMetrica error return } Task { do { try await Adapty.setIntegrationIdentifier( key: "appmetrica_device_id", value: deviceIDHash ) } catch { // handle the error } } } ``` **Thiết lập appmetrica_profile_id** ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "appmetrica_profile_id", value: "YOUR_APPMETRICA_PROFILE_ID" ) } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> **Thiết lập appmetrica_device_id** ```kotlin showLineNumbers val startupParamsCallback = object: StartupParamsCallback { override fun onReceive(result: StartupParamsCallback.Result?) { val deviceIdHash = result?.deviceIdHash ?: return Adapty.setIntegrationIdentifier("appmetrica_device_id", deviceIdHash) { error -> if (error != null) { // handle the error } } } override fun onRequestError( reason: StartupParamsCallback.Reason, result: StartupParamsCallback.Result? ) { //handle the error } } AppMetrica.requestStartupParams(context, startupParamsCallback, listOf(StartupParamsCallback.APPMETRICA_DEVICE_ID_HASH)) ``` **Thiết lập appmetrica_profile_id** ```kotlin showLineNumbers val startupParamsCallback = object: StartupParamsCallback { override fun onReceive(result: StartupParamsCallback.Result?) { val deviceIdHash = result?.deviceIdHash ?: return Adapty.setIntegrationIdentifier("appmetrica_device_id", deviceIdHash) { error -> if (error != null) { // handle the error } } Adapty.setIntegrationIdentifier("appmetrica_profile_id", "YOUR_ADAPTY_CUSTOMER_USER_ID") { error -> if (error != null) { // handle the error } } } override fun onRequestError( reason: StartupParamsCallback.Reason, result: StartupParamsCallback.Result? ) { //handle the error } } AppMetrica.requestStartupParams(context, startupParamsCallback, listOf(StartupParamsCallback.APPMETRICA_DEVICE_ID_HASH)) ``` </TabItem> <TabItem value="Flutter" label="Flutter (Dart)" default> **Thiết lập appmetrica_device_id** ```javascript showLineNumbers final startupParams = await AppMetrica.requestStartupParams([AppMetricaStartupParams.deviceIdHashKey]); final deviceIdHash = startupParams.result?.deviceIdHash; if (deviceIdHash != null) { try { await Adapty().setIntegrationIdentifier( key: "appmetrica_device_id", value: deviceIdHash, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } } ``` **Thiết lập appmetrica_profile_id** ```javascript showLineNumbers try { await Adapty().setIntegrationIdentifier( key: "appmetrica_profile_id", value: "YOUR_APPMETRICA_PROFILE_ID", ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="Unity" label="Unity (C#)" default> **Thiết lập appmetrica_device_id** ```csharp showLineNumbers using AdaptySDK; using Io.AppMetrica; AppMetrica.RequestStartupParams( (result, errorReason) => { string deviceIdHash = result.DeviceIdHash; if (deviceIdHash != null) { Adapty.SetIntegrationIdentifier( "appmetrica_device_id", deviceIdHash, (error) => { // handle the error }); } }, new List<string>() { StartupParamsKey.AppMetricaDeviceIDHash } ); ``` **Thiết lập appmetrica_profile_id** ```csharp showLineNumbers Adapty.SetIntegrationIdentifier( "appmetrica_profile_id", "YOUR_APPMETRICA_PROFILE_ID", (error) => { // handle the error }); ``` </TabItem> <TabItem value="RN" label="React Native (TS)" default> **Thiết lập appmetrica_device_id** ```typescript showLineNumbers // ... const startupParamsCallback = async ( params?: StartupParams, reason?: StartupParamsReason ) => { const deviceIdHash = params?.deviceIdHash if (deviceIdHash) { try { await adapty.setIntegrationIdentifier("appmetrica_device_id", deviceIdHash); } catch (error) { // handle `AdaptyError` } } } AppMetrica.requestStartupParams(startupParamsCallback, [DEVICE_ID_HASH_KEY]) ``` **Thiết lập appmetrica_profile_id** ```typescript showLineNumbers try { await adapty.setIntegrationIdentifier("appmetrica_profile_id", 'YOUR_ADAPTY_CUSTOMER_USER_ID'); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> ## Cấu trúc sự kiện AppMetrica \{#appmetrica-event-structure\} Adapty gửi sự kiện tới AppMetrica qua các POST request với các tham số được truyền dưới dạng query parameter. Với mỗi sự kiện Adapty, AppMetrica nhận tới **hai request riêng biệt**: 1. **Sự kiện profile** (luôn được gửi): Chứa metadata của sự kiện 2. **Sự kiện doanh thu** (tùy chọn): Chứa dữ liệu doanh thu nếu tùy chọn "Gửi sự kiện doanh thu" được bật trong Adapty Dashboard ### Yêu cầu sự kiện profile \{#profile-event-request\} Gửi tới: `https://api.appmetrica.yandex.ru/logs/v1/import/events` Ví dụ URL với query parameter: ``` POST https://api.appmetrica.yandex.ru/logs/v1/import/events?post_api_key=your_key&application_id=your_app_id&event_name=subscription_renewed&event_timestamp=1709294400&event_json=%7B%22vendor_product_id%22%3A%22yearly.premium%22...%7D&os_name=ios&ios_ifa=00000000-0000-0000-0000-000000000000&ios_ifv=12345678-1234-1234-1234-123456789012&profile_id=user_12345&session_type=foreground ``` Query parameter: | Tham số | Kiểu | Mô tả | |:-----------------------|:-------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| | `post_api_key` | String | Post API Key AppMetrica của bạn. | | `application_id` | String | Application ID AppMetrica của bạn. | | `event_name` | String | Tên sự kiện (được ánh xạ từ sự kiện Adapty). | | `event_timestamp` | Long | Timestamp UNIX của sự kiện tính bằng giây. Bị giới hạn về 7 ngày gần nhất nếu cũ hơn. | | `event_json` | String | Chuỗi JSON được URL-encoded chứa tất cả các [trường sự kiện](webhook-event-types-and-fields#for-most-event-types) có sẵn. Chỉ bao gồm các trường không null. | | `os_name` | String | "ios" hoặc "android". | | `profile_id` | String | AppMetrica Profile ID (nếu đã thiết lập), nếu không thì Customer User ID (nếu có). | | `appmetrica_device_id` | String | AppMetrica Device ID Hash. Chỉ được gửi nếu `profile_id` không có sẵn. | | `session_type` | String | Luôn là "foreground". | | `ios_ifa` | String | **Chỉ iOS**. ID for Advertisers. | | `ios_ifv` | String | **Chỉ iOS**. ID for Vendors. | | `google_aid` | String | **Chỉ Android**. Google Advertising ID. | ### Yêu cầu sự kiện doanh thu (Tùy chọn) \{#revenue-event-request-optional\} Gửi tới: `https://api.appmetrica.yandex.ru/logs/v1/import/revenue` Request này chỉ được gửi khi tùy chọn "Gửi sự kiện doanh thu" được bật trong cài đặt tích hợp Adapty Dashboard của bạn. Ví dụ URL với query parameter: ``` POST https://api.appmetrica.yandex.ru/logs/v1/import/revenue?post_api_key=your_key&application_id=your_app_id&revenue_event_type=subscription_renewed&price=9.99¤cy=USD&product_id=yearly.premium&quantity=1&transaction_id=GPA.3383...&payload=%7B%22vendor_product_id%22%3A%22yearly.premium%22...%7D&os_name=ios&ios_ifa=00000000-0000-0000-0000-000000000000&profile_id=user_12345&session_type=foreground ``` Query parameter: | Tham số | Kiểu | Mô tả | |:---------------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `post_api_key` | String | Post API Key AppMetrica của bạn. | | `application_id` | String | Application ID AppMetrica của bạn. | | `revenue_event_type` | String | Loại sự kiện doanh thu (ví dụ: "subscription_renewed", "refund", "intro_started"). Xem [ánh xạ sự kiện AppMetrica](#revenue-event-type-mapping). | | `price` | Float | Giá trị doanh thu (dựa trên cài đặt tính toán doanh thu của bạn). | | `currency` | String | Mã tiền tệ (ví dụ: "USD"). | | `product_id` | String | Product ID từ cửa hàng. | | `quantity` | Integer | Luôn là 1. | | `transaction_id` | String | Store Transaction ID. | | `payload` | String | Chuỗi JSON được URL-encoded chứa thông tin chi tiết sự kiện. Tự động cắt bớt nếu vượt quá 30KB bằng cách xóa các trường tùy chọn theo thứ tự ưu tiên để giữ lại dữ liệu quan trọng nhất. | | `os_name` | String | "ios" hoặc "android". | | `profile_id` | String | AppMetrica Profile ID (nếu đã thiết lập), nếu không thì Customer User ID (nếu có). | | `appmetrica_device_id` | String | AppMetrica Device ID Hash. Chỉ được gửi nếu `profile_id` không có sẵn. | | `session_type` | String | Luôn là "foreground". | | `ios_ifa` | String | **Chỉ iOS**. ID for Advertisers. | | `ios_ifv` | String | **Chỉ iOS**. ID for Vendors. | | `google_aid` | String | **Chỉ Android**. Google Advertising ID. | --- # File: firebase-and-google-analytics --- --- title: "Firebase và Google Analytics" description: "Gửi các sự kiện gói đăng ký của Adapty đến Firebase và Google Analytics — hỗ trợ Audiences, Remote Config, Google Ads attribution và các công cụ Firebase khác." --- Adapty có thể chuyển các sự kiện gói đăng ký — mua hàng, gia hạn, hoàn tiền, bắt đầu dùng thử — đến Firebase và Google Analytics, giúp một lần tích hợp cung cấp dữ liệu cho cả hai nền tảng. :::warning Bạn cần có cả Firebase project lẫn Google Analytics property được liên kết — dù bạn chỉ dùng một trong hai. Firebase và Google Analytics là cùng một dữ liệu hiển thị trên hai giao diện khác nhau. ::: Sự kiện mua hàng và hoàn tiền được gửi kèm theo doanh thu, đơn vị tiền tệ và thông tin chi tiết về sản phẩm. Dữ liệu này đồng thời cung cấp cho các công cụ di động của Firebase (Audiences, Remote Config, v.v.) và báo cáo trong Google Analytics. Đây là tích hợp analytics, không phải công cụ attribution Google Ads — xem [Giới hạn](#limitations). ## Bạn có thể làm gì với tích hợp này \{#what-you-can-do-with-this-integration\} Adapty phân nhóm người dùng theo `subscription_state` (`subscribed`, `active_trial`, `never_subscribed`, v.v.) và chuyển tiếp các sự kiện vòng đời gói đăng ký đến Firebase và Google Analytics. - **Audiences**: Xây dựng đối tượng người đăng ký trong Firebase và Google Analytics cho các kênh bên ngoài — retargeting Google Ads, chiến dịch FCM, mô hình lookalike. - **Google Ads conversions** *(Google Analytics)*: Sử dụng `purchase` và `refund` làm mục tiêu chuyển đổi Google Ads. - **Firebase Remote Config**: Thay đổi giới hạn sử dụng, nội dung copy, hoặc feature flag mà không cần cập nhật app — áp điều kiện dựa trên trạng thái gói đăng ký. (Không nhầm với [Adapty Remote Config](customize-paywall-with-remote-config), dùng để cấu hình nội dung flow/paywall.) - **Cloud Messaging**: Gửi push notification đến người dùng đã hủy gói khi app đang đóng. - **Cross-device tracking** *(Google Analytics)*: Adapty gửi `customer_user_id` đến Google Analytics để Google Ads có thể theo dõi cùng một người trên nhiều thiết bị. - **Conversion funnels** *(Google Analytics)*: Xem người dùng đã làm gì trong app trước khi chuyển đổi hoặc rời bỏ. - **Predictions**: Dự đoán churn và chi tiêu dựa trên lịch sử mua hàng. - **A/B Testing**: Kiểm thử tính năng app trên các cohort người đăng ký — ví dụ, triển khai mẫu điều hướng mới cho người dùng đang dùng thử và đo độ dài phiên. (Để kiểm thử các biến thể paywall, hãy dùng [Adapty A/B Tests](ab-tests).) ## Cách tích hợp hoạt động \{#how-the-integration-works\} 1. Khi người dùng mở ứng dụng lần đầu, Firebase SDK tạo một định danh duy nhất cho lần cài đặt đó — gọi là **Firebase App Instance ID**. Firebase và Google Analytics dùng ID này để xác định lần cài đặt nào chịu trách nhiệm cho từng sự kiện. 2. Ứng dụng của bạn truyền Firebase App Instance ID cho SDK của Adapty. Adapty liên kết hồ sơ người dùng với lần cài đặt Firebase đó. 3. Khi người dùng thực hiện mua hàng, máy chủ của Adapty chuyển tiếp sự kiện đến Firebase kèm theo Firebase App Instance ID. Dữ liệu được trao đổi theo kiểu server-to-server, ngoài ứng dụng. 4. Firebase khớp giao dịch mua hàng với lần cài đặt tương ứng, giúp bạn xem các giao dịch mua hàng cùng với mọi hành động khác của người dùng trong ứng dụng. :::note Firebase App Instance ID là ID dành riêng cho từng thiết bị. Khi cùng một người dùng Adapty mở ứng dụng trên thiết bị khác, Firebase ID mới sẽ ghi đè lên ID trước đó. Hãy sử dụng [customer user ID](identifying-users) để duy trì danh tính người dùng trên nhiều thiết bị. ::: ### Giao dịch mua qua Stripe \{#stripe-purchases\} Giao dịch mua qua Stripe chỉ được ghi nhận vào Firebase nếu người dùng đã mở ứng dụng mobile của bạn **trước**. Firebase App Instance ID phải được thiết lập **trước khi** giao dịch Stripe được kích hoạt. Đối với các giao dịch qua App Store và Play Store, điều này xảy ra tự động. Chúng bắt nguồn từ bên trong ứng dụng mobile của bạn, cùng với lệnh gọi [`setIntegrationIdentifier`](#configure-your-app-code). Firebase ID đã có mặt khi giao dịch được kích hoạt. Giao dịch mua qua Stripe bắt nguồn từ ngoài ứng dụng, trên server của bạn. Ứng dụng mobile phải gọi `setIntegrationIdentifier` khi khởi động — trước khi bất kỳ giao dịch mua Stripe nào được thực hiện. Nếu không, Adapty sẽ không có ID để gắn vào, và giao dịch mua Stripe sẽ không bao giờ đến được Firebase. ### Giới hạn \{#limitations\} - **Không phải công cụ attribution cho Google Ads.** Tích hợp này gửi các sự kiện Adapty vào Firebase và Google Analytics để phân tích. Nó không attribution lượt cài đặt ứng dụng cho các chiến dịch Google Ads (UAC / Universal App Campaigns) hay phân tách traffic trả phí với organic. Để attribution lượt cài đặt, hãy dùng tính năng [Adapty Attribution](adapty-user-acquisition) tích hợp sẵn. - **Không có backfill dữ liệu lịch sử.** Adapty chỉ chuyển tiếp các sự kiện kể từ thời điểm bạn bật tích hợp — các giao dịch mua, gia hạn và hoàn tiền trong quá khứ sẽ không bao giờ đến Firebase. (Dữ liệu cũ lưu trong các bản xuất [S3](s3-exports) / [GCS](google-cloud-storage) của Adapty, nhưng việc nhập dữ liệu đó vào Firebase không thuộc phạm vi tích hợp này.) - **Người mua chỉ trên web không đến được Firebase.** Adapty chuyển tiếp các giao dịch mua đến Firebase thông qua Firebase App Instance ID do ứng dụng di động của bạn thiết lập. Những người mua chưa từng cài ứng dụng sẽ không có ID — giao dịch của họ sẽ không đến Firebase. Xem [Stripe purchases](#stripe-purchases) ở trên để biết thêm chi tiết, và cân nhắc dùng [tích hợp Firebase của FunnelFox](https://funnelfox.com/docs/integrations/subscription-management/adapty) hoặc một web data stream của Google Analytics để theo dõi trên web. - **Paddle purchases không được hỗ trợ.** Tích hợp này hiện chưa hỗ trợ Paddle. Các giao dịch Paddle vẫn nằm trong Adapty Analytics và không đến Firebase qua đường này. - **Stripe purchases kế thừa các giới hạn riêng của Stripe.** Xem [giới hạn tích hợp Stripe](stripe#current-limitations). ## Hướng dẫn cài đặt \{#setup-instructions\} ### Cấu hình Firebase \{#configure-firebase\} 1. Mở [Firebase Console](https://console.firebase.google.com/) và chọn hoặc tạo một dự án. Để giữ analytics production không bị ảnh hưởng bởi các sự kiện sandbox, hãy dùng một dự án Firebase riêng cho các bản build phát triển. 2. Liên kết dự án với một thuộc tính Google Analytics. Firebase sẽ nhắc bạn thực hiện điều này khi tạo dự án, hoặc bạn có thể thêm sau qua **Project settings** > **Integrations** > **Google Analytics**. 3. Trong **Project settings** > **General** > **Your apps**, thêm một mục cho mỗi nền tảng bạn phát hành (iOS / Android / Web). Đối với Stripe, thêm mục Web app — không có loại Stripe app riêng. Mỗi mục sẽ tạo ra một **Firebase App ID** duy nhất và một data stream tương ứng trong Google Analytics. Bạn sẽ dán ID này vào phần cài đặt tích hợp Firebase của Adapty trong quá trình thiết lập. ### Cấu hình Adapty \{#configure-adapty\} 1. Mở [**Integrations** > **Firebase**](https://app.adapty.io/integrations/firebase) trong Adapty Dashboard. 2. Bật toggle **Firebase integration**. 3. Nhập thông tin xác thực cho từng nền tảng bạn phân phối. Adapty cần cả **Firebase App ID** và **Google Analytics secret** cho mỗi nền tảng — mỗi giá trị sẽ khác nhau giữa iOS, Android và Stripe. | Adapty Dashboard | Google Analytics | Tìm ở đâu | | --- | --- | --- | | **Firebase App ID** | **App ID** | Firebase Console > **Project settings** > **General** > **Your apps** | | **Google Analytics secret** | **Measurement Protocol API secret** | Google Analytics > **Admin** > **Data streams** > **Measurement Protocol API secrets** > **Create** | 4. Cấu hình cách Adapty chuyển tiếp dữ liệu doanh thu và người dùng. Bốn tùy chọn này nằm chung một hàng trên dashboard: - Dropdown **Revenue definition**: Gross revenue, Proceeds after store commission, hoặc [Proceeds after store commission and taxes](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue). - Toggle **Send user properties**: Khi bật, các sự kiện sẽ bao gồm `subscription_state` và `subscription_product_id`. Để sử dụng chúng trong báo cáo hoặc đối tượng, xem [Sử dụng dữ liệu gói đăng ký trong báo cáo](#use-subscription-data-in-reports-and-audiences). - Toggle **Report user's currency**: Khi bật, Adapty chuyển đổi tiền tệ địa phương của mỗi giao dịch sang tiền tệ báo cáo của tài khoản bạn trước khi chuyển tiếp sang Google Analytics. - Toggle **Send trial price**: Các lần bắt đầu dùng thử rất có giá trị — hầu hết người dùng trả phí thường bắt đầu bằng một lần dùng thử. Nhưng tính năng tối ưu hóa đặt giá thầu của Google Ads chỉ coi các sự kiện có doanh thu mới đáng theo đuổi. Bật tính năng này để gán một mức giá tạm thời cho mỗi lần dùng thử để Google coi chúng là lượt chuyển đổi và tối ưu hóa chi tiêu quảng cáo hướng đến việc thu hút người dùng bắt đầu dùng thử. Khi bật, trường **Trial price percentage** sẽ xuất hiện. Đặt giá trị này là tỷ lệ phần trăm của giá gói đăng ký đầy đủ mà Google nên coi mỗi lần dùng thử có giá trị — ví dụ: `50%` sẽ báo cáo nửa giá gói đăng ký trong thời gian dùng thử. 5. Map các sự kiện Adapty với tên sự kiện Firebase/Google Analytics. Adapty cung cấp bảng ánh xạ sự kiện riêng cho **iOS** và **Android** để bạn có thể dùng tên khác nhau theo từng nền tảng. **Các giao dịch mua qua Stripe sử dụng bảng ánh xạ sự kiện iOS** — không có bảng ánh xạ riêng cho Stripe. Google Analytics áp dụng các giới hạn nghiêm ngặt về Measurement Protocol — tên sự kiện tối đa 40 ký tự, tên thuộc tính người dùng tối đa 24 ký tự, và giá trị tối đa 36 ký tự. Google Analytics sẽ âm thầm bỏ qua các sự kiện tùy chỉnh vượt quá các giới hạn này. :::warning Một số sự kiện sử dụng từ vựng ecommerce được đặt trước trong Firebase và Google Analytics — `purchase` và `refund`. Tính năng nhập chuyển đổi Google Ads, báo cáo doanh thu Google Analytics, và đối tượng dự đoán đều phụ thuộc vào chính xác các chuỗi này. Chỉ ghi đè giá trị mặc định nếu bạn không cần những tính năng đó. ::: 6. Nhấp **Save**. Adapty sẽ bắt đầu chuyển tiếp các sự kiện đến Firebase trong vài phút. ### Cấu hình mã ứng dụng của bạn \{#configure-your-app-code\} :::tip Hãy đảm bảo ứng dụng của bạn đã tích hợp <InlineTooltip tooltip="Firebase SDK">[iOS](https://firebase.google.com/docs/ios/setup), [Android](https://firebase.google.com/docs/android/setup), [Flutter](https://firebase.google.com/docs/flutter/setup), [Unity](https://firebase.google.com/docs/unity/setup), [React Native](https://rnfirebase.io/), và [Capacitor](https://github.com/capawesome-team/capacitor-firebase)</InlineTooltip>. ::: Adapty cần gửi kèm **Firebase App Instance ID** với mỗi sự kiện — nếu không, không có gì được gửi đến Firebase (`MISSING_INTEGRATION_ID`). Sau khi gọi `FirebaseApp.configure()` và `Adapty.activate()`, hãy yêu cầu Firebase SDK cung cấp App Instance ID. Truyền nó vào Adapty qua `setIntegrationIdentifier`. Chạy đoạn code này một lần mỗi lần khởi động app, trước bất kỳ luồng mua hàng nào. :::note Các SDK bên thứ ba tạo user ID theo cách bất đồng bộ. ID có thể chưa sẵn sàng khi `Adapty.activate()` chạy. Nếu **Customer User ID** của bạn đến từ một trong các SDK này, hãy gọi `Adapty.activate()` mà không truyền ID đó. Khi ID đã có, gọi `setIntegrationIdentifier()`, sau đó gọi `identify()` với CUID. ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> <Tabs groupId="sdk-version" queryString> <TabItem value="v4" label="Adapty SDK v4+"> ```swift showLineNumbers FirebaseApp.configure() if let appInstanceId = Analytics.appInstanceID() { do { try await Adapty.setIntegrationIdentifier(.firebaseAppInstanceId(appInstanceId)) } catch { // handle the error } } ``` </TabItem> <TabItem value="v3" label="Adapty SDK v3" default> ```swift showLineNumbers FirebaseApp.configure() if let appInstanceId = Analytics.appInstanceID() { do { try await Adapty.setIntegrationIdentifier( key: "firebase_app_instance_id", value: appInstanceId ) } catch { // handle the error } } ``` </TabItem> </Tabs> </TabItem> <TabItem value="kotlin" label="Android (Kotlin)"> ```kotlin showLineNumbers // after Adapty.activate() FirebaseAnalytics.getInstance(context).appInstanceId.addOnSuccessListener { appInstanceId -> Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId) { error -> if (error != null) { // handle the error } } } ``` </TabItem> <TabItem value="java" label="Android (Java)"> ```java showLineNumbers // after Adapty.activate() FirebaseAnalytics.getInstance(context).getAppInstanceId().addOnSuccessListener(appInstanceId -> { Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId, error -> { if (error != null) { // handle the error } }); }); ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)"> ```dart showLineNumbers final appInstanceId = await FirebaseAnalytics.instance.appInstanceId; if (appInstanceId != null) { try { await Adapty().setIntegrationIdentifier( key: "firebase_app_instance_id", value: appInstanceId, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } } ``` </TabItem> <TabItem value="unity" label="Unity (C#)"> ```csharp showLineNumbers using AdaptySDK; using Firebase.Analytics; FirebaseAnalytics .GetAnalyticsInstanceIdAsync() .ContinueWithOnMainThread((task) => { if (!task.IsCompletedSuccessfully) { // handle the error return; } Adapty.SetIntegrationIdentifier( "firebase_app_instance_id", task.Result, (error) => { // handle the error } ); }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)"> ```typescript showLineNumbers try { const appInstanceId = await analytics().getAppInstanceId(); if (appInstanceId) { await adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId); } } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> ### Xác minh tích hợp \{#verify-the-integration\} Cách nhanh nhất để xác nhận các sự kiện đang được gửi đi là Firebase DebugView: 1. Trên thiết bị thử nghiệm, chạy ứng dụng với [chế độ debug của Firebase được bật](https://firebase.google.com/docs/analytics/debugview#enable_debug_mode). 2. Kích hoạt một giao dịch mua sandbox hoặc bất kỳ sự kiện nào bạn đã bật trong Adapty Dashboard. 3. Mở Firebase Console > **Analytics** > **DebugView**. Các sự kiện sẽ xuất hiện trong vài giây, kèm theo đầy đủ các tham số. Các báo cáo thông thường — Realtime, Reports, audiences — sẽ được cập nhật trong vài phút đến 24 giờ, tùy thuộc vào loại báo cáo. DebugView là nơi duy nhất để xác nhận theo thời gian thực. ## Sử dụng dữ liệu gói đăng ký trong báo cáo và đối tượng \{#use-subscription-data-in-reports-and-audiences\} Bật **Send user properties** trong Adapty Dashboard ([Cấu hình Adapty](#configure-adapty), bước 4). Nếu không bật, Adapty sẽ không gửi `subscription_state` hay `subscription_product_id` — và phần còn lại của mục này sẽ không có tác dụng gì. Theo mặc định, Firebase và Google Analytics không hiển thị các thuộc tính người dùng. Hãy đăng ký từng thuộc tính dưới dạng custom dimension. Điều này giúp `subscription_state` và `subscription_product_id` xuất hiện trong các báo cáo, Explorations và đối tượng. Cấu hình dimension trong Google Analytics Admin. Sau đó bạn có thể truy vấn chúng trong cả Firebase lẫn Google Analytics — vì hai nền tảng này dùng chung backend. Hữu ích cho việc xây dựng đối tượng Google Ads từ người dùng trả phí hoặc đưa dữ liệu vào các mô hình dự đoán. Khi hoàn tất thiết lập, Adapty sẽ điền các thuộc tính này cho các sự kiện tiếp theo. Các sự kiện cũ sẽ không được cập nhật. 1. Trong Google Analytics, mở **Admin** > **Custom definitions**. 2. Nhấp **Create custom dimensions**. 3. Với mỗi thuộc tính, thiết lập: - **Dimension name**: Bất kỳ tên nào dễ đọc, ví dụ "Subscription state". - **Scope**: **User**. - **User property**: `subscription_state` hoặc `subscription_product_id`. Tên phải khớp chính xác — Google Analytics có phân biệt chữ hoa/thường. ## Khắc phục sự cố \{#troubleshooting\} ### Sự kiện không xuất hiện trong Firebase \{#events-dont-appear-in-firebase\} - Xác nhận rằng Firebase App Instance ID đã được thiết lập **trước** khi giao dịch mua đầu tiên được thực hiện. Các sự kiện không có Firebase ID sẽ không đến được Firebase và sẽ gây ra lỗi. - Xác nhận rằng thuộc tính Google Analytics được liên kết trong Firebase Console khớp với data stream. - Xác nhận rằng bộ thông tin xác thực (ID + secret) trong Adapty khớp với nền tảng. ### `access_level_updated` hiển thị là thất bại trong Event Feed \{#access_level_updated-shows-as-failed-in-the-event-feed\} `access_level_updated` là **sự kiện chỉ dành cho webhook**. Adapty không bao giờ cố gắng gửi nó tới Firebase — nhưng Event Feed vẫn liệt kê nó là giao nhận thất bại. Hãy bỏ qua dòng đó. Tích hợp của bạn vẫn hoạt động bình thường. Để sử dụng sự kiện này, hãy cấu hình [tích hợp webhook](webhook). ### Các sự kiện sandbox làm ô nhiễm dữ liệu production \{#sandbox-events-pollute-production-data\} Adapty chuyển tiếp các giao dịch sandbox và production đến cùng một Firebase project. Xem [Cấu hình Firebase](#configure-firebase) — sử dụng một Firebase project riêng cho các bản build development sẽ tránh hoàn toàn vấn đề này. ### Firebase đếm thiếu doanh thu cho các ứng dụng dùng StoreKit 2 \{#firebase-undercounts-revenue-for-storekit-2-apps\} Firebase tự động ghi lại sự kiện `in_app_purchase` cho mọi giao dịch mua qua StoreKit 1 — không cần viết code. StoreKit 2 dùng API khác. Firebase không bao giờ nhìn thấy các giao dịch đó. Hệ quả: các ứng dụng chủ yếu dùng SK2 mà không có pipeline doanh thu riêng sẽ báo cáo thiếu một nửa hoặc hơn — trong Firebase, trong Google Analytics, và trong mọi chiến dịch Google Ads phía sau. Tối ưu hóa đặt giá thầu chạy dựa trên con số sai. Báo cáo doanh thu chỉ phản ánh một nửa thực tế. Cách khắc phục: truyền `firebase_app_instance_id` vào Adapty (xem [Cấu hình mã ứng dụng của bạn](#configure-your-app-code)). Adapty sẽ chuyển tiếp mọi giao dịch mua qua Measurement Protocol — bao gồm doanh thu, đơn vị tiền tệ và sản phẩm đi kèm. ### Số liệu Adapty Analytics và Firebase không khớp \{#adapty-analytics-and-firebase-numbers-diverge\} - **StoreKit 2**: Đây là nguyên nhân lớn nhất. Xem [Firebase thống kê thiếu doanh thu cho ứng dụng StoreKit 2](#firebase-undercounts-revenue-for-storekit-2-apps). - **Mức độ áp dụng SDK**: Firebase chỉ đếm các sự kiện từ người dùng có ứng dụng gửi Firebase App Instance ID. Các phiên bản ứng dụng cũ hơn không thực hiện lệnh gọi đó. Adapty vẫn đếm những người dùng đó; còn Firebase thì không. - **Sự kiện sandbox**: Adapty cũng chuyển tiếp các giao dịch sandbox sang Firebase. Hãy dùng một dự án Firebase riêng cho các bản build phát triển để tách biệt chúng. - **Lấy mẫu**: Google Analytics Explorations lấy mẫu các tập dữ liệu lớn. Để có số liệu chưa qua lấy mẫu, hãy kiểm tra chế độ xem Realtime hoặc các báo cáo tiêu chuẩn. ### Tên sự kiện tùy chỉnh bị Google Analytics từ chối \{#custom-event-names-are-rejected-by-google-analytics\} Google Analytics giới hạn tên sự kiện tối đa 40 ký tự, chỉ bao gồm chữ cái, số và dấu gạch dưới, và phải bắt đầu bằng một chữ cái. Hãy đổi tên các sự kiện Adapty tùy chỉnh trên dashboard nếu chúng vi phạm những giới hạn này. --- # File: mixpanel --- --- title: "Mixpanel" description: "Kết nối Mixpanel với Adapty để phân tích gói đăng ký mạnh mẽ hơn." --- [Mixpanel](https://mixpanel.com/home/) là một dịch vụ phân tích sản phẩm mạnh mẽ. Giải pháp theo dõi dựa trên sự kiện của nó giúp các nhóm sản phẩm có được những hiểu biết sâu sắc về chiến lược thu hút người dùng, chuyển đổi và giữ chân người dùng tối ưu trên các nền tảng khác nhau. Tích hợp này cho phép bạn đưa tất cả các sự kiện Adapty vào Mixpanel. Nhờ đó, bạn sẽ có cái nhìn toàn diện hơn về hoạt động kinh doanh gói đăng ký và hành động của khách hàng. Adapty cung cấp bộ dữ liệu đầy đủ giúp bạn theo dõi [các sự kiện gói đăng ký](events) từ các cửa hàng ở một nơi. Với Adapty, bạn có thể dễ dàng thấy người dùng của mình đang hành xử như thế nào, tìm hiểu những gì họ thích và sử dụng thông tin đó để giao tiếp với họ theo cách có mục tiêu và hiệu quả. ## Cách thiết lập tích hợp Mixpanel \{#how-to-set-up-mixpanel-integration\} 1. Mở trang [Integrations -> Mixpanel](https://app.adapty.io/integrations/mixpanel) trong Adapty Dashboard. 2. Bật toggle và nhập **Mixpanel Token** của bạn. Bạn có thể chỉ định một token cho tất cả các nền tảng hoặc giới hạn cho các nền tảng cụ thể nếu bạn chỉ muốn nhận dữ liệu từ một số nền tảng nhất định. 3. Đặt **Mixpanel Data Residency** để khớp với dự án Mixpanel của bạn. Trường này bắt buộc và mặc định là **US**. Chọn **US** cho endpoint `api.mixpanel.com` hoặc **Europe** cho `api-eu.mixpanel.com`. :::warning Nếu dự án Mixpanel của bạn sử dụng data residency tại EU, bạn phải đặt **Mixpanel Data Residency** thành **Europe**. Mixpanel sẽ từ chối các sự kiện được gửi đến endpoint US từ các dự án EU. ::: <img src="/assets/shared/img/mixpanel.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Tìm Mixpanel Token của bạn \{#finding-your-mixpanel-token\} Để lấy **Mixpanel Token** của bạn: 1. Đăng nhập vào [Mixpanel Dashboard](https://mixpanel.com/settings/project/) của bạn. 2. Mở **Settings** và chọn **Organization Settings**. <img src="/assets/shared/img/mixpanel-settings.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Từ thanh bên trái, đi đến **Projects** và chọn dự án của bạn. <img src="/assets/shared/img/mixpanel-project-id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Cách tích hợp hoạt động \{#how-the-integration-works\} Adapty tự động ánh xạ các thuộc tính sự kiện liên quan—như ID người dùng và doanh thu—sang [các thuộc tính gốc của Mixpanel](https://docs.mixpanel.com/docs/data-structure/user-profiles). Điều này đảm bảo việc theo dõi và báo cáo chính xác các sự kiện liên quan đến gói đăng ký. Ngoài ra, Adapty tích lũy dữ liệu doanh thu theo từng người dùng và cập nhật [User Profile Properties](https://docs.mixpanel.com/docs/data-structure/user-profiles) của họ, bao gồm `subscription state` và `subscription product ID`. Sau khi nhận được một sự kiện, Mixpanel sẽ cập nhật các trường tương ứng theo thời gian thực. ## Sự kiện và thẻ \{#events-and-tags\} Bên dưới phần thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi đến Mixpanel từ Adapty. Chỉ cần bật những sự kiện bạn cần. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/mixpanel-events.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chúng tôi khuyến nghị sử dụng tên sự kiện mặc định do Adapty cung cấp. Tuy nhiên, bạn có thể thay đổi tên sự kiện theo nhu cầu của mình. ## Cấu hình SDK \{#sdk-configuration\} Sử dụng phương thức `.setIntegrationIdentifier()` để đặt `mixpanelUserId`. Nếu không đặt, Adapty sẽ sử dụng ID người dùng của bạn (`customerUserId`) hoặc nếu giá trị đó là null thì dùng Adapty ID. Hãy đảm bảo rằng ID người dùng bạn dùng để gửi dữ liệu đến Mixpanel từ ứng dụng của mình trùng với ID bạn gửi đến Adapty. :::note Các SDK bên thứ ba tạo user ID theo cách bất đồng bộ. ID có thể chưa sẵn sàng khi `Adapty.activate()` chạy. Nếu **Customer User ID** của bạn đến từ một trong các SDK này, hãy gọi `Adapty.activate()` mà không truyền ID đó. Khi ID đã có, gọi `setIntegrationIdentifier()`, sau đó gọi `identify()` với CUID. ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "mixpanel_user_id", value: Mixpanel.mainInstance().distinctId ) } catch { // handle the error } ``` </TabItem> <TabItem value="swift-callback" label="iOS (Swift-Callback)" default> ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(mixpanelUserId: Mixpanel.mainInstance().distinctId) Adapty.updateProfile(params: builder.build()) ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelAPI.distinctId) { error -> if (error != null) { // handle the error } } ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```javascript showLineNumbers final mixpanel = await Mixpanel.init("Your Token", trackAutomaticEvents: true); final distinctId = await mixpanel.getDistinctId(); try { await Adapty().setIntegrationIdentifier( key: "mixpanel_user_id", value: distinctId, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="unity" label="Unity (C#)" default> ```csharp showLineNumbers using AdaptySDK; var distinctId = Mixpanel.DistinctId; if (distinctId != null) { Adapty.SetIntegrationIdentifier( "mixpanel_user_id", distinctId, (error) => { // handle the error }); } ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers // If you already have a shared Mixpanel instance in your app, use that instance instead. const trackAutomaticEvents = true; const mixpanel = new Mixpanel('YOUR_PROJECT_TOKEN', trackAutomaticEvents); await mixpanel.init(); // This is Mixpanel's current distinct_id (auto-generated, or set via mixpanel.identify(...)) const mixpanelUserId = await mixpanel.getDistinctId(); try { await adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelUserId); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> ## Cấu trúc sự kiện Mixpanel \{#mixpanel-event-structure\} Adapty gửi sự kiện đến Mixpanel bằng phương thức `track`. Các thuộc tính sự kiện được cấu trúc như sau: ```json { "event": "subscription_renewed", "properties": { "ip": 0, "time": 1709294400, "$insert_id": "123e4567-e89b-12d3-a456-426614174000", "vendor_product_id": "yearly.premium.6999", "original_transaction_id": "GPA.3383...", "currency": "USD", "environment": "Production", "store": "app_store", "purchase_date": "2024-03-01T12:00:00.000000+0000" } } ``` Trong đó: | Tham số | Kiểu | Mô tả | |:-------------------------------------|:--------|:---------------------------------------------------------------| | `event` | String | Tên sự kiện (được ánh xạ từ sự kiện Adapty). | | `properties` | Object | Thuộc tính sự kiện. | | `properties.ip` | Integer | Địa chỉ IP (gửi là 0 khi dùng server-to-server). | | `properties.time` | Long | Timestamp UNIX của sự kiện tính bằng giây. | | `properties.$insert_id` | String | ID sự kiện duy nhất (UUID) để loại trùng. | | `properties.vendor_product_id` | String | ID sản phẩm từ cửa hàng. | | `properties.original_transaction_id` | String | ID giao dịch gốc. | | `properties.currency` | String | Mã tiền tệ. | | `properties.store` | String | Tên cửa hàng (ví dụ: "app_store"). | | `properties.environment` | String | Môi trường ("Sandbox" hoặc "Production"). | ### Cập nhật hồ sơ người dùng \{#user-profile-updates\} Adapty cũng cập nhật User Profile của Mixpanel bằng `people_set` với các thuộc tính sau: | Tham số | Kiểu | Mô tả | |:--------------------------|:-------|:-------------------------------------------------------------------| | `subscription_state` | String | Trạng thái gói đăng ký hiện tại (ví dụ: "subscribed"). | | `subscription_product_id` | String | ID của sản phẩm gói đăng ký đang hoạt động. | --- # File: posthog --- --- title: "PostHog" description: "" --- PostHog là một nền tảng phân tích cung cấp các công cụ để theo dõi hành vi người dùng, trực quan hóa mức độ sử dụng sản phẩm và phân tích khả năng giữ chân người dùng. Với các tính năng như theo dõi sự kiện, user flows và feature flags, nền tảng này được thiết kế để giúp bạn hiểu rõ hơn và cải thiện sản phẩm của mình. Tích hợp PostHog với Adapty giúp theo dõi liền mạch các sự kiện liên quan đến gói đăng ký, chẳng hạn như bắt đầu dùng thử, gia hạn và hủy đăng ký. Bằng cách gửi các sự kiện này tới PostHog, bạn có thể phân tích cách các thay đổi gói đăng ký ảnh hưởng đến hành vi người dùng, đánh giá hiệu suất paywall và có được cái nhìn sâu hơn về các chiến lược kiếm tiền — tất cả trong quy trình phân tích hiện có của bạn. ## Đặc điểm tích hợp \{#integration-characteristics\} | Đặc điểm tích hợp | Mô tả | | ------------------ | ------------------------------------------------------------ | | Lịch trình | Thời gian thực; các sự kiện có thể không xuất hiện ngay lập tức trên dashboard PostHog. | | Chiều dữ liệu | Các sự kiện Adapty được gửi từ máy chủ Adapty tới máy chủ PostHog. | | Điểm tích hợp Adapty | <ul><li> SDK PostHog và Adapty trong code ứng dụng di động</li><li> Máy chủ Adapty</li></ul> | ## Cấu trúc sự kiện PostHog \{#posthog-event-structure\} Adapty gửi các sự kiện đã chọn tới PostHog như được cấu hình trong phần **Events names** trên [trang tích hợp PostHog](https://app.adapty.io/integrations/posthog). Mỗi sự kiện có cấu trúc như sau: ```json showLineNumbers { "distinct_id": "john.doe@example.com", "timestamp": "2025-01-08T11:06:12+00:00", "event": "subscription_started", "properties": { "$set": { "email": "user@example.com", "first_name": "John", "last_name": "Doe", "birthday": "1990-01-01", "gender": "male", "os": "iOS" }, "timezone": "America/New_York", "ip_address": "10.168.1.1", "*": "{{other_event_properties}}" } } ``` Trong đó | **Tham số** | **Kiểu** | **Mô tả** | | --------------- | -------------------- | ------------------------------------------------------------ | | **distinct_id** | String | Mã định danh duy nhất của người dùng (ví dụ: `profile.posthog_distinct_user_id`, `customer_user_id` hoặc `profile_id`). | | **timestamp** | ISO 8601 date & time | Ngày và giờ xảy ra sự kiện. | | **event** | String | Tên sự kiện như bạn đã định nghĩa trong phần Events names của [cấu hình PostHog](https://app.adapty.io/integrations/posthog). | | **properties** | Object | Chứa [properties.$set](posthog#propertiesset-parameters) và tất cả [các thuộc tính theo từng sự kiện](messaging#event-properties). Mỗi thuộc tính là tùy chọn và sẽ không được gửi tới PostHog nếu bị thiếu. | ### Các tham số properties.$set \{#propertiesset-parameters\} Mỗi tham số trong object `properties.$set` là tùy chọn và sẽ không được gửi tới PostHog nếu bị thiếu. | **Tham số** | **Kiểu** | **Mô tả** | | --------------- | ------------- | ------------------------------------------------------------ | | **email** | String | Địa chỉ email của người dùng. | | **first_name** | String | Tên của người dùng. | | **last_name** | String | Họ của người dùng. | | **birthday** | String (Date) | Ngày sinh của người dùng. | | **gender** | String | Giới tính của người dùng. | | **os** | String | Hệ điều hành của thiết bị người dùng. | ## Thiết lập tích hợp PostHog \{#setting-up-posthog-integration\} 1. Mở trang [Integrations -> PostHog](https://app.adapty.io/integrations/posthog) trong Adapty Dashboard và bật toggle. <img src="/assets/shared/img/posthog-on.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Đăng nhập vào [PostHog Dashboard](https://posthog.com/). 3. Điều hướng đến **Settings -> Project**. <img src="/assets/shared/img/posthog-settings.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trong cửa sổ **Project**, cuộn xuống phần **Project ID** và sao chép **Project API key**. 5. Dán API key vào trường **Project API key** trong Adapty Dashboard. PostHog không có chế độ Sandbox riêng cho tích hợp server-to-server. 6. Chọn **PostHog Deployment** của bạn: | Tùy chọn | Mô tả | | -------- | ------------------------------------------------------------ | | us/eu | Các deployment mặc định được PostHog lưu trữ. | | Custom | Dành cho các instance tự lưu trữ. Nhập URL instance của bạn vào trường **PostHog Instance URL**. | 7. (Tùy chọn) Nếu bạn đang dùng deployment PostHog tự lưu trữ, nhập địa chỉ deployment của bạn vào trường **PostHog Instance URL**. 8. (Tùy chọn) Điều chỉnh các cài đặt như **Reporting Proceeds**, **Exclude Historical Events**, **Report User's Currency** và **Send Trial Price**. Xem [Cài đặt tích hợp](configuration#integration-settings) để biết thêm chi tiết về các tùy chọn này. 9. (Tùy chọn) Bạn cũng có thể tùy chỉnh các sự kiện nào được gửi tới PostHog trong phần **Events names**. Tắt các sự kiện không cần thiết hoặc đổi tên chúng theo nhu cầu. 10. Nhấn **Save** để hoàn tất thiết lập. ## Cấu hình SDK \{#sdk-configuration\} Để bật tính năng nhận dữ liệu attribution từ PostHog, truyền giá trị `distinctId` vào Adapty như sau: :::note Các SDK bên thứ ba tạo user ID theo cách bất đồng bộ. ID có thể chưa sẵn sàng khi `Adapty.activate()` chạy. Nếu **Customer User ID** của bạn đến từ một trong các SDK này, hãy gọi `Adapty.activate()` mà không truyền ID đó. Khi ID đã có, gọi `setIntegrationIdentifier()`, sau đó gọi `identify()` với CUID. ::: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> ```swift showLineNumbers do { let distinctId = PostHogSDK.shared.getDistinctId() try await Adapty.setIntegrationIdentifier( key: "posthog_distinct_user_id", value: distinctId ) } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Kotlin" default> ```kotlin showLineNumbers Adapty.setIntegrationIdentifier("posthog_distinct_user_id", PostHog.distinctId()) { error -> if (error != null) { // handle the error } ``` </TabItem> <TabItem value="java" label="Java" default> ```java showLineNumbers Adapty.setIntegrationIdentifier("posthog_distinct_user_id", PostHog.distinctId(), error -> { if (error != null) { // handle the error } }); ``` </TabItem> <TabItem value="flutter" label="Flutter" default> ```javascript showLineNumbers try { final distinctId = await Posthog().getDistinctId(); await Adapty().setIntegrationIdentifier( key: "posthog_distinct_user_id", value: distinctId, ); } catch (e) { // handle the error } ``` </TabItem> <TabItem value="unity" label="Unity" default> Không có SDK PostHog chính thức cho Unity. </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers // ... const posthog = usePostHog(); // ... try { await adapty.setIntegrationIdentifier("posthog_distinct_user_id", posthog.getDistinctId()); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> Adapty sẽ gửi các sự kiện tới PostHog và nhận attribution từ đó. --- # File: splitmetrics --- --- title: "SplitMetrics Acquire" description: "Sử dụng SplitMetrics với Adapty để A/B test và tối ưu hóa gói đăng ký." --- Với tích hợp [SplitMetrics Acquire](https://splitmetrics.com/acquire/), bạn có thể xem chính xác số tiền mà Apple Search Ads mang lại từ các gói đăng ký. Và bạn có thể theo dõi người dùng trong nhiều tháng để biết quảng cáo của mình sinh ra bao nhiêu doanh thu theo thời gian. Ngoài ra, Adapty gửi [các sự kiện gói đăng ký](events) đến SplitMetrics Acquire để bạn có thể xây dựng dashboard tùy chỉnh và tự động hóa dựa trên attribution từ Apple Search Ads. Tích hợp này không thêm dữ liệu attribution nào vào Adapty, vì chúng ta đã có đầy đủ dữ liệu cần thiết trực tiếp từ ASA. ## Cách thiết lập tích hợp SplitMetrics Acquire \{#how-to-set-up-splitmetrics-acquire-integration\} Để tích hợp SplitMetrics Acquire, vào [Integrations > SplitMetrics Acquire](https://app.adapty.io/integrations/splitmetrics) và điền thông tin xác thực. <img src="/assets/shared/img/8255349-CleanShot_2023-08-14_at_17.39.422x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Mở tài khoản SplitMetrics Acquire của bạn, di chuột qua một trong các logo MMP và nhấn nút **Settings**. Tìm Client ID trong hộp thoại tại mục **5**, sao chép và dán vào Adapty tại trường **Client ID**. <img src="/assets/shared/img/4d0b2b6-Adapty.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <img src="/assets/shared/img/4f8d0b8-AdaptyGuide.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bạn cũng cần thiết lập Apple App ID để sử dụng tích hợp. Để tìm App ID, mở trang ứng dụng trong App Store Connect, vào trang **App Information** trong phần **General**, và tìm **Apple ID** ở góc dưới bên trái màn hình. <img src="/assets/shared/img/61578ee-CleanShot_2022-04-20_at_17.55.03.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sự kiện và tag \{#events-and-tags\} Bên dưới phần thông tin xác thực có ba nhóm sự kiện bạn có thể gửi từ Adapty đến SplitMetrics Acquire. Chỉ cần bật những sự kiện bạn cần. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/1b0c777-CleanShot_2023-08-11_at_14.56.362x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chúng tôi khuyến nghị sử dụng tên sự kiện mặc định do Adapty cung cấp. Tuy nhiên, bạn có thể thay đổi tên sự kiện theo nhu cầu. Adapty sẽ gửi các sự kiện gói đăng ký đến SplitMetrics Acquire thông qua tích hợp server-to-server, cho phép bạn xem toàn bộ sự kiện gói đăng ký trong dashboard SplitMetrics. ## Cấu hình SDK \{#sdk-configuration\} Bạn không cần cấu hình gì thêm ở phía SDK, nhưng chúng tôi khuyến nghị gửi `customerUserId` đến Adapty để đảm bảo độ chính xác cao hơn. :::warning Hãy đảm bảo bạn đã cấu hình [Apple Search Ads](apple-search-ads) trong Adapty và [tải lên thông tin xác thực](https://app.adapty.io/settings/apple-search-ads). Nếu không có chúng, SplitMetrics Acquire sẽ không hoạt động. ::: ## Khắc phục sự cố \{#troubleshooting\} Nếu tích hợp với SplitMetrics Acquire không hoạt động dù đã cấu hình đúng: - Đảm bảo bạn đã bật toggle **Receive Apple Search Ads attribution in Adapty** trong [App Settings -> Apple Search Ads tab](https://app.adapty.io/settings/apple-search-ads), đã cấu hình [Apple Search Ads](apple-search-ads) trong Adapty và [tải lên thông tin xác thực](https://app.adapty.io/settings/apple-search-ads). Nếu không có chúng, SplitMetric sẽ không hoạt động. - Đảm bảo các hồ sơ người dùng có attribution ASA không phải organic. Chỉ những hồ sơ người dùng có attribution ASA chi tiết, không phải organic mới gửi sự kiện đến Adapty. ## Cấu trúc sự kiện SplitMetrics Acquire \{#splitmetrics-acquire-event-structure\} Adapty gửi sự kiện đến SplitMetrics Acquire qua GET request sử dụng query parameter. Mỗi sự kiện có cấu trúc như sau: ```json { "source": "Apple Search Ads", "app_id": "123456789", "name": "subscription_renewed", "type": "subscription_renewed", "revenue": 9.99, "currency": "USD", "tap_time": "2024-03-01 12:00:00", "open_time": "2024-03-01 12:05:00", "event_time": "2024-03-02 12:00:00", "adaccount_id": "123456", "campaign_id": "123456789", "adgroup_id": "123456789", "keyword_id": "123456789", "creative_set_id": "123456789", "Ad_id": "123456789", "country_or_region": "US", "conversion_type": "Download", "user_id": "user_12345", "att_status": "3", "device_type": "iphone", "app_version": "1.2.3", "sdk_version": "2.10.0", "ios_version": "17.2", "event_value": "{\"vendor_product_id\":\"yearly.premium.6999\",\"original_transaction_id\":\"GPA.3383...\"}", "event_id": "123e4567-e89b-12d3-a456-426614174000" } ``` Trong đó: | Tham số | Kiểu | Mô tả | |:--------------------|:-------|:---------------------------------------------------------------------------------------------------------------------------| | `source` | String | Luôn là "Apple Search Ads". | | `app_id` | String | Apple App ID. | | `name` | String | Tên sự kiện (ánh xạ từ sự kiện Adapty). | | `type` | String | Loại sự kiện (giống với `name`). | | `revenue` | Float | Số tiền doanh thu. | | `currency` | String | Mã tiền tệ. | | `tap_time` | String | Ngày và giờ nhấn vào quảng cáo. | | `open_time` | String | Ngày và giờ mở ứng dụng (cài đặt). | | `event_time` | String | Ngày và giờ xảy ra sự kiện. | | `adaccount_id` | String | ID tổ chức ASA. | | `campaign_id` | String | ID chiến dịch ASA. | | `adgroup_id` | String | ID nhóm quảng cáo ASA. | | `keyword_id` | String | ID từ khóa ASA. | | `creative_set_id` | String | ID bộ sáng tạo ASA. | | `Ad_id` | String | ID quảng cáo ASA. | | `country_or_region` | String | Quốc gia hoặc khu vực của cửa hàng. | | `conversion_type` | String | Loại chuyển đổi (ví dụ: "Download"). | | `user_id` | String | Customer User ID hoặc Adapty Profile ID. | | `att_status` | String | Trạng thái sử dụng tracking (0-3). | | `device_type` | String | Loại thiết bị (ví dụ: "iphone", "ipad"). | | `app_version` | String | Phiên bản ứng dụng. | | `sdk_version` | String | Phiên bản Adapty SDK. | | `ios_version` | String | Phiên bản iOS. | | `event_value` | String | Chuỗi JSON chứa tất cả [chi tiết sự kiện](webhook-event-types-and-fields#for-most-event-types) hiện có. | | `event_id` | String | ID sự kiện duy nhất (UUID). | --- # File: braze --- --- title: "Braze" description: "Tích hợp Braze với Adapty để tối ưu hóa tương tác khách hàng và push notification." --- Là một trong những giải pháp tương tác khách hàng hàng đầu, [Braze](https://www.braze.com/) cung cấp nhiều công cụ đa dạng cho push notification, email, SMS và in-app messaging. Bằng cách tích hợp Adapty với Braze, bạn có thể dễ dàng truy cập tất cả các sự kiện gói đăng ký của mình ở một nơi, cho phép kích hoạt giao tiếp tự động dựa trên các sự kiện đó. Adapty cung cấp bộ dữ liệu đầy đủ giúp bạn theo dõi [sự kiện gói đăng ký](events) từ tất cả các cửa hàng ở một nơi và có thể dùng để cập nhật hồ sơ người dùng trong Braze. Với Adapty, bạn có thể dễ dàng theo dõi hành vi của người dùng, tìm hiểu sở thích của họ và sử dụng thông tin đó để giao tiếp theo cách có mục tiêu và hiệu quả. Do đó, tích hợp này cho phép bạn theo dõi các sự kiện gói đăng ký trong Braze dashboard và liên kết chúng với [các chiến dịch thu hút người dùng.](https://www.braze.com/product/journey-orchestration) Adapty gửi các sự kiện gói đăng ký, thuộc tính người dùng và giao dịch mua sang Braze, giúp bạn xây dựng giao tiếp có mục tiêu với khách hàng qua push notification của Braze sau một quá trình tích hợp ngắn gọn và đơn giản như mô tả bên dưới. ## Cách thiết lập tích hợp Braze \{#how-to-set-up-braze-integration\} Để tích hợp Braze, vào [Integrations -> Braze](https://app.adapty.io/integrations/braze), bật toggle và điền vào các trường thông tin. Bước đầu tiên của quá trình tích hợp là cung cấp các thông tin xác thực cần thiết để thiết lập kết nối giữa hồ sơ Braze và Adapty của bạn. Bạn sẽ cần **REST API Key**, **Braze Instance ID** và **App IDs** cho iOS và Android để tích hợp hoạt động đúng cách: <img src="/assets/shared/img/5f1e62c-adapty_braze.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 1. **REST API Key** có thể được tạo trong **Braze Dashboard** → **Settings** → **API Keys**. Đảm bảo key của bạn có quyền `users.track` khi tạo: <img src="/assets/shared/img/b5fdf16-adapty_braze_create_api_key.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <img src="/assets/shared/img/1e5b4b8-adapty_braze_api_key_users_track.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Để lấy **Braze Instance ID**, hãy ghi lại URL Braze Dashboard của bạn và truy cập phần [Braze Docs](https://www.braze.com/docs/api/basics/#endpoints) nơi chỉ định instance ID. ID này có dạng khu vực như US-03, EU-01, v.v. 3. iOS và Android App IDs cũng có thể tìm thấy trong Braze Dashboard → Settings → API Keys. Sao chép chúng từ đây: <img src="/assets/shared/img/1e6d21b-adapty_braze_app_ids.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sự kiện, thuộc tính người dùng và giao dịch mua \{#events-user-attributes-and-purchases\} Bên dưới phần thông tin xác thực, có ba nhóm sự kiện bạn có thể gửi từ Adapty sang Braze. Chỉ cần bật những sự kiện bạn cần. Bạn cũng có thể thay đổi tên các sự kiện theo nhu cầu trước khi gửi sang Braze. Xem danh sách đầy đủ các sự kiện do Adapty cung cấp [tại đây](events): <img src="/assets/shared/img/702e628-adapty_braze_events_names.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Adapty sẽ gửi các sự kiện gói đăng ký và thuộc tính người dùng sang Braze thông qua tích hợp server-to-server, cho phép bạn xem chúng trong Braze Dashboard và cấu hình các chiến dịch dựa trên đó. Đối với các sự kiện có doanh thu, chẳng hạn như chuyển đổi dùng thử và gia hạn, Adapty sẽ gửi thông tin này sang Braze dưới dạng giao dịch mua. [Tại đây](messaging#event-properties) bạn có thể tìm thấy thông số kỹ thuật đầy đủ về các thuộc tính sự kiện được gửi sang Braze. :::note Các thuộc tính người dùng hữu ích Adapty gửi một số thuộc tính người dùng cho tích hợp Braze theo mặc định. Bạn có thể tham khảo danh sách bên dưới để xác định thuộc tính nào phù hợp nhất với nhu cầu của bạn. ::: | Thuộc tính người dùng | Loại | Giá trị | |--------------|----|-----| | `adapty_customer_user_id` | String | Chứa giá trị của định danh duy nhất của người dùng do khách hàng định nghĩa. Có thể tìm thấy trong cả Adapty [Dashboard](profiles-crm) và Braze. | | `adapty_profile_id` | String | Chứa giá trị của định danh duy nhất Adapty User Profile ID của người dùng, có thể tìm thấy trong Adapty [Dashboard](profiles-crm). | | `environment` | String | <p>Cho biết người dùng đang hoạt động trong môi trường sandbox hay production.</p><p></p><p>Giá trị là `Sandbox` hoặc `Production`</p> | | `store` | String | <p>Chứa tên cửa hàng đã dùng để thực hiện giao dịch mua.</p><p></p><p>Các giá trị có thể có:</p><p>`app_store` hoặc `play_store`.</p> | | `vendor_product_id` | String | <p>Chứa giá trị Product Id trong cửa hàng Apple/Google.</p><p></p><p>Ví dụ: org.locals.12345</p> | | `subscription_expires_at` | String | <p>Chứa ngày hết hạn của gói đăng ký mới nhất.</p><p></p><p>Định dạng giá trị:</p><p>YYYY-MM-DDTHH:mm:ss.SSS+TZ</p><p>Ví dụ: 2023-02-15T17:22:03.000+0000</p> | | `active_subscription` | String | Giá trị sẽ được đặt thành `true` khi có bất kỳ sự kiện mua/gia hạn nào, hoặc `false` nếu gói đăng ký đã hết hạn. | | `period_type` | String | <p>Cho biết loại kỳ mới nhất cho giao dịch mua hoặc gia hạn.</p><p></p><p>Các giá trị có thể có:</p><p>`trial` cho kỳ dùng thử hoặc `normal` cho các trường hợp còn lại.</p> | Tất cả giá trị float sẽ được làm tròn thành int. Chuỗi giữ nguyên. Ngoài danh sách các tag được định nghĩa sẵn, bạn cũng có thể gửi [thuộc tính tùy chỉnh](segments#custom-attributes) bằng cách sử dụng tag. Điều này mang lại sự linh hoạt hơn trong loại dữ liệu có thể đưa vào tag và có thể hữu ích để theo dõi thông tin cụ thể liên quan đến sản phẩm hoặc dịch vụ. Tất cả các thuộc tính người dùng tùy chỉnh đều được gửi tự động sang Braze nếu người dùng đánh dấu vào checkbox ** Send user attributes** từ [trang tích hợp](https://app.adapty.io/integrations/braze). ## Cấu hình SDK \{#sdk-configuration\} Để liên kết hồ sơ người dùng trong Adapty và Braze, bạn cần cấu hình Braze SDK với cùng customer user ID như Adapty hoặc sử dụng phương thức `.changeUser()` của nó: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers let braze = Braze(configuration: configuration) braze.changeUser(userId: "adapty_customer_user_id") ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers Braze.getInstance(context).changeUser("adapty_customer_user_id") ``` </TabItem> </Tabs> --- # File: onesignal --- --- title: "OneSignal" description: "Tích hợp OneSignal với Adapty để cải thiện tương tác dựa trên thông báo đẩy." --- [OneSignal](https://onesignal.com/) là nền tảng tương tác khách hàng hàng đầu, cung cấp thông báo đẩy, email, SMS và tin nhắn trong ứng dụng. Tích hợp Adapty với OneSignal cho phép bạn truy cập tất cả các sự kiện gói đăng ký ở một nơi, giúp bạn kích hoạt giao tiếp tự động dựa trên những sự kiện đó. Với Adapty, bạn có thể theo dõi [các sự kiện gói đăng ký](events) trên nhiều cửa hàng, phân tích hành vi người dùng và sử dụng dữ liệu đó để giao tiếp có mục tiêu hơn. Tích hợp này giúp bạn theo dõi các sự kiện gói đăng ký trong dashboard OneSignal và ánh xạ chúng tới [các chiến dịch thu hút](https://documentation.onesignal.com/docs/en/automated-messages). Adapty cập nhật các tag của OneSignal dựa trên sự kiện gói đăng ký, giúp bạn gửi thông báo đẩy được cá nhân hóa với cài đặt tối thiểu. **Đặc điểm tích hợp** | Đặc điểm tích hợp | Mô tả | | :------------------------- | :----------------------------------------------------------- | | Lịch trình | Cập nhật theo thời gian thực | | Chiều dữ liệu | Một chiều: từ Adapty sang máy chủ OneSignal | | Điểm tích hợp Adapty | <ul><li>SDK của OneSignal và Adapty trong mã ứng dụng di động</li><li>Máy chủ Adapty</li></ul>| ## Thiết lập tích hợp OneSignal \{#setting-up-one-signal-integration\} Để thiết lập tích hợp: 1. Mở [Integrations → OneSignal](https://app.adapty.io/integrations/onesignal) trong Adapty Dashboard của bạn. <img src="/assets/shared/img/onesignal-on.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Bật nút chuyển đổi tích hợp. 3. Nhập **OneSignal App ID** của bạn. Để thiết lập tích hợp với OneSignal, vào [Integrations -> OneSignal](https://app.adapty.io/integrations/onesignal) trong Adapty dashboard của bạn, bật nút chuyển đổi và cấu hình thông tin xác thực tích hợp. ## Lấy OneSignal App ID của bạn \{#retrieving-your-onesignal-app-id\} Tìm **OneSignal App ID** của bạn trong [OneSignal Dashboard](https://dashboard.onesignal.com/login): 1. Điều hướng đến **Settings** → **Keys & IDs**. <img src="/assets/shared/img/onesignal-dashboard.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Sao chép **OneSignal App ID** của bạn và dán vào trường **App ID** trong Adapty Dashboard. <img src="/assets/shared/img/onesignal-id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bạn có thể tìm thêm thông tin về OneSignal ID trong [tài liệu sau.](https://documentation.onesignal.com/docs/en/keys-and-ids) ### Cấu hình sự kiện \{#configuring-events\} Adapty cho phép bạn gửi ba nhóm sự kiện tới OneSignal. Bật những sự kiện bạn cần trong Adapty Dashboard. Bạn có thể xem danh sách đầy đủ các sự kiện có sẵn kèm mô tả chi tiết [tại đây](events). <img src="/assets/shared/img/onesignal.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Adapty gửi các sự kiện gói đăng ký tới OneSignal thông qua tích hợp server-to-server, cho phép bạn theo dõi toàn bộ hoạt động liên quan đến gói đăng ký trong OneSignal. :::warning Bắt đầu từ ngày 17 tháng 4 năm 2023, Gói Miễn phí của OneSignal không còn hỗ trợ tích hợp này. Tính năng này chỉ có trên các gói **Growth**, **Professional** và **cao hơn**. Để biết chi tiết, xem [Bảng giá OneSignal](https://onesignal.com/pricing). ::: ## Tag tùy chỉnh \{#custom-tags\} Tích hợp này cập nhật và gán nhiều thuộc tính cho người dùng Adapty của bạn dưới dạng tag, sau đó được gửi tới OneSignal. Tham khảo danh sách tag bên dưới để tìm những tag phù hợp nhất với nhu cầu của bạn. :::warning OneSignal có giới hạn số lượng tag. Điều này bao gồm cả các tag do Adapty tạo ra và các tag hiện có trong OneSignal. Vượt quá giới hạn có thể gây ra lỗi khi gửi sự kiện. ::: | Tag | Kiểu | Mô tả | |---|----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `adapty_customer_user_id` | String | Mã định danh duy nhất của người dùng trong ứng dụng của bạn. Phải nhất quán trong toàn hệ thống, Adapty và OneSignal. | | `adapty_profile_id` | String | ID hồ sơ người dùng Adapty, có sẵn trong [Adapty Dashboard](profiles-crm). | | `environment` | String | `Sandbox` hoặc `Production`, cho biết môi trường hiện tại của người dùng. | | `store` | String | Cửa hàng nơi sản phẩm được mua. Các tùy chọn: **app_store**, **play_store**, **stripe**, hoặc tên [cửa hàng tùy chỉnh](custom-store) của bạn. | | `vendor_product_id` | String | ID sản phẩm trong cửa hàng ứng dụng (ví dụ: `org.locals.12345`). | | `subscription_expires_at` | String | Ngày hết hạn của gói đăng ký mới nhất (`YYYY-MM-DDTHH:MM:SS+0000`, ví dụ: `2023-02-10T17:22:03.000000+0000`). | | `last_event_type` | String | Loại sự kiện mới nhất từ [danh sách sự kiện Adapty](events).<br/> Lưu ý những điều sau:<br/>- Đối với sự kiện **Subscription expired**, Adapty gửi thuộc tính `last_event_type` là `subscription_cancelled`.<br/>- Đối với **Trial renew canceled** – là `auto_renew_off`<br/>- Đối với **Subscription renew canceled** – là `auto_renew_off_subscription` | | `purchase_date` | String | Ngày giao dịch gần nhất (`YYYY-MM-DDTHH:MM:SS+0000`, ví dụ: `2023-02-10T17:22:03.000000+0000`). | | `active_subscription` | String | `true` nếu người dùng có gói đăng ký đang hoạt động và `false` nếu gói đăng ký đã hết hạn. | | `period_type` | String | Cho biết loại kỳ gần nhất cho lần mua hoặc gia hạn. Các giá trị có thể: `trial` cho thời gian dùng thử hoặc `normal` cho tất cả các trường hợp khác. | Tất cả các giá trị float được làm tròn thành số nguyên. Các chuỗi vẫn giữ nguyên. Ngoài các tag đã định sẵn, bạn có thể gửi [thuộc tính tùy chỉnh](segments#custom-attributes) dưới dạng tag, mang lại sự linh hoạt hơn trong dữ liệu bạn đưa vào. Điều này hữu ích cho việc theo dõi các chi tiết cụ thể liên quan đến sản phẩm hoặc dịch vụ của bạn. Các thuộc tính người dùng tùy chỉnh được tự động gửi tới OneSignal nếu checkbox **Send user attributes** được bật trên [trang tích hợp](https://app.adapty.io/integrations/onesignal). Khi không được chọn, Adapty gửi đúng 10 tag. Nếu được chọn, có thể gửi hơn 10 tag, cho phép thu thập dữ liệu phong phú hơn. ## Cấu hình SDK \{#sdk-configuration\} Có hai cách để tích hợp OneSignal với Adapty: 1. **Legacy (trước v5):** Sử dụng `playerId` (đã deprecated trong [OneSignal SDK v5](https://github.com/OneSignal/OneSignal-iOS-SDK/releases/tag/5.0.0)). 2. **Hiện tại (v5+):** Sử dụng `subscriptionId`. :::warning Đảm bảo gửi `playerId` (cho OneSignal SDK trước v5) hoặc `subscriptionId` (cho OneSignal SDK v5+) tới Adapty. Nếu không có điều này, các tag OneSignal sẽ không được cập nhật và tích hợp sẽ không hoạt động đúng. ::: <Tabs groupId="current-version" queryString> <TabItem value="v5+" label="OneSignal SDK v5+ (current)" default> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers // SubscriptionID OneSignal.Notifications.requestPermission({ accepted in Task { // Adapty SDK 4.x try await Adapty.setIntegrationIdentifier(.oneSignalSubscriptionId(OneSignal.User.pushSubscription.id)) // Adapty SDK 3.x try await Adapty.setIntegrationIdentifier( key: "one_signal_subscription_id", value: OneSignal.User.pushSubscription.id ) } }, fallbackToSettings: true) ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers // SubscriptionID val oneSignalSubscriptionObserver = object: IPushSubscriptionObserver { override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) { Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.current.id) { error -> if (error != null) { // handle the error } } } } ``` </TabItem> <TabItem value="java" label="(Android) Java" default> ```java showLineNumbers // SubscriptionID IPushSubscriptionObserver oneSignalSubscriptionObserver = state -> { Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.getCurrent().getId(), error -> { if (error != null) { // handle the error } }); }; ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```javascript showLineNumbers // 1. Since OneSignal.User.pushSubscription.id may return null if called too early, // OneSignal suggests to listen for the updates: OneSignal.User.pushSubscription.addObserver((state) { if (state.current.optedIn) { // now you can try to retrieve subscriptionId } }); // 2. Then you can push subscriptionId to Adapty: final subscriptionId = OneSignal.User.pushSubscription.id; if (subscriptionId != null) { await Adapty().setIntegrationIdentifier(key: "one_signal_subscription_id", value: subscriptionId); } ``` </TabItem> <TabItem value="unity" label="Unity (C#)" default> ```csharp showLineNumbers using AdaptySDK; using OneSignalSDK; var pushUserId = OneSignal.Default.PushSubscriptionState.userId; Adapty.SetIntegrationIdentifier( "one_signal_player_id", pushUserId, (error) => { // handle the error }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers OneSignal.User.pushSubscription.addEventListener('change', (subscription) => { const subscriptionId = subscription.current.id; if (subscriptionId) { adapty.setIntegrationIdentifier("one_signal_subscription_id", subscriptionId); } }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="pre-v5" label="OneSignal SDK v. up to 4.x (legacy)" default> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers // PlayerID // in your OSSubscriptionObserver implementation func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) { if let playerId = stateChanges.to.userId { Task { // Adapty SDK 4.x try await Adapty.setIntegrationIdentifier(.oneSignalPlayerId(playerId)) // Adapty SDK 3.x try await Adapty.setIntegrationIdentifier( key: "one_signal_player_id", value: playerId ) } } } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers // PlayerID val osSubscriptionObserver = OSSubscriptionObserver { stateChanges -> stateChanges?.to?.userId?.let { playerId -> Adapty.setIntegrationIdentifier("one_signal_player_id", playerId) { error -> if (error != null) { // handle the error } } } } ``` </TabItem> <TabItem value="java" label="Java" default> ```java showLineNumbers // PlayerID OSSubscriptionObserver osSubscriptionObserver = stateChanges -> { OSSubscriptionState to = stateChanges != null ? stateChanges.getTo() : null; String playerId = to != null ? to.getUserId() : null; if (playerId != null) { Adapty.setIntegrationIdentifier("one_signal_player_id", playerId, error -> { if (error != null) { // handle the error } }); } }; ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```javascript showLineNumbers // PlayerID (pre-v5 OneSignal SDK) // in your OSSubscriptionObserver implementation func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) { if let playerId = stateChanges.to.userId { Task { try await Adapty.setIntegrationIdentifier( key: "one_signal_player_id", value: playerId ) } } } ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers OneSignal.addSubscriptionObserver(event => { const playerId = event.to.userId; adapty.setIntegrationIdentifier("one_signal_player_id", playerId); }); ``` </TabItem> </Tabs> </TabItem> </Tabs> Đọc thêm trong tài liệu OneSignal: - [Push subscription ID](https://documentation.onesignal.com/docs/en/mobile-sdk-reference#user-pushsubscription-id) - [Thay đổi push subscription](https://documentation.onesignal.com/docs/en/mobile-sdk-reference#addobserver-push-subscription-changes) ## Xử lý nhiều thiết bị \{#dealing-with-multiple-devices\} Khi một người dùng có nhiều thiết bị, việc theo dõi sự kiện mua hàng và gói đăng ký có thể là thách thức. OneSignal cung cấp cách xử lý vấn đề này thông qua [external user IDs](https://documentation.onesignal.com/docs/en/users). Để giữ dữ liệu người dùng nhất quán trên các thiết bị: 1. Khớp các thiết bị khác nhau ở **phía máy chủ** của bạn và gửi dữ liệu này tới OneSignal. 2. Sử dụng [customer_user_id](identifying-users) của Adapty làm [externalUserId](https://documentation.onesignal.com/docs/en/users#external-id) trong OneSignal. Nếu ứng dụng của bạn không có hệ thống đăng ký, hãy cân nhắc sử dụng một mã định danh duy nhất khác vẫn nhất quán trên tất cả thiết bị của người dùng. Điều quan trọng là duy trì sự nhất quán của mã định danh người dùng trên tất cả thiết bị và cập nhật OneSignal mỗi khi ID của người dùng thay đổi. Điều này đơn giản hóa việc theo dõi hoạt động và gói đăng ký của người dùng, đồng thời đảm bảo tin nhắn nhất quán và cho phép phân tích chính xác hơn cũng như trải nghiệm người dùng tốt hơn. Để biết thêm chi tiết, xem [tài liệu external user ID của OneSignal](https://documentation.onesignal.com/docs/en/users). --- # File: pushwoosh --- --- title: "Pushwoosh" description: "Tích hợp Pushwoosh với Adapty để theo dõi thông báo đẩy một cách liền mạch." --- Adapty sử dụng các sự kiện gói đăng ký để cập nhật tags cho hồ sơ người dùng [Pushwoosh](https://www.pushwoosh.com/), giúp bạn xây dựng chiến lược giao tiếp có mục tiêu với khách hàng thông qua thông báo đẩy sau khi hoàn tất thiết lập tích hợp đơn giản như mô tả bên dưới. ## Cách thiết lập tích hợp Pushwoosh \{#how-to-set-up-pushwoosh-integration\} Để tích hợp Pushwoosh, hãy truy cập [**Integrations** -> **Pushwoosh**](https://app.adapty.io/integrations/pushwoosh), bật toggle từ tắt sang bật, và điền vào các trường thông tin. Trước tiên, hãy cài đặt thông tin xác thực để thiết lập kết nối giữa hồ sơ Pushwoosh và Adapty của bạn. Cần có Pushwoosh App ID và auth token. <img src="/assets/shared/img/64e48a1-CleanShot_2023-08-18_at_11.13.212x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 1. **App ID** có thể tìm thấy trong dashboard Pushwoosh của bạn. <img src="/assets/shared/img/ee27687-CleanShot_2023-08-18_at_14.37.442x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. **Auth token** có thể tìm thấy trong phần API Access của Pushwoosh Settings. <img src="/assets/shared/img/50e634b-CleanShot_2023-08-18_at_14.35.022x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sự kiện và tags \{#events-and-tags\} Bên dưới phần thông tin xác thực có ba nhóm sự kiện bạn có thể gửi từ Adapty đến Pushwoosh. Chỉ cần bật những sự kiện bạn cần. Bạn cũng có thể thay đổi tên sự kiện theo nhu cầu trước khi gửi sang Pushwoosh. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/392dc31-screencapture-app-adapty-io-integrations-pushwoosh-2023-08-22-13_31_07.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Adapty sẽ gửi các sự kiện gói đăng ký đến Pushwoosh thông qua tích hợp server-to-server, cho phép bạn xem tất cả sự kiện gói đăng ký trong Pushwoosh Dashboard. :::note Custom tags Với Adapty, bạn cũng có thể sử dụng custom tags cho tích hợp Pushwoosh. Bạn có thể tham khảo danh sách tags bên dưới để xác định tag nào phù hợp nhất với nhu cầu của mình. ::: | Tag | Kiểu | Giá trị | |---|----|-----| | `adapty_customer_user_id` | String | Chứa giá trị định danh duy nhất của người dùng, có thể tìm thấy ở phía Pushwoosh. | | `adapty_profile_id` | String | Chứa giá trị ID hồ sơ người dùng Adapty duy nhất của người dùng, có thể tìm thấy trong [dashboard](profiles-crm) Adapty của bạn. | | `environment` | String | <p>Cho biết người dùng đang hoạt động trong môi trường sandbox hay production.</p><p></p><p>Giá trị là `Sandbox` hoặc `Production`</p> | | `store` | String | <p>Chứa tên cửa hàng được sử dụng để thực hiện giao dịch mua.</p><p></p><p>Các giá trị có thể có:</p><p>`app_store` hoặc `play_store`.</p> | | `vendor_product_id` | String | <p>Chứa giá trị Product ID trong cửa hàng Apple/Google.</p><p></p><p>Ví dụ: org.locals.12345</p> | | `subscription_expires_at` | String | <p>Chứa ngày hết hạn của gói đăng ký mới nhất.</p><p></p><p>Định dạng giá trị:</p><p>year-month dayThour:minute:second</p><p>Ví dụ: 2023-02-10T17:22:03.000000+0000</p> | | `last_event_type` | String | Cho biết loại sự kiện cuối cùng nhận được từ danh sách [sự kiện Adapty](events) tiêu chuẩn mà bạn đã bật cho tích hợp. | | `purchase_date` | String | <p>Chứa ngày của giao dịch cuối cùng (mua lần đầu hoặc gia hạn).</p><p></p><p>Định dạng giá trị:</p><p>year-month dayThour:minute:second</p><p>Ví dụ: 2023-02-10T17:22:03.000000+0000</p> | | `original_purchase_date` | String | <p>Chứa ngày mua lần đầu tiên theo giao dịch.</p><p></p><p>Định dạng giá trị:</p><p>year-month dayThour:minute:second</p><p>Ví dụ: 2023-02-10T17:22:03.000000+0000</p> | | `active_subscription` | String | Giá trị sẽ được đặt thành `true` khi có sự kiện mua/gia hạn, hoặc `false` nếu gói đăng ký đã hết hạn. | | `period_type` | String | <p>Cho biết loại kỳ mới nhất cho giao dịch mua hoặc gia hạn.</p><p></p><p>Các giá trị có thể có:</p><p>`trial` cho giai đoạn dùng thử hoặc `normal` cho các trường hợp còn lại.</p> | Tất cả giá trị float sẽ được làm tròn thành int. Chuỗi string giữ nguyên. Ngoài danh sách tags có sẵn, bạn còn có thể gửi [thuộc tính tùy chỉnh](segments#custom-attributes) bằng tags. Điều này mang lại sự linh hoạt hơn về loại dữ liệu có thể đưa vào tag và hữu ích cho việc theo dõi thông tin cụ thể liên quan đến sản phẩm hoặc dịch vụ. Tất cả thuộc tính tùy chỉnh của người dùng sẽ được tự động gửi đến Pushwoosh nếu người dùng đánh dấu vào ô **Send user custom attributes** trên [trang tích hợp](https://app.adapty.io/integrations/pushwoosh). ## Cấu hình SDK \{#sdk-configuration\} Để liên kết Adapty với Pushwoosh, bạn cần gửi cho chúng tôi giá trị `HWID`: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" default> ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "pushwoosh_hwid", value: Pushwoosh.sharedInstance().getHWID() ) } catch { // handle the error } ``` </TabItem> <TabItem value="kotlin" label="Android (Kotlin)" default> ```kotlin showLineNumbers Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().hwid) { error -> if (error != null) { // handle the error } } ``` </TabItem> <TabItem value="java" label="Android (Java)" default> ```java showLineNumbers Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().getHwid(), error -> { if (error != null) { // handle the error } }); ``` </TabItem> <TabItem value="flutter" label="Flutter (Dart)" default> ```javascript showLineNumbers final hwid = await Pushwoosh.getInstance.getHWID; try { await Adapty().setIntegrationIdentifier( key: "pushwoosh_hwid", value: hwid, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="unity" label="Unity (C#)" default> ```csharp showLineNumbers using AdaptySDK; Adapty.SetIntegrationIdentifier( "pushwoosh_hwid", Pushwoosh.Instance.HWID, (error) => { // handle the error }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers // ... try { await adapty.setIntegrationIdentifier("pushwoosh_hwid", hwid); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> --- # File: slack --- --- title: "Slack" description: "Tích hợp Slack với Adapty để nhận thông báo thời gian thực về các sự kiện gói đăng ký." --- [Slack](https://slack.com/) là ứng dụng nhắn tin và nền tảng năng suất cho doanh nghiệp mà chắc hẳn không cần giới thiệu thêm. Với tích hợp này, bạn sẽ nhận được thông báo trên Slack mỗi khi Adapty ghi nhận một sự kiện doanh thu. Tính năng này rất hữu ích nếu bạn muốn theo dõi từng khoảnh khắc MRR tăng lên, hoặc muốn nắm bắt kịp thời các trường hợp hủy bản dùng thử, sự cố thanh toán, hoàn tiền, và nhiều hơn nữa. ## Cách thiết lập tích hợp Slack \{#how-to-set-up-slack-integration\} Bạn cần: - tạo một app trong workspace Slack của bạn - cấp quyền đăng tin nhắn - sau đó cung cấp thông tin cần thiết cho Adapty tại [Integrations → Slack](https://app.adapty.io/integrations/slack). ### 1\. Tạo app trong Slack \{#1-create-an-app-in-slack\} 1. Truy cập [Slack API dashboard](https://api.slack.com/apps) và tạo app như sau: <img src="/assets/shared/img/f43aedc-CleanShot_2024-01-04_at_18.27.412x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <img src="/assets/shared/img/08fa9e6-CleanShot_2024-01-04_at_18.28.142x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Đặt tên tùy ý (ví dụ `Adapty`) và thêm vào workspace của bạn: <img src="/assets/shared/img/5002bb1-CleanShot_2024-01-04_at_18.29.132x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### 2\. Cấp quyền đăng tin nhắn và lấy token cho app \{#2-give-permission-to-post-and-get-a-token-for-your-app\} Bạn sẽ được chuyển hướng đến trang app của mình trong Slack. 1. Kéo xuống và nhấn **Permissions**: <img src="/assets/shared/img/9750451-CleanShot_2024-01-04_at_18.48.072x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Sau khi chuyển hướng, kéo xuống phần **Scopes** và nhấn **Add an OAuth Scope**: <img src="/assets/shared/img/db5b5f4-CleanShot_2024-01-04_at_18.50.262x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Cấp quyền `chat:write`, `chat:write.public` và `chat:write.customize`. Các quyền này cần thiết để đăng tin nhắn vào các kênh và tùy chỉnh nội dung tin nhắn: <img src="/assets/shared/img/d97ccb9-CleanShot_2024-01-04_at_18.51.572x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Kéo lại lên đầu trang và nhấn **Install to Workspace**: <img src="/assets/shared/img/14608e3-CleanShot_2024-01-04_at_19.17.58.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấn **Allow** tại đây: <img src="/assets/shared/img/143967e-CleanShot_2024-01-04_at_18.53.292x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau bước này, bạn sẽ được chuyển về trang tương tự, nhưng lần này sẽ có OAuth Token (`xoxb-...`). Đây chính là thứ cần thiết để hoàn tất thiết lập: <img src="/assets/shared/img/59b33ee-CleanShot_2024-01-04_at_18.55.222x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### 3\. Cấu hình tích hợp trong Adapty \{#3-configure-the-integration-in-adapty\} 1. Truy cập [**Integrations** → **Slack**](https://app.adapty.io/integrations/slack): <img src="/assets/shared/img/b4ffd71-CleanShot_2024-01-04_at_19.05.222x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Dán token `xoxb-...` từ bước trước và chọn các kênh mà app sẽ đăng tin nhắn vào. Bạn có thể thiết lập để chỉ nhận sự kiện từ môi trường production, sandbox hoặc cả hai. Bạn cũng có thể chọn đơn vị tiền tệ hiển thị (giữ nguyên hoặc quy đổi sang USD). :::note Lưu ý: nếu bạn muốn đăng tin nhắn từ Adapty vào một kênh riêng tư, bạn cần thêm thủ công app `Adapty` mà bạn đã tạo trong Slack vào kênh đó. Nếu không, tính năng này sẽ không hoạt động. ::: 3. Cuối cùng, bạn có thể chọn các sự kiện muốn nhận trong phần **Events**: <img src="/assets/shared/img/970a7bb-CleanShot_2024-01-04_at_19.09.472x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Vậy là xong! Các sự kiện sẽ được gửi đến các kênh bạn đã chỉ định. Bạn sẽ thấy doanh thu khi có và có thể xem hồ sơ người dùng trong Adapty: <img src="/assets/shared/img/852b8c8-CleanShot_2024-01-04_at_19.11.332x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: s3-exports --- --- title: "Amazon S3" description: "Xuất dữ liệu gói đăng ký sang S3 để phân tích và báo cáo nâng cao." --- Tích hợp của Adapty với Amazon S3 cho phép bạn lưu trữ dữ liệu sự kiện và lượt truy cập paywall một cách an toàn tại một nơi tập trung. Bạn có thể lưu [các sự kiện gói đăng ký](events) vào Amazon S3 bucket của mình dưới dạng tệp .csv. Để thiết lập tích hợp này, bạn cần thực hiện một vài bước đơn giản trong AWS Console và Adapty Dashboard. :::note Lịch gửi dữ liệu Adapty gửi dữ liệu của bạn mỗi **24h** vào lúc 4:00 UTC. Mỗi file sẽ chứa dữ liệu cho các sự kiện được tạo trong toàn bộ ngày dương lịch trước đó theo UTC. Ví dụ: dữ liệu được xuất tự động lúc 4:00 UTC ngày 8 tháng 3 sẽ chứa tất cả các sự kiện được tạo vào ngày 7 tháng 3 từ 00:00:00 đến 23:59:59 theo UTC. ::: ## Cách thiết lập tích hợp Amazon S3 \{#how-to-set-up-amazon-s3-integration\} Để bắt đầu nhận dữ liệu, bạn cần có các thông tin xác thực sau: 1. Access key ID 2. Secret access key 3. Tên S3 bucket 4. Tên thư mục bên trong S3 bucket :::note Thư mục lồng nhau Bạn có thể chỉ định các thư mục lồng nhau trong trường tên Amazon S3 bucket, ví dụ: adapty-events/com.sample-app ::: Để tích hợp Amazon S3, hãy vào [**Integrations** -> **Amazon S3**](https://app.adapty.io/integrations/s3), bật toggle từ tắt sang bật và điền vào các trường thông tin. Trước tiên, hãy nhập thông tin xác thực để thiết lập kết nối giữa Amazon S3 và hồ sơ người dùng Adapty. <img src="/assets/shared/img/2b1a6e3-CleanShot_2023-03-24_at_14.51.272x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trong Adapty Dashboard, các trường sau đây cần được điền để thiết lập kết nối: | Trường | Mô tả | | :--------------------------- | :----------------------------------------------------------- | | **Access Key ID** | Một mã định danh duy nhất dùng để xác thực quyền truy cập của người dùng hoặc ứng dụng vào dịch vụ AWS. Tìm ID này trong [tệp csv](s3-exports#how-to-create-amazon-s3-credentials) đã tải xuống. | | **Secret Access Key** | Khóa bí mật được dùng kết hợp với Access Key ID để xác thực quyền truy cập của người dùng hoặc ứng dụng vào dịch vụ AWS. Tìm khóa này trong [tệp csv](s3-exports#how-to-create-amazon-s3-credentials) đã tải xuống. | | **S3 Bucket Name** | Tên duy nhất trên toàn cầu để xác định một S3 bucket cụ thể trong đám mây AWS. S3 bucket là dịch vụ lưu trữ đơn giản cho phép người dùng lưu trữ và truy xuất các đối tượng dữ liệu như tệp và hình ảnh trên đám mây. | | **Folder Inside the Bucket** | Tên thư mục bạn muốn tạo bên trong S3 bucket đã chọn. Lưu ý rằng S3 mô phỏng các thư mục bằng cách sử dụng tiền tố khóa đối tượng, về cơ bản chính là tên thư mục. | ## Cách tạo thông tin xác thực Amazon S3 \{#how-to-create-amazon-s3-credentials\} Hướng dẫn này sẽ giúp bạn tạo các thông tin xác thực cần thiết trong AWS Console. ### 1\. Tạo Access Policy \{#1-create-access-policy\} Đầu tiên, truy cập [IAM Policy Dashboard](https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/policies) trong AWS Console và chọn **Create Policy**. <img src="/assets/shared/img/7af075c-CleanShot_2023-03-21_at_10.52.002x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> In the Policy editor, paste the following JSON and change `adapty-s3-integration-test` to your bucket name: ```json showLineNumbers title="Json" { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowListObjectsInBucket", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::adapty-s3-integration-test" }, { "Sid": "AllowAllObjectActions", "Effect": "Allow", "Action": "s3:*Object", "Resource": [ "arn:aws:s3:::adapty-s3-integration-test/*", "arn:aws:s3:::adapty-s3-integration-test" ] }, { "Sid": "AllowBucketLocation", "Effect": "Allow", "Action": "s3:GetBucketLocation", "Resource": "arn:aws:s3:::adapty-s3-integration-test" } ] } ``` <img src="/assets/shared/img/d4e474a-CleanShot_2023-03-21_at_10.56.212x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi hoàn tất cấu hình policy, bạn có thể thêm tags (tùy chọn) rồi nhấn **Next** để chuyển sang bước cuối. Ở bước này, bạn đặt tên cho policy và nhấn **Create policy** để hoàn tất quá trình tạo. <img src="/assets/shared/img/7dcb02f-CleanShot_2023-03-21_at_11.03.372x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### 2\. Tạo IAM user \{#create-iam-user\} Để Adapty có thể tải các báo cáo dữ liệu thô lên bucket của bạn, bạn cần cung cấp cho họ Access Key ID và Secret Access Key của một user có quyền ghi vào bucket đó. Để thực hiện, hãy truy cập IAM Console và chọn [mục Users](https://console.aws.amazon.com/iamv2/home#/users). Từ đó, nhấp vào nút **Add users**. <img src="/assets/shared/img/bb612c8-CleanShot_2023-03-21_at_11.12.392x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đặt tên cho người dùng, chọn **Access key – Programmatic access**, rồi tiến hành cài đặt quyền. <img src="/assets/shared/img/467ee4d-j6aoX.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Ở bước tiếp theo, hãy chọn tùy chọn **Add user to group** rồi nhấn nút **Create group**. <img src="/assets/shared/img/bfd0e80-CleanShot_2023-03-21_at_11.24.592x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Tiếp theo, bạn cần đặt tên cho User Group và chọn policy mà bạn đã tạo trước đó. Sau khi chọn xong policy, nhấn vào nút **Create group** để hoàn tất quá trình. <img src="/assets/shared/img/df29c12-CleanShot_2023-03-21_at_11.28.052x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi tạo nhóm thành công, vui lòng **chọn nhóm đó** và tiến hành bước tiếp theo. <img src="/assets/shared/img/1f3722e-CleanShot_2023-03-21_at_11.36.192x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đây là bước cuối cùng của phần này, bạn chỉ cần nhấn vào nút **Create User** để tiếp tục. <img src="/assets/shared/img/ea43722-CleanShot_2023-03-21_at_11.40.462x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Cuối cùng, bạn có thể **tải thông tin xác thực ở định dạng .csv** hoặc sao chép và dán trực tiếp từ dashboard. <img src="/assets/shared/img/bcf35e1-S3created.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Xuất dữ liệu thủ công \{#manual-data-export\} Ngoài tính năng tự động xuất dữ liệu sự kiện sang Amazon S3, Adapty còn cung cấp chức năng xuất file thủ công. Với tính năng này, bạn có thể chọn một khoảng thời gian cụ thể cho dữ liệu sự kiện và xuất thủ công sang S3 bucket của mình. Điều này giúp bạn kiểm soát tốt hơn dữ liệu cần xuất và thời điểm xuất. Khoảng ngày được chỉ định sẽ được dùng để xuất các sự kiện được tạo từ Ngày A lúc 00:00:00 UTC đến Ngày B lúc 23:59:59 UTC. <img src="/assets/shared/img/466bd29-CleanShot_2023-03-21_at_12.35.252x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Cấu trúc bảng \{#table-structure\} Trong tích hợp AWS S3, Adapty cung cấp một bảng để lưu trữ dữ liệu lịch sử cho các sự kiện giao dịch và lượt truy cập paywall. Bảng này chứa thông tin về hồ sơ người dùng, doanh thu và lợi nhuận, cửa hàng gốc, cùng nhiều điểm dữ liệu khác. Về cơ bản, các bảng này ghi lại toàn bộ giao dịch được tạo ra bởi một ứng dụng trong một khoảng thời gian nhất định. :::warning Lưu ý rằng cấu trúc này có thể thay đổi theo thời gian — với dữ liệu mới được chúng tôi hoặc các bên thứ ba mà chúng tôi hợp tác giới thiệu. Hãy đảm bảo rằng code xử lý dữ liệu của bạn đủ linh hoạt và dựa vào các trường cụ thể, chứ không phụ thuộc vào toàn bộ cấu trúc. ::: Dưới đây là cấu trúc bảng cho các sự kiện: :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: | Cột | Mô tả | |---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **profile_id** | ID người dùng Adapty. | | **event_type** | Tên sự kiện viết thường. Xem phần [Events](events) để tìm hiểu các loại sự kiện. | | **event_datetime** | Ngày theo chuẩn ISO 8601. | | **transaction_id** | Mã định danh duy nhất cho một giao dịch như mua hàng hoặc gia hạn. | | **original_transaction_id** | Mã định danh giao dịch của lần mua hàng gốc. | | **subscription_expires_at** | Ngày hết hạn của gói đăng ký. Thường là trong tương lai. | | **environment** | Có thể là Sandbox hoặc Production. | | **revenue_usd** | Doanh thu tính bằng USD. Có thể trống. | | **proceeds_usd** | Tiền thu về tính bằng USD. Có thể trống. | | **net_revenue_usd** | Doanh thu thuần (thu nhập sau thuế) tính bằng USD. Có thể trống. | | **tax_amount_usd** | Số tiền khấu trừ cho thuế tính bằng USD. Có thể trống. | | **revenue_local** | Doanh thu tính bằng đơn vị tiền tệ địa phương. Có thể trống. | | **proceeds_local** | Tiền thu về tính bằng đơn vị tiền tệ địa phương. Có thể trống. | | **net_revenue_local** | Doanh thu thuần (thu nhập sau thuế) tính bằng đơn vị tiền tệ địa phương. Có thể trống. | | **tax_amount_local** | Số tiền khấu trừ cho thuế tính bằng đơn vị tiền tệ địa phương. Có thể trống. | | **customer_user_id** | ID người dùng do nhà phát triển cung cấp. Ví dụ: có thể là UUID, email hoặc bất kỳ ID nào khác của người dùng. Null nếu bạn chưa thiết lập. | | **store** | Có thể là _app_store_ hoặc _play_store_. | | **product_id** | ID sản phẩm trên Apple App Store, Google Play Store hoặc Stripe. | | **base_plan_id** | [ID gói cơ bản](https://support.google.com/googleplay/android-developer/answer/12154973) trên Google Play Store hoặc [ID giá](https://docs.stripe.com/products-prices/how-products-and-prices-work#use-products-and-prices) trên Stripe. | | **developer_id** | ID nhà phát triển (SDK) của paywall nơi giao dịch bắt nguồn. | | **ab_test_name** | Tên A/B test nơi giao dịch bắt nguồn. | | **ab_test_revision** | Phiên bản của A/B test nơi giao dịch bắt nguồn. | | **paywall_name** | Tên paywall nơi giao dịch bắt nguồn. | | **paywall_revision** | Phiên bản của paywall nơi giao dịch bắt nguồn. | | **profile_county** | Quốc gia của hồ sơ người dùng do Adapty xác định dựa trên địa chỉ IP. | | **install_date** | Ngày cài đặt theo chuẩn ISO 8601. | | **idfv** | [identifierForVendor](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor) trên thiết bị iOS | | **idfa** | [advertisingIdentifier](https://developer.apple.com/documentation/adsupport/asidentifiermanager/advertisingidentifier) trên thiết bị iOS | | **advertising_id** | Advertising ID là mã duy nhất do hệ điều hành Android cấp, mà các nhà quảng cáo có thể dùng để nhận dạng thiết bị của người dùng | | **ip_address** | Địa chỉ IP của thiết bị (có thể là IPv4 hoặc IPv6, ưu tiên IPv4 nếu có). Được cập nhật mỗi khi địa chỉ IP của thiết bị thay đổi. | | **cancellation_reason** | <p>Lý do người dùng hủy gói đăng ký.</p><p></p><p>Có thể là:</p><p>**iOS & Android** _voluntarily_cancelled_, _billing_error_, _refund_</p><p>**iOS** _price_increase_, _product_was_not_available_, _unknown_, _upgraded_</p><p>**Android** _new_subscription_replace_, _cancelled_by_developer_</p> | | **android_app_set_id** | Một [AppSetId](https://developer.android.com/design-for-safety/privacy-sandbox/reference/adservices/appsetid/AppSetId) - ID duy nhất theo thiết bị, theo tài khoản nhà phát triển, có thể đặt lại bởi người dùng, dành cho các trường hợp quảng cáo không liên quan đến kiếm tiền. | | **android_id** | Trên Android 8.0 (API level 26) và các phiên bản cao hơn, đây là một số 64-bit (biểu diễn dưới dạng chuỗi thập lục phân), duy nhất cho mỗi tổ hợp khóa ký ứng dụng, người dùng và thiết bị. Xem thêm tại [tài liệu dành cho nhà phát triển Android](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID). | | **device** | Tên model thiết bị hiển thị cho người dùng. | | **currency** | Mã tiền tệ 3 chữ cái (ISO-4217) của giao dịch. | | **store_country** | Quốc gia của hồ sơ người dùng do Apple/Google store xác định. | | **attribution_source** | Nguồn attribution. | | **attribution_network_user_id** | ID do nguồn attribution cấp cho người dùng. | | **attribution_status** | Có thể là organic, non_organic hoặc unknown. | | **attribution_channel** | Tên kênh marketing. | | **attribution_campaign** | Tên chiến dịch marketing. | | **attribution_ad_group** | Nhóm quảng cáo attribution. | | **attribution_ad_set** | Bộ quảng cáo attribution. | | **attribution_creative** | Từ khóa creative của attribution. | | **attributes** | JSON của [thuộc tính người dùng tùy chỉnh](setting-user-attributes#custom-user-attributes). Bao gồm các thuộc tính tùy chỉnh mà bạn đã thiết lập để gửi từ ứng dụng mobile. Để gửi, hãy bật tùy chọn **Send User Attributes** trong trang [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook). | | **integration_ids** | Tất cả các ID tích hợp liên kết với một hồ sơ người dùng. Dạng từ điển. Ví dụ: {'mixpanel_user_id': 'mixpanelUserId-test', 'facebook_anonymous_id': 'facebookAnonymousId-test'} | Here is the table structure for the paywall visits: | Cột | Mô tả | | :-------------------- | :----------------------------------------------------------------------------------------------------------------------- | | **profile_id** | ID người dùng Adapty. | | **customer_user_id** | ID người dùng do nhà phát triển đặt. Ví dụ: UUID, email, hoặc bất kỳ ID nào khác. Null nếu bạn chưa thiết lập. | | **profile_country** | Quốc gia của hồ sơ người dùng được xác định bởi cửa hàng Apple/Google. | | **install_date** | Ngày cài đặt theo định dạng ISO 8601. | | **store** | Có thể là _app_store_ hoặc _play_store_. | | **paywall_showed_at** | Ngày paywall được hiển thị cho khách hàng. | | **developer_id** | ID (SDK) của nhà phát triển cho paywall nơi giao dịch bắt nguồn. | | **ab_test_name** | Tên của A/B test nơi giao dịch bắt nguồn. | | **ab_test_revision** | Phiên bản của A/B test nơi giao dịch bắt nguồn. | | **paywall_name** | Tên của paywall nơi giao dịch bắt nguồn. | | **paywall_revision** | Phiên bản của paywall nơi giao dịch bắt nguồn. | ## Sự kiện và thẻ tag \{#events-and-tags\} Bạn có thể quản lý dữ liệu nào được truyền qua integration. Integration cung cấp các tùy chọn cấu hình sau: | Cài đặt | Mô tả | | :--------------------------------- | :----------------------------------------------------------- | | **Exclude Historical Events** | Chọn để loại trừ các sự kiện đã xảy ra trước khi người dùng cài đặt ứng dụng có tích hợp Adapty SDK. Điều này giúp tránh trùng lặp sự kiện và đảm bảo báo cáo chính xác. Ví dụ: nếu người dùng kích hoạt gói đăng ký hàng tháng vào ngày 10 tháng 1 và cập nhật ứng dụng có Adapty SDK vào ngày 6 tháng 3, Adapty sẽ bỏ qua các sự kiện trước ngày 6 tháng 3 và chỉ giữ lại các sự kiện sau đó. | | **Include events without profile** | Chọn để bao gồm các giao dịch không được liên kết với hồ sơ người dùng trong Adapty. Những giao dịch này có thể bao gồm các sản phẩm mua một lần được thực hiện trước khi cài đặt Adapty SDK hoặc các giao dịch nhận được từ thông báo máy chủ của cửa hàng mà chưa thể liên kết ngay với một người dùng cụ thể. | | **Send User Attributes** | Nếu bạn muốn gửi các thuộc tính riêng của người dùng, chẳng hạn như tùy chọn ngôn ngữ, và gói OneSignal của bạn hỗ trợ hơn 10 tag, hãy chọn tùy chọn này. Khi bật tùy chọn này, bạn có thể gửi thêm thông tin ngoài 10 tag mặc định. Lưu ý rằng việc vượt quá giới hạn tag có thể dẫn đến lỗi. | <img src="/assets/shared/img/s3-settings.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bên dưới cài đặt tích hợp, có ba nhóm sự kiện bạn có thể xuất, gửi và lưu trữ trong Amazon S3 từ Adapty. Chỉ cần bật những nhóm bạn cần. Xem danh sách đầy đủ các sự kiện mà Adapty cung cấp [tại đây](events). <img src="/assets/shared/img/fd5ccb9-CleanShot_2023-08-17_at_14.49.282x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: google-cloud-storage --- --- title: "Google Cloud Storage" description: "Tích hợp Google Cloud Storage với Adapty để lưu trữ dữ liệu an toàn." --- Kích hoạt tích hợp Google Cloud Storage để lưu trữ an toàn [các sự kiện gói đăng ký](events) và [dữ liệu lượt xem paywall](paywall-metrics) tại một nơi tập trung: bucket Google Cloud Storage của bạn. Mỗi ngày lúc 4 giờ sáng UTC, Adapty sẽ tải các file .csv chứa dữ liệu của ngày hôm trước lên bucket của bạn. Bạn có thể chọn nhận dữ liệu **event**, dữ liệu **paywall visit**, hoặc **cả hai**. Bạn cũng có thể xuất dữ liệu này [thủ công](#manual-data-export) bất cứ lúc nào, cho bất kỳ khoảng thời gian nào. Để thiết lập tích hợp, hãy [tạo khóa truy cập bucket](#create-google-cloud-storage-credentials) trong Google Cloud console của bạn, rồi [thêm vào cài đặt Adapty](#set-up-google-cloud-storage-integration). ## Lịch tải lên và thời gian xử lý \{#upload-schedule-and-duration\} Adapty tải dữ liệu lên Google Cloud Storage mỗi 24 giờ, lúc 04:00 UTC. Các file chứa dữ liệu cho các sự kiện được tạo trong ngày dương lịch trước đó (UTC). File được tải lên vào ngày 8 tháng 3 sẽ chứa tất cả sự kiện được tạo vào ngày 7 tháng 3, từ 00:00:00 đến 23:59:59 UTC. Quá trình này có thể mất đến vài giờ, tùy thuộc vào tổng số file trong hàng đợi cũng như lượng dữ liệu bạn yêu cầu. Nếu Adapty đưa dữ liệu lịch sử vào lần tải lên đầu tiên, thời gian xử lý sẽ lâu hơn so với các lần tải lên hàng ngày sau đó. ## Thiết lập tích hợp Google Cloud Storage \{#set-up-google-cloud-storage-integration\} Bạn cần có khóa tài khoản dịch vụ Google Cloud hợp lệ với **quyền ghi**. Để tạo khóa, làm theo các bước trong phần [tạo thông tin xác thực](#create-google-cloud-storage-credentials). :::warning Bạn có thể dùng các bucket khác nhau với thông tin xác thực khác nhau cho sự kiện và lượt xem paywall. Tuy nhiên, nếu **một trong hai** bộ thông tin xác thực không hợp lệ, [**cả hai lần tải lên đều sẽ thất bại**](#troubleshooting). ::: Vào [**Integrations** -> **Google Cloud Storage**](https://app.adapty.io/integrations/google-cloud-storage), mở tab cần thiết (**Events** hoặc **Paywall visits**). Bật tích hợp. Tải lên file chứa **khóa tài khoản dịch vụ Google Cloud** của bạn. Chỉ định **bucket** và **folder** đích. Lưu các thay đổi. ### Cài đặt tùy chọn cho dữ liệu sự kiện \{#optional-settings-for-event-data\} Bạn có thể chỉ định những sự kiện nào được đưa vào báo cáo và đặt tên tùy chỉnh cho các sự kiện. Xem bài viết [events](events) để biết danh sách đầy đủ các sự kiện khả dụng. | Tên | Mặc định | Mô tả | | ------------------------------ | ----------------- | ----------- | | Exclude historical events | true | Loại trừ thông tin về các sự kiện xảy ra trước khi bạn tích hợp Adapty SDK vào ứng dụng. <br /> <br />Nếu nền tảng phân tích của bạn đã nhận các sự kiện gói đăng ký **trước khi** bạn bắt đầu dùng Adapty, tùy chọn này đảm bảo nền tảng không nhận các sự kiện trùng lặp. <Details summary="Ví dụ thực tế"><p>Một người dùng đã mua gói đăng ký hàng tháng vào ngày 10 tháng 1. Bản cập nhật ngày 1 tháng 3 của ứng dụng là bản đầu tiên tích hợp Adapty SDK. <br /> <br /> Nếu cài đặt này **bật**, báo cáo sẽ không bao gồm sự kiện "subscription started" từ tháng 1, cũng như sự kiện "subscription renewed" từ tháng 2. Nhưng **sẽ** bao gồm sự kiện "subscription renewed" từ ngày 10 tháng 3.</p> </Details> | | Include events without profile | false | Bao gồm các giao dịch không được liên kết với hồ sơ người dùng, hoặc không thể liên kết ngay với một người dùng cụ thể. Có thể bao gồm các giao dịch thực hiện trước khi cài Adapty SDK, hoặc các giao dịch nhận qua thông báo từ server. | | Send user attributes | false | Bao gồm [các thuộc tính người dùng tùy chỉnh](setting-user-attributes), chẳng hạn như dữ liệu người dùng và dữ liệu sử dụng ứng dụng. Chọn tùy chọn này nếu gói OneSignal của bạn hỗ trợ hơn 10 thẻ. Lưu ý rằng vượt quá giới hạn thẻ có thể gây ra lỗi. | ## Tạo thông tin xác thực Google Cloud Storage \{#create-google-cloud-storage-credentials\} Hướng dẫn này giúp bạn tạo các thông tin xác thực cần thiết trong Google Cloud Platform Console. Để Adapty có thể tải báo cáo dữ liệu thô lên bucket của bạn, cần có khóa tài khoản dịch vụ cùng với quyền ghi vào bucket tương ứng. Bằng cách cung cấp khóa tài khoản dịch vụ và cấp quyền ghi vào bucket, bạn cho phép Adapty chuyển báo cáo dữ liệu thô từ nền tảng của Adapty sang môi trường lưu trữ của bạn một cách an toàn và hiệu quả. :::warning Lưu ý rằng chúng tôi chỉ hỗ trợ xác thực bằng khóa HMAC của Service Account, nghĩa là Service Account HMAC key của bạn phải được gán các vai trò "Storage Object Viewer", "Storage Legacy Bucket Writer" và "Storage Object Creator" để truy cập đúng cách vào Google Cloud Storage. ::: 1. Đầu tiên, truy cập phần [IAM](https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts) trong tài khoản Google Cloud của bạn và chọn dự án phù hợp hoặc tạo dự án mới. 1. Tiếp theo, tạo tài khoản dịch vụ mới cho Adapty bằng cách nhấp vào nút "+ CREATE SERVICE ACCOUNT". 2. Điền vào các trường ở bước đầu tiên; quyền truy cập sẽ được cấp ở bước sau. Để tìm hiểu thêm về trang này, đọc tài liệu [tại đây](https://docs.cloud.google.com/iam/docs/service-accounts-create). 3. Để tạo và tải xuống [khóa JSON riêng tư](https://docs.cloud.google.com/iam/docs/keys-create-delete), điều hướng đến phần KEYS và nhấp vào nút "ADD KEY". 4. Trong phần DETAILS, tìm giá trị Email liên kết với tài khoản dịch vụ vừa tạo và sao chép lại. Thông tin này sẽ cần thiết cho các bước tiếp theo để xác thực tài khoản và cho phép ghi vào bucket. 5. Tiếp theo, vào trang [Buckets](https://console.cloud.google.com/storage/browser) của Google Cloud Storage, chọn bucket hiện có hoặc tạo bucket mới để lưu trữ báo cáo dữ liệu Sự kiện hoặc Lượt xem từ Adapty. Sau đó điều hướng đến phần PERMISSIONS và chọn tùy chọn [GRANT ACCESS](https://docs.cloud.google.com/identity/docs/how-to?hl=en). 6. Trong phần PERMISSIONS, nhập Email của tài khoản dịch vụ đã lấy ở bước năm, chọn vai trò Storage Object Creator. Cuối cùng, nhấp SAVE để áp dụng các thay đổi. Hãy ghi nhớ tên bucket để sử dụng sau này. ## Xuất dữ liệu thủ công \{#manual-data-export\} Ngoài tính năng tự động xuất dữ liệu sự kiện sang Google Cloud Storage, Adapty còn cung cấp chức năng xuất file thủ công. Với tính năng này, bạn có thể chọn khoảng thời gian cụ thể cho dữ liệu sự kiện và xuất thủ công vào bucket GCS của mình. Điều này giúp bạn kiểm soát tốt hơn dữ liệu cần xuất và thời điểm xuất. Khoảng thời gian được chỉ định sẽ dùng để xuất các sự kiện được tạo từ Ngày A 00:00:00 UTC đến Ngày B 23:59:59 UTC. ## Cấu trúc dữ liệu \{#data-structure\} Adapty sử dụng file `.csv` để xuất dữ liệu ở định dạng bảng. :::warning Nội dung sự kiện có thể tăng theo thời gian — khi chúng tôi hoặc các bên thứ ba mà chúng tôi hợp tác giới thiệu dữ liệu mới. Hãy đảm bảo code xử lý dữ liệu của bạn đủ linh hoạt và dựa vào các trường cụ thể, không phụ thuộc vào cấu trúc tổng thể. ::: ### Sự kiện \{#events\} Bạn có thể [chỉnh sửa](#optional-settings-for-event-data) danh sách sự kiện được đưa vào báo cáo của mình. :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: | Cột | Mô tả | |------|-----------| | **profile_id** | ID người dùng Adapty. | | **event_type** | Tên sự kiện viết thường. Tham khảo phần [Events](events) để biết các loại sự kiện. | | **event_datetime** | Ngày theo định dạng ISO 8601. | | **transaction_id** | Mã định danh duy nhất cho một giao dịch như mua hàng hoặc gia hạn. | | **original_transaction_id** | Mã định danh giao dịch của lần mua ban đầu. | | **subscription_expires_at** | Ngày hết hạn của gói đăng ký. Thường là trong tương lai. | | **environment** | Có thể là Sandbox hoặc Production. | | **revenue_usd** | Doanh thu tính bằng USD. Có thể trống. | | **proceeds_usd** | Tiền thu được tính bằng USD. Có thể trống. | | **net_revenue_usd** | Doanh thu thuần (thu nhập sau thuế) tính bằng USD. Có thể trống. | | **tax_amount_usd** | Số tiền khấu trừ cho thuế tính bằng USD. Có thể trống. | | **revenue_local** | Doanh thu theo tiền tệ địa phương. Có thể trống. | | **proceeds_local** | Tiền thu được theo tiền tệ địa phương. Có thể trống. | | **net_revenue_local** | Doanh thu thuần (thu nhập sau thuế) theo tiền tệ địa phương. Có thể trống. | | **tax_amount_local** | Số tiền khấu trừ cho thuế theo tiền tệ địa phương. Có thể trống. | | **customer_user_id** | ID người dùng do nhà phát triển đặt. Ví dụ: UUID người dùng, email hoặc ID bất kỳ. Null nếu bạn chưa thiết lập. | | **store** | Có thể là *app_store* hoặc *play_store*. | | **product_id** | ID sản phẩm trên Apple App Store, Google Play Store hoặc Stripe. | | **base_plan_id** | [ID gói cơ bản](https://support.google.com/googleplay/android-developer/answer/12154973) trên Google Play Store hoặc [ID giá](https://docs.stripe.com/products-prices/how-products-and-prices-work#use-products-and-prices) trên Stripe. | | **developer_id** | ID nhà phát triển (SDK) của paywall nơi giao dịch bắt nguồn. | | **ab_test_name** | Tên A/B test nơi giao dịch bắt nguồn. | | **ab_test_revision** | Phiên bản của A/B test nơi giao dịch bắt nguồn. | | **paywall_name** | Tên paywall nơi giao dịch bắt nguồn. | | **paywall_revision** | Phiên bản của paywall nơi giao dịch bắt nguồn. | | **profile_country** | Quốc gia của hồ sơ người dùng được Adapty xác định dựa trên IP. | | **install_date** | Ngày cài đặt theo định dạng ISO 8601. | | **idfv** | [identifierForVendor](https://developer.apple.com/documentation/uikit/uidevice/identifierforvendor) trên thiết bị iOS | | **idfa** | [advertisingIdentifier](https://developer.apple.com/documentation/adsupport/asidentifiermanager/advertisingidentifier) trên thiết bị iOS | | **advertising_id** | Advertising ID là mã duy nhất do Hệ điều hành Android gán, mà các nhà quảng cáo có thể dùng để nhận dạng thiết bị của người dùng | | **ip_address** | IP thiết bị (có thể là IPv4 hoặc IPv6, ưu tiên IPv4 khi có). Được cập nhật mỗi khi IP thiết bị thay đổi | | **cancellation_reason** | <p>Lý do người dùng hủy gói đăng ký.</p><p></p><p>Các giá trị có thể:</p><p>**iOS & Android** — *voluntarily_cancelled*, *billing_error*, *refund*</p><p>**Chỉ iOS** — *price_increase*, *product_was_not_available*, *unknown*, *upgraded*</p><p>**Chỉ Android** — *new_subscription_replace*, *cancelled_by_developer*</p> | | **android_app_set_id** | Một [AppSetId](https://developer.android.com/design-for-safety/privacy-sandbox/reference/adservices/appsetid/AppSetId) - ID duy nhất theo từng thiết bị, theo từng tài khoản nhà phát triển, có thể đặt lại bởi người dùng, dùng cho các trường hợp quảng cáo không phát sinh doanh thu. | | **android_id** | Trên Android 8.0 (API level 26) trở lên, đây là số 64-bit (biểu diễn dưới dạng chuỗi thập lục phân), duy nhất cho mỗi tổ hợp khóa ký ứng dụng, người dùng và thiết bị. Xem thêm tại [tài liệu dành cho nhà phát triển Android](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID). | | **device** | Tên model thiết bị hiển thị cho người dùng. | | **currency** | Mã tiền tệ 3 chữ cái (ISO-4217) của giao dịch. | | **store_country** | Quốc gia của hồ sơ người dùng được xác định bởi cửa hàng Apple/Google. | | **attribution_source** | Nguồn attribution. | | **attribution_network_user_id** | ID được nguồn attribution gán cho người dùng. | | **attribution_status** | Có thể là organic, non_organic hoặc unknown. | | **attribution_channel** | Tên kênh marketing. | | **attribution_campaign** | Tên chiến dịch marketing. | | **attribution_ad_group** | Nhóm quảng cáo attribution. | | **attribution_ad_set** | Bộ quảng cáo attribution. | | **attribution_creative** | Từ khóa sáng tạo attribution. | | **attributes** | JSON của [thuộc tính người dùng tùy chỉnh](setting-user-attributes#custom-user-attributes). Bao gồm mọi thuộc tính tùy chỉnh bạn đã thiết lập để gửi từ ứng dụng di động. Để gửi, bật tùy chọn **Send User Attributes** trong trang [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook). | | **integration_ids** | Tất cả ID tích hợp liên kết với một hồ sơ người dùng. Dạng Dictionary. Ví dụ: {'mixpanel_user_id': 'mixpanelUserId-test', 'facebook_anonymous_id': 'facebookAnonymousId-test'} | ### Lượt xem paywall \{#paywall-visits\} | Cột | Mô tả | | :-------------------- | :----------------------------------------------------------------------------------------------------------- | | **profile_id** | ID người dùng Adapty. | | **customer_user_id** | ID người dùng do nhà phát triển đặt. Ví dụ: UUID người dùng, email hoặc ID bất kỳ. Null nếu bạn chưa thiết lập. | | **profile_country** | Quốc gia của hồ sơ người dùng được xác định bởi cửa hàng Apple/Google. | | **install_date** | Ngày cài đặt theo định dạng ISO 8601. | | **store** | Có thể là *app_store* hoặc *play_store*. | | **paywall_showed_at** | Ngày paywall được hiển thị cho khách hàng. | | **developer_id** | ID nhà phát triển (SDK) của paywall nơi giao dịch bắt nguồn. | | **ab_test_name** | Tên A/B test nơi giao dịch bắt nguồn. | | **ab_test_revision** | Phiên bản của A/B test nơi giao dịch bắt nguồn. | | **paywall_name** | Tên paywall nơi giao dịch bắt nguồn. | | **paywall_revision** | Phiên bản của paywall nơi giao dịch bắt nguồn. | ## Xử lý sự cố \{#troubleshooting\} Adapty kiểm tra tính hợp lệ của khóa truy cập **trước khi** bắt đầu tải lên. Ngay cả khi chỉ một trong các khóa Google Cloud Storage không hợp lệ, Adapty **sẽ hủy toàn bộ quá trình tải lên** và báo lỗi. Để đảm bảo quá trình tải lên không bị gián đoạn, hãy thay thế khóa trước khi hết hạn. Nếu bạn cập nhật khóa cho **sự kiện**, đừng quên cập nhật khóa cho **lượt xem paywall**, và ngược lại. --- # File: webhook-event-types-and-fields --- --- title: "Các loại sự kiện và trường dữ liệu webhook" description: "" --- Adapty gửi webhook để phản hồi các sự kiện gói đăng ký. Phần này định nghĩa các loại sự kiện và dữ liệu có trong mỗi webhook. ## Các loại sự kiện Webhook \{#webhook-event-types\} Bạn có thể gửi tất cả các loại sự kiện đến webhook của mình hoặc chỉ chọn một số loại nhất định. Tham khảo [Event flows](event-flows) để tìm hiểu loại dữ liệu đầu vào cần mong đợi và cách xây dựng logic nghiệp vụ xung quanh đó. Bạn có thể tắt các loại sự kiện không cần thiết khi [thiết lập tích hợp Webhook](set-up-webhook-integration#configure-webhook-integration-in-the-adapty-dashboard). Tại đó, bạn cũng có thể thay thế ID sự kiện mặc định của Adapty bằng ID của riêng mình nếu cần. | Tên sự kiện | Mô tả | |:-----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | subscription_started | Kích hoạt khi người dùng bắt đầu gói đăng ký trả phí không có thời gian dùng thử, tức là bị tính phí ngay lập tức. | | subscription_renewed | Xảy ra khi gói đăng ký được gia hạn và người dùng bị tính phí. Sự kiện này bắt đầu từ lần thanh toán thứ hai, dù là gói đăng ký có hay không có dùng thử. | | subscription_renewal_cancelled | Người dùng đã tắt tính năng tự động gia hạn gói đăng ký. Người dùng vẫn có thể sử dụng các tính năng cao cấp cho đến khi kết thúc chu kỳ đăng ký đã thanh toán. | | subscription_renewal_reactivated | Kích hoạt khi người dùng bật lại tính năng tự động gia hạn gói đăng ký. | | subscription_expired | Kích hoạt khi gói đăng ký hết hạn hoàn toàn sau khi bị hủy. Ví dụ: nếu người dùng hủy gói đăng ký vào ngày 12 tháng 12 nhưng vẫn còn hiệu lực đến ngày 31 tháng 12, sự kiện sẽ được ghi nhận vào ngày 31 tháng 12 khi gói đăng ký hết hạn. | | subscription_paused | Xảy ra khi người dùng kích hoạt tính năng [tạm dừng gói đăng ký](https://developer.android.com/google/play/billing/lifecycle/subscriptions#pause) (chỉ dành cho Android). | | subscription_deferred | Kích hoạt khi giao dịch mua gói đăng ký được [hoãn lại](https://adapty.io/glossary/subscription-purchase-deferral/), cho phép người dùng trì hoãn việc thanh toán trong khi vẫn duy trì quyền truy cập các tính năng cao cấp. Tính năng này có sẵn thông qua Google Play Developer API và có thể dùng cho các bản dùng thử miễn phí hoặc hỗ trợ người dùng gặp khó khăn về tài chính. | | non_subscription_purchase | Bất kỳ sản phẩm mua một lần nào, chẳng hạn như quyền truy cập trọn đời hoặc các sản phẩm consumable như tiền trong game. | | trial_started | Kích hoạt khi người dùng bắt đầu gói đăng ký dùng thử. | | trial_converted | Xảy ra khi thời gian dùng thử kết thúc và người dùng bị tính phí (lần mua đầu tiên). Ví dụ: nếu người dùng có thời gian dùng thử đến ngày 14 tháng 1 nhưng bị tính phí vào ngày 7 tháng 1, sự kiện sẽ được ghi nhận vào ngày 7 tháng 1. | | trial_renewal_cancelled | Người dùng đã tắt tính năng tự động gia hạn trong thời gian dùng thử. Người dùng vẫn có thể sử dụng các tính năng cao cấp cho đến khi thời gian dùng thử kết thúc nhưng sẽ không bị tính phí hay chuyển sang gói đăng ký. | | trial_renewal_reactivated | Xảy ra khi người dùng bật lại tính năng tự động gia hạn trong thời gian dùng thử. | | trial_expired | Kích hoạt khi thời gian dùng thử kết thúc mà không chuyển sang gói đăng ký. | | entered_grace_period | Xảy ra khi một lần thanh toán thất bại và người dùng bước vào thời gian ân hạn (nếu được bật). Người dùng vẫn có thể truy cập các tính năng cao cấp trong thời gian này. | | billing_issue_detected | Kích hoạt khi xảy ra sự cố thanh toán trong quá trình thực hiện giao dịch (ví dụ: số dư thẻ không đủ). | | subscription_refunded | Kích hoạt khi gói đăng ký được hoàn tiền (ví dụ: do Apple Support xử lý). | | non_subscription_purchase_refunded | Kích hoạt khi một sản phẩm mua một lần được hoàn tiền. | | access_level_updated | Xảy ra khi mức độ truy cập của người dùng được cập nhật. | :::note `subscription_renewal_reactivated` mang **ID sản phẩm trước đó** — tức là sản phẩm đang hoạt động khi người dùng hủy — ngay cả khi người dùng sau đó kích hoạt lại bằng cách mua một sản phẩm khác. Apple giữ nguyên `original_transaction_id` trong toàn bộ chuỗi hủy → kích hoạt lại, vì vậy sự kiện này phản ánh sản phẩm ban đầu. Sản phẩm mới sẽ xuất hiện trong sự kiện `subscription_renewed` tiếp theo khi việc tính phí cho sản phẩm mới bắt đầu. ::: ## Cấu trúc sự kiện Webhook \{#webhook-event-structure\} Adapty chỉ gửi những sự kiện mà bạn đã chọn trong phần **Events names** của trang [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook). Các sự kiện webhook được tuần tự hóa dưới dạng JSON. Phần thân của yêu cầu `POST` gửi đến máy chủ của bạn sẽ chứa sự kiện đã được tuần tự hóa, được đóng gói trong cấu trúc dưới đây. Tất cả các sự kiện đều có cùng cấu trúc, nhưng các trường dữ liệu sẽ khác nhau tùy theo loại sự kiện, cửa hàng và cấu hình cụ thể của bạn. Thuộc tính người dùng là các [thuộc tính người dùng tùy chỉnh](setting-user-attributes#custom-user-attributes) mà bạn đã thiết lập, vì vậy chúng chứa những gì bạn đã cấu hình. Các trường dữ liệu attribution cũng giống nhau cho tất cả các loại sự kiện, tuy nhiên danh sách các attribution sẽ phụ thuộc vào nguồn attribution bạn sử dụng trong ứng dụng di động của mình. Xem bên dưới ví dụ về một sự kiện: ```json title="Json" showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": "UserIdInYourSystem", "idfv": "00000000-0000-0000-0000-000000000000", "idfa": "00000000-0000-0000-0000-000000000000", "advertising_id": "00000000-0000-0000-0000-000000000000", "profile_install_datetime": "2000-01-31T00:00:00.000000+0000", "user_agent": "ExampleUserAgent/1.0 (Device; OS Version) Browser/Engine", "email": "john.doe@company.com", "event_type": "subscription_started", "event_datetime": "2000-01-31T00:00:00.000000+0000", "event_properties": { "store": "play_store", "currency": "USD", "price_usd": 4.99, "profile_id": "00000000-0000-0000-0000-000000000000", "cohort_name": "All Users", "environment": "Production", "price_local": 4.99, "original_price_usd": 4.99, "original_price_local": 4.99, "discount_amount_usd": 0, "discount_amount_local": 0, "base_plan_id": "b1", "developer_id": "onboarding_placement", "ab_test_name": "onboarding_ab_test", "ab_test_revision": 1, "paywall_name": "UsedPaywall", "proceeds_usd": 4.2315, "variation_id": "00000000-0000-0000-0000-000000000000", "purchase_date": "2024-11-15T10:45:36.181000+0000", "store_country": "AR", "event_datetime": "2000-01-31T00:00:00.000000+0000", "proceeds_local": 4.2415, "tax_amount_usd": 0, "transaction_id": "0000000000000000", "net_revenue_usd": 4.2415, "profile_country": "AR", "paywall_revision": "1", "profile_event_id": "00000000-0000-0000-0000-000000000000", "tax_amount_local": 0, "net_revenue_local": 4.2415, "vendor_product_id": "onemonth_no_trial", "profile_ip_address": "10.10.1.1", "consecutive_payments": 1, "rate_after_first_year": false, "original_purchase_date": "2000-01-31T00:00:00.000000+0000", "original_transaction_id": "0000000000000000", "subscription_expires_at": "2000-01-31T00:00:00.000000+0000", "profile_has_access_level": true, "profile_total_revenue_usd": 4.99, "promotional_offer_id": null, "store_offer_category": null, "store_offer_discount_type": null }, "event_api_version": 1, "profiles_sharing_access_level": [{"profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": "UserIdInYourSystem"}], "attributions": { "appsflyer": { "ad_set": "Keywords 1.12", "status": "non_organic", "channel": "Google Ads", "ad_group": null, "campaign": "Social media influencers - Rest of the world", "creative": null, "created_at": "2000-01-31T00:00:00.000000+0000" } }, "user_attributes": {"Favourite_color": "Violet", "Pet_name": "Fluffy"}, "integration_ids": {"firebase_app_instance_id": "val1", "branch_id": "val2", "one_signal_player_id": "val3"}, "play_store_purchase_token": { "product_id": "product_123", "purchase_token": "token_abc_123", "is_subscription": true } } ``` ### Các trường của sự kiện \{#event-fields\} Các tham số sự kiện đều giống nhau cho tất cả các loại sự kiện. | **Trường** | **Kiểu** | **Mô tả** | |---|---|---| | **advertising_id** | UUID | ID quảng cáo (chỉ dành cho Android). | | **attributions** | JSON | [Dữ liệu attribution](webhook-event-types-and-fields#attributions). Được đính kèm nếu **Send Attribution** được bật trong [Webhook settings](https://app.adapty.io/integrations/customwebhook). | | **customer_user_id** | String | ID người dùng từ app của bạn (UUID, email, hoặc ID khác) nếu bạn đã thiết lập trong code app khi [xác định người dùng](ios-quickstart-identify). Nếu bạn không xác định người dùng trong code app hoặc người dùng cụ thể này là ẩn danh (chưa đăng nhập), trường này sẽ là `null`. | | **email** | String | Email của người dùng nếu bạn thiết lập bằng phương thức [`updateProfile`](setting-user-attributes) trong Adapty SDK hoặc khi tạo/cập nhật hồ sơ người dùng qua server-side API. Nếu bạn không truyền giá trị `email` vào SDK hoặc phương thức API, trường này sẽ là `null`. | | **event_api_version** | Integer | Phiên bản Adapty API (hiện tại: `1`). | | **event_datetime** | ISO 8601 | Thời điểm thực tế (nghiệp vụ) của sự kiện, chẳng hạn ngày mua hàng đối với giao dịch mua hoặc ngày hết hạn đối với sự kiện hết hạn — không phải thời điểm Adapty nhận hoặc gửi sự kiện. Định dạng [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) (ví dụ: `2020-07-10T15:00:00.000000+0000`). Xem ghi chú bên dưới về thứ tự sắp xếp. | | **event_properties** | JSON | [Thuộc tính sự kiện](webhook-event-types-and-fields#event-properties). | | **event_type** | String | Tên sự kiện theo định dạng Adapty. Xem [Loại sự kiện Webhook](webhook-event-types-and-fields#webhook-event-types) để biết danh sách đầy đủ. | | **idfa** | UUID | ID quảng cáo (chỉ dành cho Apple). **IDFA** trong hồ sơ người dùng trên [Adapty Dashboard](https://app.adapty.io/profiles/users). Có thể là `null` nếu không khả dụng do hạn chế theo dõi, chế độ trẻ em, hoặc cài đặt quyền riêng tư. | | **idfv** | UUID | Identifier for Vendors (IDFV), duy nhất theo nhà phát triển. **IDFV** trong hồ sơ người dùng trên [Adapty Dashboard](https://app.adapty.io/profiles/users). | | **integration_ids** | JSON | ID tích hợp người dùng nếu bạn thiết lập bằng phương thức `setIntegrationIdentifier` trong Adapty SDK hoặc khi tạo/cập nhật hồ sơ người dùng qua server-side API. `null` nếu không khả dụng hoặc các tích hợp bị tắt. | | **play_store_purchase_token** | JSON | [Token mua hàng Play Store](webhook-event-types-and-fields#play-store-purchase-token), được đính kèm nếu **Send Play Store purchase token** được bật trong [Webhook settings](https://app.adapty.io/integrations/customwebhook). | | **profile_id** | UUID | ID hồ sơ người dùng được Adapty tự động tạo cho mỗi hồ sơ. Một Apple/Google ID có thể được liên kết với nhiều profile ID khác nhau nếu bạn không xác định người dùng hoặc cho phép mua hàng trước khi đăng nhập. Tìm hiểu [thêm về cách Adapty hoạt động với hồ sơ cha/con](how-profiles-work#parent-and-inheritor-profiles). | | **profile_install_datetime** | ISO 8601 | Thời điểm cài đặt theo định dạng [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) (ví dụ: `2020-07-10T15:00:00.000000+0000`). | | **profiles_sharing_access_level** | JSON | Danh sách người dùng [chia sẻ mức độ truy cập](general#6-sharing-paid-access-between-user-accounts) ngoại trừ hồ sơ người dùng hiện tại. Nếu tính năng chia sẻ mức độ truy cập được bật cho app của bạn, danh sách này bao gồm các hồ sơ khác đã được sử dụng với cùng Apple/Google ID.<br/>Định dạng: <ul><li>**profile_id**: (UUID) Adapty ID</li><li>**customer_user_id**: (String) Customer User ID nếu được cung cấp</li></ul> | | **user_agent** | String | User-agent trình duyệt của thiết bị. | | **user_attributes** | JSON | Dữ liệu tùy chỉnh bạn có thể thiết lập để bổ sung thông tin cụ thể của app vào hồ sơ người dùng. Thường dùng để theo dõi tùy chọn của người dùng (ví dụ: giao diện, ngôn ngữ) hoặc các cờ hành vi (đã hoàn thành onboarding, mức độ sử dụng tính năng). <br/>Được định dạng dưới dạng các cặp key-value trong đó key là chuỗi và value có thể là chuỗi hoặc số (ví dụ: `{"Favourite_color": "Violet", "Pet_name": "Fluffy"}`). <br/>Bạn có thể thiết lập thuộc tính tùy chỉnh thủ công trong Adapty Dashboard cho từng hồ sơ người dùng riêng lẻ, theo lập trình bằng phương thức `updateProfile` trong Adapty SDK, hoặc qua server-side API khi tạo/cập nhật hồ sơ người dùng. <br/>Được đính kèm nếu **Send User Attributes** được bật trong [Webhook settings](https://app.adapty.io/integrations/customwebhook). <p>Trong khi các giá trị thuộc tính tùy chỉnh trong code app di động có thể được thiết lập dưới dạng float hoặc string, các thuộc tính nhận được qua server-side API hoặc import lịch sử có thể có định dạng khác. Trong trường hợp này, các giá trị boolean và integer sẽ được chuyển đổi thành float.</p> | :::note `event_datetime` phản ánh thời điểm một sự kiện xảy ra trong vòng đời gói đăng ký, chứ không phải thời điểm Adapty xử lý hay gửi nó. Vì vậy, các sự kiện có thể có cùng `event_datetime` hoặc đến không theo thứ tự thời gian. Ví dụ, một sự kiện `subscription_expired` có thể mang `event_datetime` sớm hơn một sự kiện `subscription_renewal_cancelled` mà Adapty gửi trước nó. Đừng dựa vào `event_datetime` để sắp xếp thứ tự các sự kiện. Thay vào đó, hãy sắp xếp sự kiện theo thời gian bạn nhận được, và loại bỏ trùng lặp bằng `profile_event_id` hoặc các transaction ID. ::: ### Attribution \{#attributions\} Để gửi dữ liệu attribution, hãy bật tùy chọn **Send Attribution** trong trang [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook). Nếu bạn đã bật tính năng gửi dữ liệu attribution và đã thiết lập [tích hợp attribution](attribution-integration), dữ liệu dưới đây sẽ được gửi kèm theo sự kiện cho mỗi nguồn. Cùng một dữ liệu attribution sẽ được gửi cho tất cả các loại sự kiện. ```json title="Json" showLineNumbers { "attributions": { "appsflyer": { "ad_set": "sample_ad_set_123", "status": "non_organic", "channel": "sample_channel", "ad_group": "sample_ad_group_456", "campaign": "sample_ios_campaign", "creative": "sample_creative_789", "created_at": "2000-01-31T00:00:00.000000+0000", "network_user_id": "0000000000000-0000000" } } } ``` | Tên trường | Kiểu dữ liệu | Mô tả | | :------------------ | :------------ | :------------------------------------------------- | | **ad_set** | String | Ad set attribution. | | **status** | String | Có thể là `organic`, `non_organic,` hoặc `unknown`. | | **channel** | String | Tên kênh marketing. | | **ad_group** | String | Ad group attribution. | | **campaign** | String | Tên chiến dịch marketing. | | **creative** | String | Từ khóa creative của attribution. | | **created_at** | ISO 8601 date | Ngày và giờ tạo bản ghi attribution. | | **network_user_id** | String | ID được nguồn attribution gán cho người dùng. | ### ID tích hợp \{#integration-ids\} Các ID tích hợp sau đây hiện được sử dụng trong các sự kiện: - `adjust_device_id` - `airbridge_device_id` - `amplitude_device_id` - `amplitude_user_id` - `appmetrica_device_id` - `appmetrica_profile_id` - `appsflyer_id` - `branch_id` - `facebook_anonymous_id` - `firebase_app_instance_id` - `mixpanel_user_id` - `pushwoosh_hwid` - `one_signal_player_id` - `one_signal_subscription_id` - `tenjin_analytics_installation_id` - `posthog_distinct_user_id` ### Mã token mua hàng trên Play Store \{#play-store-purchase-token\} Trường này chứa toàn bộ dữ liệu cần thiết để xác thực lại giao dịch khi cần. Nó chỉ được gửi khi tùy chọn **Send Play Store purchase token** được bật trong [cài đặt tích hợp Webhook](https://app.adapty.io/integrations/customwebhook). | Field | Type | Description | | :------------------ | :------ | :----------------------------------------------------------- | | **product_id** | String | Mã định danh duy nhất của sản phẩm (SKU) được mua trên Play Store. | | **purchase_token** | String | Token do Google Play tạo ra để xác định duy nhất giao dịch mua này. | | **is_subscription** | Boolean | Cho biết sản phẩm được mua là gói đăng ký (`true`) hay sản phẩm mua một lần (`false`). | ### Thuộc tính sự kiện \{#event-properties\} Thuộc tính sự kiện có thể khác nhau tùy theo loại sự kiện và thậm chí giữa các sự kiện cùng loại. Ví dụ, một sự kiện từ App Store sẽ không bao gồm các thuộc tính dành riêng cho Android như `base_plan_id`. Sự kiện [Access Level Updated](webhook-event-types-and-fields#for-access-level-updated-event) có các thuộc tính riêng biệt, vì vậy chúng tôi đã dành một mục riêng cho nó. Tương tự, chúng tôi cũng tách riêng [Thuộc tính sự kiện thuế và doanh thu bổ sung](webhook-event-types-and-fields#additional-tax-and-revenue-event-properties), vì chúng chỉ áp dụng cho một số loại sự kiện nhất định. #### Đối với hầu hết các loại sự kiện \{#for-most-event-types\} Thuộc tính sự kiện của hầu hết các loại sự kiện đều nhất quán (ngoại trừ sự kiện **Access Level Updated**, được mô tả trong phần riêng của nó). Dưới đây là bảng tổng hợp các thuộc tính và chỉ rõ chúng thuộc về sự kiện cụ thể nào. :::note Adapty chuyển đổi các loại tiền tệ khác sang USD theo tỷ giá hối đoái từ [currencylayer.com](https://currencylayer.com/) (cập nhật mỗi 8 giờ). Tỷ giá được **cố định tại thời điểm giao dịch** — những thay đổi sau đó không ảnh hưởng đến kết quả chuyển đổi. ::: | Trường | Kiểu | Mô tả | |:------------------------------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **ab_test_name** | String | Tên của [A/B test trên Adapty](ab-tests) mà giao dịch bắt nguồn từ đó. | | **ab_test_revision** | Integer | Phiên bản của A/B test mà giao dịch bắt nguồn từ đó. | | **base_plan_id** | String | [ID gói cơ bản](https://support.google.com/googleplay/android-developer/answer/12154973) trên Google Play Store hoặc [ID giá](https://docs.stripe.com/products-prices/how-products-and-prices-work#use-products-and-prices) trên Stripe. | | **cancellation_reason** | String | <p>Các lý do hủy có thể có: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `cancelled_by_developer`, `new_subscription_replace`, `upgraded`, `unknown`, `adapty_revoked`.</p><p>Xuất hiện trong các loại sự kiện sau:</p>`subscription_cancelled`, `subscription_refunded`, và `trial_cancelled`. | | **cohort_name** | String | Tên của [đối tượng](audience) đã xác định paywall nào được hiển thị cho người dùng. | | **consecutive_payments** | Integer | Số kỳ mà người dùng đã đăng ký liên tục không bị gián đoạn. Bao gồm kỳ hiện tại. | | **currency** | String | Đơn vị tiền tệ địa phương. | | **developer_id** | String | ID của [placement](placements) mà giao dịch bắt nguồn từ đó. | | **discount_amount_local** | Float | Mức chiết khấu áp dụng cho giao dịch: giá tiêu chuẩn trừ đi số tiền thực tế được tính, trước khi Apple/Google khấu trừ phần của họ, tính theo tiền tệ địa phương. `0` với giao dịch mua giá đầy đủ. Với bản dùng thử miễn phí, bằng giá tiêu chuẩn đầy đủ (`original_price_local`) vì không tính phí gì. `null` khi có ưu đãi áp dụng nhưng không biết giá tiêu chuẩn (xem `original_price_local`). Luôn là `null` với ưu đãi trả trước một lần của App Store: khoản thanh toán trả trước một lần bao gồm nhiều kỳ thanh toán nên không thể so sánh với giá tiêu chuẩn theo kỳ. | | **discount_amount_usd** | Float | Giá trị của `discount_amount_local` tính theo USD. | | **environment** | String | Các giá trị có thể có là `Sandbox` hoặc `Production`. | | **event_datetime** | ISO 8601 date | Ngày và giờ xảy ra sự kiện. Giống với giá trị ở cấp gốc của sự kiện. | | **original_price_local** | Float | Giá tiêu chuẩn, chưa chiết khấu của sản phẩm trước khi Apple/Google khấu trừ phần của họ, tính theo tiền tệ địa phương. Với gói đăng ký, đây là giá gia hạn. Bằng `price_local` với giao dịch mua giá đầy đủ và luôn bằng `price_local` với sản phẩm mua một lần vì cửa hàng không báo cáo giá tiêu chuẩn riêng cho chúng. `null` với giao dịch mua có chiết khấu khi cửa hàng không báo cáo giá tiêu chuẩn đáng tin cậy (ví dụ: tự gia hạn bị tắt, lần gia hạn vẫn có ưu đãi, hoặc đang chờ thay đổi sản phẩm). | | **original_price_usd** | Float | Giống `original_price_local`, tính theo USD. | | **original_purchase_date** | ISO 8601 date | Với gói đăng ký tự động gia hạn, giao dịch mua gốc là giao dịch đầu tiên trong chuỗi, ID của nó được gọi là original transaction ID liên kết chuỗi gia hạn; các giao dịch sau là phần mở rộng của nó. Ngày mua gốc là ngày và giờ của giao dịch đầu tiên này. | | **original_transaction_id** | String | <p>Với gói đăng ký tự động gia hạn, đây là original transaction ID liên kết chuỗi gia hạn. Giao dịch gốc là giao dịch đầu tiên trong chuỗi; các giao dịch sau là phần mở rộng của nó.</p><p>Nếu không có phần mở rộng, `original_transaction_id` trùng với store_transaction_id.</p> | | **paywall_name** | String | Tên của paywall mà giao dịch bắt nguồn từ đó. | | **paywall_revision** | String | Phiên bản của paywall mà giao dịch bắt nguồn từ đó. Giá trị mặc định là 1. | | **price_local** | Float | Số tiền được tính cho giao dịch trước khi Apple/Google khấu trừ phần của họ, tính theo tiền tệ địa phương. `null` với bản dùng thử miễn phí vì không tính phí gì. | | **price_usd** | Float | Số tiền được tính cho giao dịch trước khi Apple/Google khấu trừ phần của họ, tính theo USD. `null` với bản dùng thử miễn phí vì không tính phí gì. | | **profile_country** | String | Được Adapty xác định dựa trên IP của hồ sơ người dùng. | | **profile_event_id** | UUID | ID sự kiện duy nhất có thể dùng để loại trùng lặp. | | **profile_has_access_level** | Boolean | Giá trị boolean cho biết hồ sơ người dùng có mức độ truy cập đang hoạt động hay không. | | **profile_id** | UUID | ID hồ sơ người dùng do Adapty tạo ra. Giống với giá trị ở cấp gốc của sự kiện. | | **profile_ip_address** | String | IP của hồ sơ người dùng (có thể là IPv4 hoặc IPv6, ưu tiên IPv4 khi có). `null` nếu **Collect users' IP addresses** bị tắt trong [cài đặt ứng dụng](https://app.adapty.io/settings/general). | | **profile_total_revenue_usd** | Float | Tổng doanh thu của hồ sơ người dùng sau khi đã trừ đi các khoản hoàn tiền. | | **promotional_offer_id** | String | ID Adapty của [ưu đãi](offers) được sử dụng. Bạn đặt ID này khi tạo ưu đãi trong dashboard. | | **purchase_date** | ISO 8601 date | Ngày và giờ mua sản phẩm. | | **rate_after_first_year** | Boolean | Boolean cho biết gói đăng ký đủ điều kiện để được giảm tỷ lệ hoa hồng (thường là 15%) sau một năm gia hạn liên tục. Tỷ lệ hoa hồng thay đổi tùy theo điều kiện chương trình và quốc gia. Xem [Hoa hồng cửa hàng và thuế](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue) để biết thêm chi tiết. | | **store** | String | Cửa hàng nơi sản phẩm được mua. Các giá trị tiêu chuẩn: **app_store**, **play_store**, **stripe**, **paddle**. <br/>Nếu bạn thiết lập [giao dịch cửa hàng tùy chỉnh](api-adapty/operations/setTransaction) qua API phía máy chủ, giá trị từ tham số **store** sẽ được sử dụng. | | **store_country** | String | Quốc gia do cửa hàng ứng dụng gửi cho chúng tôi. | | **store_offer_category** | String | Danh mục ưu đãi được áp dụng. Các giá trị có thể có là `introductory`, `promotional`, `winback`. | | **store_offer_discount_type** | String | Loại ưu đãi được áp dụng. Các giá trị có thể có là `free_trial`, `pay_as_you_go`, và `pay_up_front`. | | **store_offer_number_of_periods** | Integer | Số kỳ thanh toán cơ bản mà ưu đãi chiết khấu (1 kỳ trở lên). Chỉ xuất hiện khi có ưu đãi áp dụng. `null` với ưu đãi trả trước một lần của App Store và khi cửa hàng không báo cáo thời hạn ưu đãi. | | **subscription_expires_at** | ISO 8601 date | Ngày hết hạn của gói đăng ký. Thường là trong tương lai. | | **transaction_id** | String | Mã định danh duy nhất của giao dịch. | | **trial_duration** | String | Thời hạn của giai đoạn dùng thử tính bằng ngày. Được gửi theo định dạng "{} days", ví dụ "7 days". Chỉ xuất hiện trong các loại sự kiện liên quan đến dùng thử: `trial_started`, `trial_converted`, `trial_cancelled`. | | **variation_id** | UUID | ID duy nhất của paywall nơi thực hiện giao dịch mua. | | **vendor_product_id** | String | <p>ID sản phẩm trên Apple App Store, Google Play Store, hoặc Stripe.</p><p>Nếu quyền truy cập được cấp mà không có giao dịch cửa hàng thực, `vendor_product_id` sẽ là một trong các giá trị:</p><ul><li>`adapty_server_side_product` — được cấp qua [API phía máy chủ](api-adapty/operations/grantAccessLevel).</li><li>`adapty_dashboard_product` — [được cấp thủ công](give-access-level-to-specific-customer) trong Adapty Dashboard.</li><li>`adapty_promotion` — phiên bản cũ.</li></ul> | #### Thuộc tính sự kiện về thuế và doanh thu bổ sung \{#additional-tax-and-revenue-event-properties\} Các thuộc tính sự kiện liên quan đến thuế và doanh thu dưới đây là các trường bổ sung chỉ áp dụng cho một số loại sự kiện nhất định. Điều này có nghĩa là các loại sự kiện được liệt kê bao gồm [Thuộc tính sự kiện cho hầu hết các loại sự kiện](webhook-event-types-and-fields#for-most-event-types), cùng với các trường bổ sung được liệt kê bên dưới. Các loại sự kiện có thuộc tính sự kiện về thuế và doanh thu: - `subscription_renewed` - `subscription_initial_purchase` (còn được gọi là `subscription_started` — cùng một sự kiện) - `subscription_refunded` - `non_subscription_purchase` | Field | Type | Description | | :-------------------- | :---- | :----------------------------------------------------------- | | **net_revenue_local** | Float | Doanh thu thuần (thu nhập sau khi trừ phần của Apple/Google và thuế) tính theo tiền tệ địa phương. | | **net_revenue_usd** | Float | Doanh thu thuần (thu nhập sau khi trừ phần của Apple/Google và thuế) tính theo USD. | | **proceeds_local** | Float | Giá sản phẩm sau khi trừ phần của Apple/Google tính theo tiền tệ địa phương. | | **proceeds_usd** | Float | Giá sản phẩm sau khi trừ phần của Apple/Google. | | **tax_amount_local** | Float | Số tiền thuế bị khấu trừ tính theo tiền tệ địa phương. | | **tax_amount_usd** | Float | Số tiền thuế bị khấu trừ tính theo USD. | #### Ví dụ payload `non_subscription_purchase` \{#non-subscription-purchase-example-payload\} `non_subscription_purchase` có cùng cấu trúc với các sự kiện gói đăng ký nhưng phản ánh một lần mua một lần hoặc consumable. Các trường chỉ dành cho gói đăng ký không áp dụng: `cancellation_reason`, `will_renew`, `is_in_grace_period`, `is_refund`, `is_lifetime`, và `trial_duration` đều không có. `subscription_expires_at` có mặt nhưng là `null`. Các trường thuế và doanh thu (`net_revenue_*`, `proceeds_*`, `tax_amount_*`) được bao gồm. <details> <summary>Ví dụ payload (nhấn để mở rộng)</summary> ```json title="Json" showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": "UserIdInYourSystem", "event_type": "non_subscription_purchase", "event_datetime": "2000-01-31T00:00:00.000000+0000", "event_properties": { "store": "app_store", "currency": "USD", "price_usd": 4.99, "price_local": 4.99, "original_price_usd": 4.99, "original_price_local": 4.99, "discount_amount_usd": 0, "discount_amount_local": 0, "proceeds_usd": 4.2415, "proceeds_local": 4.2415, "net_revenue_usd": 4.2415, "net_revenue_local": 4.2415, "tax_amount_usd": 0, "tax_amount_local": 0, "profile_id": "00000000-0000-0000-0000-000000000000", "environment": "Production", "vendor_product_id": "100coins", "transaction_id": "0000000000000000", "original_transaction_id": "0000000000000000", "purchase_date": "2024-11-15T10:45:36.181000+0000", "original_purchase_date": "2024-11-15T10:45:36.181000+0000", "subscription_expires_at": null, "store_country": "US", "profile_country": "US", "profile_ip_address": "10.10.1.1", "profile_has_access_level": false, "profile_total_revenue_usd": 4.99, "consecutive_payments": 1, "rate_after_first_year": false, "profile_event_id": "00000000-0000-0000-0000-000000000000" }, "event_api_version": 1 } ``` </details> #### Đối với sự kiện Access Level Updated \{#for-access-level-updated-event\} Sự kiện **Access Level Updated** là một loại sự kiện webhook đặc biệt, chỉ được tạo ra khi tích hợp Webhook đang hoạt động và loại sự kiện này được bật. Nếu được bật, sự kiện sẽ được gửi đến Webhook đã cấu hình và hiển thị trong **Event Feed**. Nếu không được bật, sự kiện sẽ không được tạo. Nếu bạn đã bật [chia sẻ mức độ truy cập](general#6-sharing-paid-access-between-user-accounts), sự kiện **access level updated** sẽ được gửi cho tất cả các hồ sơ người dùng đang chia sẻ mức độ truy cập đó. :::tip Sử dụng sự kiện này để cập nhật mức độ truy cập của người dùng trong cơ sở dữ liệu, cấp hoặc thu hồi các tính năng cao cấp trên backend, và đồng bộ quyền truy cập trên các thiết bị hoặc nền tảng khác nhau. ::: | Thuộc tính | Kiểu | Mô tả | | ---------------------------------- | ------------- | ------------------------------------------------------------ | | **ab_test_name** | String | Tên A/B test nơi giao dịch bắt nguồn. | | **access_level_id** | String | ID của mức độ truy cập. | | **activated_at** | ISO 8601 date | Ngày và giờ khi quyền truy cập được kích hoạt lần gần nhất. | | **active_introductory_offer_type** | String | Loại ưu đãi giới thiệu được áp dụng. Các giá trị có thể là `free_trial`, `pay_as_you_go`, và `pay_up_front`. | | **active_promotional_offer_id** | String | ID của ưu đãi như được chỉ định trong phần Product của Adapty Dashboard | | **active_promotional_offer_type** | String | Loại ưu đãi được áp dụng. Các giá trị có thể là `free_trial`, `pay_as_you_go`, và `pay_up_front`. | | **base_plan_id** | String | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) trong Google Play Store hoặc [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#use-products-and-prices) trong Stripe. | | **billing_issue_detected_at** | ISO 8601 date | Ngày và giờ xảy ra sự cố thanh toán. | | **cancellation_reason** | String | Các lý do hủy có thể có: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `cancelled_by_developer`, `new_subscription_replace`, `upgraded`, `unknown`, `adapty_revoked`. | | **cohort_name** | String | Tên đối tượng mà hồ sơ người dùng thuộc về. | | **currency** | String | Đơn vị tiền tệ địa phương (mặc định là USD). | | **developer_id** | String | ID của placement nơi giao dịch bắt nguồn. | | **environment** | String | Các giá trị có thể là `Sandbox` hoặc `Production`. | | **event_datetime** | ISO 8601 date | Ngày và giờ của sự kiện. | | **expires_at** | ISO 8601 date | Ngày và giờ khi quyền truy cập hết hạn. | | **is_active** | Boolean | Boolean cho biết mức độ truy cập có đang hoạt động hay không. | | **is_in_grace_period** | Boolean | Boolean cho biết hồ sơ người dùng có đang trong thời gian ân hạn hay không. | | **is_lifetime** | Boolean | Boolean cho biết mức độ truy cập có phải là trọn đời hay không. | | **is_refund** | Boolean | Boolean cho biết giao dịch có phải là hoàn tiền hay không. | | **original_purchase_date** | ISO 8601 date | Đối với các gói đăng ký định kỳ, giao dịch mua ban đầu là giao dịch đầu tiên trong chuỗi, ID của nó được gọi là original transaction ID liên kết chuỗi gia hạn; các giao dịch sau là phần mở rộng của nó. Ngày mua ban đầu là ngày và giờ của giao dịch đầu tiên này. | | **original_transaction_id** | String | <p>Đối với các gói đăng ký định kỳ, đây là original transaction ID liên kết chuỗi gia hạn. Giao dịch ban đầu là giao dịch đầu tiên trong chuỗi; các giao dịch sau là phần mở rộng của nó.</p><p>Nếu không có phần mở rộng nào, `original_transaction_id` trùng với store_transaction_id.</p>Mã định danh giao dịch của lần mua ban đầu. | | **paywall_name** | String | Tên paywall nơi giao dịch bắt nguồn. | | **paywall_revision** | String | Phiên bản của paywall nơi giao dịch bắt nguồn. Giá trị mặc định là 1. | | **profile_country** | String | Được xác định bởi Adapty, dựa trên IP của hồ sơ người dùng. | | **profile_event_id** | UUID | ID sự kiện duy nhất có thể dùng để loại trùng lặp. | | **profile_has_access_level** | Boolean | Boolean cho biết hồ sơ người dùng có mức độ truy cập đang hoạt động hay không. | | **profile_id** | UUID | ID hồ sơ người dùng nội bộ của Adapty. | | **profile_ip_address** | String | IP của hồ sơ người dùng (có thể là IPv4 hoặc IPv6, ưu tiên IPv4 khi có). `null` nếu **Collect users' IP addresses** bị tắt trong [cài đặt ứng dụng](https://app.adapty.io/settings/general). | | **profile_total_revenue_usd** | Float | Tổng doanh thu của hồ sơ người dùng, bao gồm cả hoàn tiền. | | **purchase_date** | ISO 8601 date | Ngày và giờ mua sản phẩm. | | **renewed_at** | ISO 8601 date | Ngày và giờ khi quyền truy cập sẽ được gia hạn. | | **starts_at** | ISO 8601 date | Ngày và giờ khi mức độ truy cập bắt đầu. | | **store** | String | Cửa hàng nơi sản phẩm được mua. Các giá trị tiêu chuẩn: **app_store**, **play_store**, **stripe**, **paddle**. <br/>Nếu bạn thiết lập [giao dịch cửa hàng tùy chỉnh](api-adapty/operations/setTransaction) bằng API phía máy chủ, giá trị từ tham số **store** sẽ được sử dụng. | | **store_country** | String | Quốc gia được app store gửi đến Adapty. | | **subscription_expires_at** | ISO 8601 date | Ngày hết hạn của gói đăng ký. | | **transaction_id** | String | Mã định danh duy nhất cho một giao dịch. | | **trial_duration** | String | Thời hạn của giai đoạn dùng thử tính theo ngày (ví dụ: "7 days"). | | **variation_id** | UUID | Mã định danh của một biến thể, dùng để gán các giao dịch mua cho paywall này. | | **vendor_product_id** | String | <p>ID sản phẩm trong cửa hàng (Apple/Google/Stripe).</p><p>Nếu quyền truy cập được cấp mà không có giao dịch cửa hàng thực, `vendor_product_id` sẽ là một trong các giá trị sau:</p><ul><li>`adapty_server_side_product` — được cấp qua [API phía máy chủ](api-adapty/operations/grantAccessLevel).</li><li>`adapty_dashboard_product` — [được cấp thủ công](give-access-level-to-specific-customer) trong Adapty Dashboard.</li><li>`adapty_promotion` — cũ.</li></ul> | | **will_renew** | Boolean | Cho biết mức độ truy cập trả phí có được gia hạn hay không. | :::warning Lưu ý rằng cấu trúc này có thể mở rộng theo thời gian — với dữ liệu mới được chúng tôi hoặc các bên thứ ba mà chúng tôi hợp tác giới thiệu. Hãy đảm bảo rằng code xử lý cấu trúc này đủ linh hoạt và chỉ phụ thuộc vào các trường cụ thể thay vì toàn bộ cấu trúc. ::: --- # File: set-up-webhook-integration --- --- title: "Thiết lập tích hợp webhook" description: "Thiết lập tích hợp webhook trong Adapty để tự động hóa việc theo dõi sự kiện." --- [Tích hợp webhook](webhook) của Adapty bao gồm các bước sau: <img src="/assets/shared/img/webhook-setup.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '300px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <p> </p> 1. **Bạn thiết lập endpoint của mình:** 1. Đảm bảo server của bạn có thể xử lý các yêu cầu từ Adapty với header **Content-Type** được đặt thành `application/json`. 2. Cấu hình server để nhận yêu cầu xác minh từ Adapty và phản hồi với bất kỳ trạng thái `2xx` nào kèm theo nội dung JSON. 3. [Xử lý các sự kiện gói đăng ký](#subscription-events) sau khi kết nối được xác minh. 2. **Bạn cấu hình và bật tích hợp webhook** trong [Adapty Dashboard](#configure-webhook-integration-in-the-adapty-dashboard). Bạn cũng có thể [ánh xạ các sự kiện Adapty sang tên sự kiện tùy chỉnh](#configure-webhook-integration-in-the-adapty-dashboard). Chúng tôi khuyến nghị kiểm tra trong môi trường **Sandbox** trước khi chuyển sang môi trường production. 3. **Adapty gửi yêu cầu xác minh** đến server của bạn. 4. **Server của bạn phản hồi** với trạng thái `2XX` và nội dung JSON. 5. **Sau khi Adapty nhận được phản hồi hợp lệ, hệ thống bắt đầu gửi các sự kiện gói đăng ký.** ## Thiết lập server để xử lý yêu cầu từ Adapty \{#set-up-your-server-to-process-adapty-requests\} Adapty sẽ gửi đến endpoint webhook của bạn 2 loại yêu cầu: 1. [Yêu cầu xác minh](#verification-request): yêu cầu ban đầu để xác minh kết nối đã được thiết lập đúng cách. Yêu cầu này sẽ không chứa bất kỳ sự kiện nào và sẽ được gửi ngay khi bạn nhấn nút **Save** trong phần tích hợp Webhook của Adapty Dashboard. Để xác nhận endpoint của bạn đã nhận thành công yêu cầu xác minh, endpoint cần phản hồi với thông báo xác minh. 2. [Sự kiện gói đăng ký](#subscription-events): yêu cầu tiêu chuẩn mà server Adapty gửi mỗi khi có sự kiện được tạo. Server của bạn không cần phản hồi với bất kỳ nội dung cụ thể nào. Điều duy nhất server Adapty cần là nhận được phản hồi HTTP mã 200 tiêu chuẩn khi nhận tin nhắn thành công. ### Yêu cầu xác minh \{#verification-request\} Sau khi bạn bật tích hợp webhook trong Adapty Dashboard, Adapty sẽ gửi một yêu cầu POST xác minh chứa một đối tượng JSON rỗng `{}` làm nội dung. Thiết lập endpoint của bạn với **Content-Type header** là `application/json`, tức là endpoint server của bạn cần được cấu hình để nhận các yêu cầu webhook với payload định dạng JSON. Server của bạn phải phản hồi với mã trạng thái 2xx và gửi bất kỳ phản hồi JSON hợp lệ nào, ví dụ: ```json title="Json" {} ``` Sau khi Adapty nhận được phản hồi xác minh đúng định dạng với mã trạng thái 2xx, tích hợp webhook Adapty của bạn đã được cấu hình hoàn chỉnh. ### Sự kiện gói đăng ký \{#subscription-events\} Các sự kiện gói đăng ký được gửi với header **Content-Type** được đặt thành `application/json` và chứa dữ liệu sự kiện ở định dạng JSON. Để biết các loại sự kiện và cấu trúc yêu cầu, xem [Loại và trường sự kiện Webhook](webhook-event-types-and-fields). ## Cấu hình tích hợp webhook trong Adapty Dashboard \{#configure-webhook-integration-in-the-adapty-dashboard\} Trong Adapty, bạn có thể cấu hình các flow riêng biệt cho sự kiện production và sự kiện kiểm thử nhận từ môi trường sandbox của Apple, Stripe hoặc tài khoản thử nghiệm Google. :::tip Adapty hỗ trợ một URL webhook cho mỗi môi trường (production và sandbox). Để chuyển sự kiện đến nhiều dịch vụ, hãy trỏ webhook vào backend của bạn rồi phân phối từ đó. ::: Đối với sự kiện production, sử dụng trường **Production endpoint URL** để chỉ định URL mà các callback sẽ được gửi đến. Ngoài ra, hãy cấu hình trường **Authorization header value for production endpoint** — header này giúp server của bạn xác thực các sự kiện từ Adapty. Lưu ý rằng chúng tôi sẽ sử dụng giá trị được chỉ định trong trường **Authorization header value for production endpoint** làm header `Authorization` chính xác như đã nhập, không có bất kỳ thay đổi hay bổ sung nào. Đối với sự kiện kiểm thử, hãy sử dụng các trường **Sandbox endpoint URL** và **Authorization header value for sandbox endpoint** tương ứng. Để thiết lập tích hợp webhook: 1. Mở [Integrations -> Webhook](https://app.adapty.io/integrations/customwebhook) trong Adapty Dashboard của bạn. <img src="/assets/shared/img/webhook_integration.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Bật toggle để khởi động tích hợp. 4. Điền vào các trường tích hợp: | Trường | Mô tả | | ------------------------------------------------------ | ------------------------------------------------------------ | | **Production endpoint URL** | URL mà Adapty dùng để gửi các yêu cầu HTTP POST cho sự kiện trong môi trường production. | | **Authorization header value for production endpoint** | <p>Header mà server của bạn sẽ dùng để xác thực các yêu cầu từ Adapty trong môi trường production. Lưu ý rằng chúng tôi sẽ sử dụng giá trị được chỉ định trong trường này làm header `Authorization` chính xác như đã nhập, không có bất kỳ thay đổi hay bổ sung nào.</p><p></p><p>Mặc dù không bắt buộc, nhưng rất khuyến khích thiết lập để tăng cường bảo mật.</p> | Ngoài ra, để phục vụ nhu cầu kiểm thử trong môi trường sandbox, có thêm hai trường khác: | Trường kiểm thử | Mô tả | | --------------------------------------------------- | ------------------------------------------------------------ | | **Sandbox endpoint URL** | URL mà Adapty dùng để gửi các yêu cầu HTTP POST cho sự kiện trong môi trường sandbox. | | **Authorization header value for sandbox endpoint** | <p>Header mà server của bạn sẽ dùng để xác thực các yêu cầu từ Adapty trong quá trình kiểm thử ở môi trường sandbox. Lưu ý rằng chúng tôi sẽ sử dụng giá trị được chỉ định trong trường này làm header `Authorization` chính xác như đã nhập, không có bất kỳ thay đổi hay bổ sung nào.</p><p></p><p>Mặc dù không bắt buộc, nhưng rất khuyến khích thiết lập để tăng cường bảo mật.</p> | 4. (Tùy chọn) Chọn các sự kiện bạn muốn nhận và ánh xạ tên của chúng. Xem [Event flows](event-flows) để biết những sự kiện nào được kích hoạt trong các tình huống khác nhau. Nếu ID sự kiện của bạn khác với ID được dùng trong Adapty, hãy giữ nguyên ID trong hệ thống của bạn và thay thế các ID sự kiện mặc định của Adapty bằng ID của bạn trong phần **Events names** của trang [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook). ID sự kiện có thể là bất kỳ chuỗi nào; chỉ cần đảm bảo ID sự kiện trong server xử lý webhook của bạn trùng khớp với ID bạn đã nhập trong Adapty Dashboard. Bạn không thể để trống ID sự kiện cho các sự kiện đã được bật. <img src="/assets/shared/img/86942b8-event_names_renaming.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Các trường và tùy chọn bổ sung không bắt buộc; hãy sử dụng khi cần: | Cài đặt | Mô tả | | :--------------------------------- | :----------------------------------------------------------- | | **Send Trial Price** | Khi bật, Adapty sẽ bao gồm giá gói đăng ký trong các trường `price_local` và `price_usd` cho sự kiện **Trial Started**. | | **Exclude Historical Events** | Chọn để loại trừ các sự kiện xảy ra trước khi người dùng cài đặt ứng dụng có tích hợp Adapty SDK. Điều này giúp tránh trùng lặp sự kiện và đảm bảo báo cáo chính xác. Ví dụ: nếu người dùng kích hoạt gói đăng ký hàng tháng vào ngày 10 tháng 1 và cập nhật ứng dụng với Adapty SDK vào ngày 6 tháng 3, Adapty sẽ bỏ qua các sự kiện trước ngày 6 tháng 3 và giữ lại các sự kiện sau đó. | | **Send user attributes** | Bật tùy chọn này để gửi các thuộc tính dành riêng cho người dùng, chẳng hạn như tùy chọn ngôn ngữ. Các thuộc tính này sẽ xuất hiện trong trường `user_attributes`. Xem [Trường sự kiện](webhook-event-types-and-fields#event-fields) để biết thêm thông tin. | | **Send attribution** | Bật tùy chọn này để bao gồm thông tin attribution (ví dụ: dữ liệu AppsFlyer) trong trường `attributions`. Tham khảo phần [Dữ liệu Attribution](webhook-event-types-and-fields#attributions) để biết chi tiết. | | **Send Play Store purchase token** | Bật tùy chọn này để nhận token Play Store cần thiết cho việc xác thực lại giao dịch mua, nếu cần. Khi bật, tham số `play_store_purchase_token` sẽ được thêm vào sự kiện. Để biết chi tiết về nội dung của nó, tham khảo phần [Play Store purchase token](webhook-event-types-and-fields#play-store-purchase-token). | 6. Nhớ nhấn nút **Save** để xác nhận các thay đổi. Ngay khi bạn nhấn nút **Save**, Adapty sẽ gửi yêu cầu xác minh và chờ phản hồi xác minh từ server của bạn. ### Chọn sự kiện cần gửi và ánh xạ tên sự kiện \{#choose-events-to-send-and-map-event-names\} Chọn các sự kiện bạn muốn nhận trên server bằng cách bật toggle bên cạnh sự kiện đó. Nếu tên sự kiện của bạn khác với tên được dùng trong Adapty và bạn cần giữ nguyên tên của mình, bạn có thể thiết lập ánh xạ bằng cách thay thế tên sự kiện mặc định của Adapty bằng tên của bạn trong phần **Events names** của trang [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook). <img src="/assets/shared/img/86942b8-event_names_renaming.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Tên sự kiện có thể là bất kỳ chuỗi nào. Bạn không thể để trống các trường cho sự kiện đã được bật. Nếu bạn vô tình xóa tên sự kiện Adapty, bạn luôn có thể sao chép tên từ chủ đề [Sự kiện gửi đến tích hợp bên thứ ba](events). ## Xử lý sự kiện webhook \{#handle-webhook-events\} Webhook thường được gửi trong vòng 5 đến 60 giây sau khi sự kiện xảy ra. Tuy nhiên, các sự kiện hủy có thể mất đến 2 giờ để được gửi sau khi người dùng hủy gói đăng ký của họ. Nếu mã trạng thái phản hồi của server nằm ngoài khoảng 200-404, Adapty sẽ thử gửi lại với backoff theo cấp số nhân. Lần thử lại đầu tiên xảy ra khoảng **1 phút** sau lần thất bại đầu tiên, tăng gấp đôi sau mỗi lần tiếp theo — tối đa 9 lần thử trong vòng 24 giờ. Chúng tôi khuyến nghị bạn thiết lập webhook chỉ thực hiện các xác thực cơ bản đối với nội dung sự kiện từ Adapty trước khi phản hồi. Nếu server của bạn không thể xử lý sự kiện và bạn không muốn Adapty thử lại, hãy sử dụng mã trạng thái trong khoảng 200-404. Ngoài ra, hãy xử lý các tác vụ tốn thời gian một cách bất đồng bộ và phản hồi Adapty nhanh chóng. Nếu Adapty không nhận được phản hồi trong vòng 10 giây, hệ thống sẽ coi đó là lần thất bại và sẽ thử lại. --- # File: test-webhook --- --- title: "Kiểm tra tích hợp webhook" description: "Kiểm tra tích hợp webhook trong Adapty để tự động theo dõi sự kiện gói đăng ký." --- Sau khi thiết lập xong tích hợp, đã đến lúc kiểm tra. Bạn có thể kiểm tra cả tích hợp sandbox lẫn production. Chúng tôi khuyến nghị bắt đầu với môi trường sandbox và xác thực tối đa trên đó: - Các sự kiện được gửi đi và chuyển phát thành công. - Bạn đã thiết lập đúng các tùy chọn cho sự kiện lịch sử, giá gói đăng ký cho sự kiện **Trial started**, attribution, thuộc tính người dùng, và token mua hàng Google Play Store để gửi hoặc không gửi kèm theo sự kiện. - Bạn đã ánh xạ tên sự kiện đúng và server của bạn có thể xử lý chúng. ## Cách kiểm tra \{#how-to-test\} Trước khi bắt đầu kiểm tra tích hợp, hãy đảm bảo bạn đã: 1. Thiết lập tích hợp webhook như mô tả trong phần [Thiết lập tích hợp webhook](set-up-webhook-integration). 2. Thiết lập môi trường như mô tả trong các phần [Kiểm tra in-app purchase trên Apple App Store](test-purchases-in-sandbox) và [Kiểm tra in-app purchase trên Google Play Store](testing-on-android). Đảm bảo bạn đã build ứng dụng thử nghiệm trong môi trường sandbox chứ không phải production. 3. Thực hiện mua hàng/bắt đầu dùng thử/hoàn tiền để tạo ra sự kiện bạn đã chọn gửi đến webhook. Ví dụ, để nhận sự kiện **Subscription started**, hãy mua một gói đăng ký mới. ## Xác thực kết quả \{#validation-of-the-result\} ### Kết quả gửi sự kiện thành công \{#successful-sending-events-result\} Khi tích hợp thành công, một sự kiện sẽ xuất hiện trong phần **Last sent events** của tích hợp và có trạng thái **Success**. <img src="/assets/shared/img/6ccc3bb-webhook_integration_success.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Kết quả gửi sự kiện thất bại \{#unsuccessful-sending-events-result\} | Vấn đề | Giải pháp | |-----|--------| | Sự kiện không xuất hiện | Giao dịch mua hàng của bạn chưa xảy ra và do đó sự kiện không được tạo ra. Tham khảo phần [Khắc phục sự cố mua hàng thử nghiệm](troubleshooting-test-purchases) để tìm giải pháp. | | Sự kiện xuất hiện và có trạng thái **Sending failed** | <p>Chúng tôi xác định khả năng chuyển phát dựa trên HTTP status và coi mọi giá trị **ngoài phạm vi 200-399** là thất bại.</p><p>Để tìm hiểu thêm về vấn đề, hãy di chuột qua trạng thái **Sending failed** của sự kiện thất bại như hiển thị bên dưới.</p> | <img src="/assets/shared/img/12ff189-hover_sending_failed.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: handle-integration-errors --- --- title: "Xử lý lỗi trong tích hợp" description: "Xử lý lỗi trong tích hợp" --- Khi sử dụng bất kỳ tích hợp attribution, messaging hay analytics nào, bạn có thể gặp một số lỗi phổ biến. Xem hướng dẫn này để biết cách xử lý các trường hợp đó. ## Sai lệch dữ liệu \{#data-discrepancy\} **Nguyên nhân**: Điều này có thể xảy ra vì không phải tất cả người dùng đều đang dùng phiên bản app có tích hợp Adapty SDK. **Giải pháp**: Để đảm bảo tính nhất quán của dữ liệu, bạn có thể yêu cầu người dùng cập nhật app lên phiên bản có Adapty SDK. ## Lỗi mạng \{#network-errors\} **Nguyên nhân**: Nhiều khả năng là do mất kết nối internet giữa server Adapty và server tích hợp. **Giải pháp**: Những sự cố này thường không kéo dài và chỉ ảnh hưởng đến một số ít sự kiện. ## Server tích hợp không xử lý được sự kiện \{#integration-server-failed-to-process-the-event\} **Nguyên nhân**: Tích hợp được thiết lập không đúng cách. **Giải pháp**: Xem bài viết về tích hợp trong tài liệu của chúng tôi. Đảm bảo bạn đã hoàn tất tất cả các bước thiết lập trên Adapty Dashboard, phía công cụ bên thứ ba, và trong code của app. ## Thiếu dữ liệu tích hợp \{#missing-integration-data\} **Nguyên nhân**: Hồ sơ người dùng thiếu một số ID đặc thù cho tích hợp. Điều này có thể xảy ra khi tích hợp chưa được thiết lập đúng cách trong code của app. **Giải pháp**: Xem bài viết về tích hợp trong tài liệu của chúng tôi. Đảm bảo bạn đã triển khai các phương thức từ đoạn code mẫu trong app, và các phương thức này thực sự tương tác với hồ sơ người dùng của bạn. ## Thiếu thông tin xác thực tích hợp \{#missing-integration-credentials\} **Nguyên nhân**: Một số thông tin xác thực của tích hợp bị thiếu hoặc không chính xác. **Giải pháp**: Vui lòng kiểm tra lại tất cả thông tin xác thực cho tích hợp đó trên Adapty Dashboard. Sự cố có thể xảy ra do không khớp về phiên bản hoặc môi trường. ## Sự kiện đã hết hạn \{#the-event-has-expired\} **Nguyên nhân**: Tùy chọn **Exclude historical events** được bật trong cài đặt tích hợp, và ngày tạo sự kiện xảy ra trước ngày tạo hồ sơ người dùng trong hệ thống của chúng tôi. Điều này có thể xảy ra khi một chuỗi giao dịch bắt đầu từ nhiều năm trước được đưa vào Adapty thông qua xác thực biên lai cho một hồ sơ người dùng mới tạo gần đây. **Giải pháp**: Đảm bảo điều này không xảy ra với các sự kiện mới. Nếu bạn muốn gửi các sự kiện lịch sử đến tích hợp, hãy tắt **Exclude historical events**. ## Loại sự kiện bị vô hiệu hóa/không được hỗ trợ \{#disabledunsupported-event-type\} **Nguyên nhân**: Sự kiện không được hỗ trợ cho tích hợp này, hoặc bạn đã tắt nó khi thiết lập tích hợp. Ví dụ, các sự kiện `access_level_updated` không được hỗ trợ bởi hầu hết các tích hợp. **Giải pháp**: Kiểm tra trong tài liệu tích hợp xem tích hợp có hỗ trợ loại sự kiện này không. Nếu có, trên Adapty Dashboard, hãy đảm bảo rằng loại sự kiện này đã được bật trong cài đặt tích hợp. --- # File: manage-adapty-with-ai --- --- title: "Quản lý Adapty với AI agents và công cụ lập trình" description: "Mọi cách sử dụng Adapty cùng AI — tích hợp SDK với coding agent, truy xuất analytics bằng LLM, và đưa tài liệu Adapty vào công cụ AI của bạn." --- Adapty hoạt động tốt với các công cụ AI dành cho lập trình và AI agents. Dùng chúng để tích hợp SDK, hỏi về analytics, hoặc tra cứu tài liệu Adapty ngay trong editor. Trang này liệt kê những gì hiện có và đối tượng phù hợp với từng công cụ. ## Tích hợp Adapty SDK với AI \{#integrate-the-adapty-sdk-with-ai\} Có hai cách thêm Adapty SDK vào ứng dụng của bạn bằng công cụ AI. Cả hai đều hoạt động với Cursor, Claude và các AI assistant khác. ### Tích hợp theo kỹ năng \{#skill-based-integration\} Adapty SDK integration skill thực hiện toàn bộ quá trình tích hợp từ công cụ AI của bạn chỉ bằng một lệnh. Dùng khi bạn muốn thiết lập tự động có hướng dẫn. Chọn nền tảng: [iOS](adapty-sdk-integration-skill) · [Android](adapty-sdk-integration-skill-android) · [React Native](adapty-sdk-integration-skill-react-native) · [Flutter](adapty-sdk-integration-skill-flutter) · [Unity](adapty-sdk-integration-skill-unity) · [Kotlin Multiplatform](adapty-sdk-integration-skill-kmp) · [Capacitor](adapty-sdk-integration-skill-capacitor) ### Tích hợp từng bước \{#step-by-step-integration\} Hướng dẫn công cụ AI của bạn qua từng giai đoạn tích hợp, cung cấp đúng tài liệu theo thứ tự. Dùng khi bạn muốn xem xét từng bước trong quá trình thực hiện. Chọn nền tảng: [iOS](adapty-cursor) · [Android](adapty-cursor-android) · [React Native](adapty-cursor-react-native) · [Flutter](adapty-cursor-flutter) · [Unity](adapty-cursor-unity) · [Kotlin Multiplatform](adapty-cursor-kmp) · [Capacitor](adapty-cursor-capacitor) ## Quản lý Adapty từ dòng lệnh \{#manage-adapty-from-the-command-line\} [Adapty Developer CLI](developer-cli-quickstart) cho phép bạn quản lý các thực thể Adapty — ứng dụng, mức độ truy cập, sản phẩm, paywall và placement — từ terminal mà không cần mở dashboard. Vì đây là công cụ dòng lệnh, AI coding agent của bạn có thể chạy trực tiếp. ## Hỏi về dữ liệu của bạn \{#ask-about-your-data\} Trỏ AI coding agent vào Export Analytics API để truy vấn các chỉ số bằng ngôn ngữ tự nhiên — doanh thu, retention, LTV và nhiều hơn nữa. Không cần MCP server. [Hỏi AI về dữ liệu analytics của bạn](export-analytics-with-ai) ## Cung cấp tài liệu Adapty cho công cụ AI \{#give-your-ai-tool-the-adapty-docs\} ### Tài liệu dạng văn bản thuần \{#plain-text-docs\} Mọi tài liệu Adapty đều có sẵn dưới dạng Markdown — thêm `.md` vào URL trang, hoặc nhấp **Copy for LLM** bên dưới tiêu đề. Để có ngữ cảnh rộng hơn, hãy cung cấp cho công cụ của bạn chỉ mục [`llms.txt`](https://adapty.io/docs/vi/llms.txt) hoặc tập hợp theo nền tảng như [`ios-llms.txt`](https://adapty.io/docs/vi/ios-llms.txt). ### Context7 \{#context7\} [Context7](https://context7.com/adaptyteam/adapty-docs) là một MCP server phục vụ tài liệu Adapty cho công cụ AI của bạn, nhưng nó chỉ lập chỉ mục các đoạn code — không phải toàn bộ nội dung văn bản. Dùng để lấy ví dụ code nhanh; để có hướng dẫn đầy đủ, hãy cung cấp tài liệu dạng văn bản thuần ở trên cho công cụ của bạn. Context7 hoạt động với Cursor, Claude Code, Windsurf và các công cụ tương thích MCP khác. --- # File: export-analytics-with-ai --- --- title: "Hỏi AI về dữ liệu analytics của bạn" description: "Truy vấn analytics Adapty bằng ngôn ngữ tự nhiên với AI coding agent, sử dụng Export Analytics API." --- Hỏi AI coding agent về dữ liệu analytics Adapty của bạn bằng ngôn ngữ tự nhiên — doanh thu, conversion, retention, LTV — và để nó kéo số liệu cho bạn. Trỏ một công cụ có thể thực hiện API call vào [Export Analytics API](https://adapty.io/docs/vi/export-analytics-api.md), và nó sẽ truy vấn các chỉ số của bạn theo yêu cầu. ## Bạn có thể hỏi về những gì \{#what-you-can-ask-about\} Export Analytics API trả về các chỉ số giống như những gì bạn thấy trong các biểu đồ trên Adapty dashboard. Mỗi chỉ số có một operation riêng: | Chỉ số | Phạm vi | Operation | | --- | --- | --- | | Revenue, MRR, ARR, ARPU | Tiền kiếm được theo thời gian, phân nhóm theo kỳ, quốc gia hoặc chiến dịch | [retrieveAnalyticsData](https://adapty.io/docs/vi/api-export-analytics/operations/retrieveAnalyticsData.md) | | Cohort retention | Thời gian người dùng trong một cohort nhất định tiếp tục thanh toán | [retrieveCohortData](https://adapty.io/docs/vi/api-export-analytics/operations/retrieveCohortData.md) | | Tỷ lệ conversion | Số lượng người dùng chuyển từ bước này sang bước khác hoặc từ kênh này sang kênh khác | [retrieveConversionData](https://adapty.io/docs/vi/api-export-analytics/operations/retrieveConversionData.md) | | Churn và funnel | Nơi người dùng rời bỏ và tốc độ hủy đăng ký | [retrieveFunnelData](https://adapty.io/docs/vi/api-export-analytics/operations/retrieveFunnelData.md) | | Lifetime value (LTV) | Doanh thu trung bình mỗi phân khúc người dùng theo thời gian | [retrieveLTVData](https://adapty.io/docs/vi/api-export-analytics/operations/retrieveLTVData.md) | | Retention | Tỷ lệ người dùng vẫn còn hoạt động sau một số ngày nhất định | [retrieveRetentionData](https://adapty.io/docs/vi/api-export-analytics/operations/retrieveRetentionData.md) | Để xem đầy đủ danh sách các tham số và bộ lọc, xem [tài liệu tham khảo API](https://adapty.io/docs/vi/api-export-analytics.md). ## Trước khi bắt đầu \{#before-you-start\} Bạn cần ba thứ: - **Tài khoản Adapty có dữ liệu**: API trả về các chỉ số giống như trong biểu đồ dashboard, vì vậy ứng dụng của bạn phải đã thu thập analytics. - **Secret API key**: Tìm tại [App settings → General](https://app.adapty.io/settings/general), trong trường **Secret key**. Key gắn với từng app, vì vậy hãy dùng key riêng cho mỗi app. Lưu key trong biến môi trường (ví dụ: `ADAPTY_SECRET_KEY`) để agent đọc được mà không cần bạn dán vào chat. - **Công cụ AI có thể gọi API**: Ví dụ: Claude Code, Cursor, hoặc Claude Desktop với fetch tool. Các công cụ chat thông thường như claude.ai hay ChatGPT không thể gọi API trực tiếp. ## Cung cấp API spec cho agent \{#give-your-agent-the-api-spec\} [OpenAPI spec](https://adapty.io/docs/vi/api-specs/export-analytics-api.yaml) mô tả mọi endpoint, header xác thực, request body và các response mẫu. Khi agent có spec, nó sẽ tạo request chính xác mà không cần bạn viết code. Cung cấp spec cho agent qua URL: - **Dán URL**: Nếu agent có thể fetch URL, cung cấp `https://adapty.io/docs/vi/api-specs/export-analytics-api.yaml` và yêu cầu nó đọc spec. - **Dùng fetch tool**: Nếu agent có tool để lấy URL (ví dụ: MCP fetch server), trỏ nó vào cùng URL đó. Spec đặt base URL là `https://api-admin.adapty.io`, vì vậy agent có đủ mọi thứ cần thiết khi key của bạn đã có trong môi trường. ## Hỏi về dữ liệu của bạn \{#ask-about-your-data\} Khi đã load spec và lưu key vào biến môi trường, hãy mô tả chỉ số bạn muốn bằng ngôn ngữ tự nhiên. Ví dụ các câu hỏi: ``` What was my MRR at the end of each month this year, and how does it compare to last year? Show my trial-to-paid conversion rate for the last 90 days, broken down by product. Which countries drive the most revenue from my yearly subscription? Top 10. How is week-1 retention trending for subscribers who started in the last 6 months? What's the refund rate on my annual plan since launch, by month? Compare LTV for paid-campaign users vs. organic over the last year, and export it as CSV. ``` Agent sẽ ánh xạ yêu cầu của bạn vào operation phù hợp, đọc key từ môi trường và trả về dữ liệu. Response mặc định là JSON. Yêu cầu CSV khi bạn muốn file có thể dùng trong bảng tính — agent sẽ đặt `format` thành `csv` trong request body. :::warning Lưu secret key trong biến môi trường — không dán vào chat hay commit vào file rules. Key gắn với từng app, vì vậy hãy xoay vòng key trong **Settings → General** nếu bị lộ. Xem [xoay vòng API key](https://adapty.io/docs/vi/export-analytics-api-authorization.md). ::: ## Thiết lập một lần để dùng lại \{#set-up-once-for-repeated-use\} Để tránh lặp lại thiết lập mỗi phiên làm việc, hãy lưu spec và key để agent có thể tái sử dụng: - **Lưu link spec**: Thêm URL spec vào file rules hoặc memory của agent (ví dụ: file `CLAUDE.md` hoặc Cursor rules file) để nó load mỗi phiên. - **Lưu key trong môi trường**: Giữ `ADAPTY_SECRET_KEY` trong shell profile hoặc secret store của công cụ, để bạn không bao giờ phải dán lại. - **Lưu các prompt hay tạo custom skill**: Giữ các câu hỏi thường dùng dưới dạng saved prompt, hoặc bọc chúng trong custom skill hay slash command để agent chạy báo cáo theo yêu cầu. ## Giới hạn \{#limits\} Hãy lưu ý những ràng buộc sau: - **Giới hạn tốc độ**: API cho phép 2 request mỗi giây mỗi API key. Vượt quá sẽ trả về lỗi `429 Too Many Requests`. Hãy bảo agent chờ và thử lại khi gặp `429`. - **Key theo từng app**: Mỗi key chỉ hoạt động cho một app. Để lấy dữ liệu từ nhiều app, cung cấp key tương ứng cho từng app. - **Định dạng đầu ra**: Response mặc định là JSON. Đặt `format` thành `csv` trong request body để xuất CSV. Để xem đầy đủ quy tắc xác thực và request, xem [Authorization và định dạng request](https://adapty.io/docs/vi/export-analytics-api-authorization.md). --- # File: handle-webhooks-with-ai --- --- title: "Xử lý sự kiện gói đăng ký Adapty bằng webhook" description: "Nhận và xử lý sự kiện gói đăng ký Adapty trên máy chủ của bạn bằng webhook — thiết lập endpoint, xác thực, payload và kiểm thử trên một trang." --- Webhook cho phép máy chủ của bạn nhận sự kiện gói đăng ký Adapty theo thời gian thực — mua hàng, gia hạn, hủy, sự cố thanh toán và hoàn tiền — để bạn có thể cấp quyền truy cập, đồng bộ backend, hoặc kích hoạt các quy trình tự động. Hướng dẫn này đưa bạn từ bước cài đặt endpoint đến một tích hợp đã được xác minh và kiểm thử, đồng thời hướng dẫn cách để AI coding agent viết handler cho stack của bạn. :::tip Đang dùng AI coding agent? Nhấn **Copy for LLM** bên dưới tiêu đề và dán toàn bộ trang này vào agent — nó có đủ thông tin về thiết lập, payload và logic handler cần thiết. ::: ## Webhook Adapty hoạt động như thế nào \{#how-adapty-webhooks-work\} - **Một chiều và thời gian thực**: Adapty gửi HTTP `POST` đến máy chủ của bạn khi có sự kiện xảy ra — không cần polling. - **Hai loại request**: Một request xác minh (gửi một lần khi bạn lưu tích hợp) và các sự kiện gói đăng ký liên tục. - **Một URL cho mỗi môi trường**: Bạn cài đặt endpoint riêng cho môi trường production và sandbox. - **Bạn phải xác nhận từng request**: Phản hồi với trạng thái `2xx` nhanh chóng, và Adapty sẽ thử lại nếu thất bại. ## Xây dựng endpoint của bạn \{#build-your-endpoint\} Tạo một HTTPS endpoint công khai xử lý hai loại request: - **Request xác minh**: Gửi một lần khi bạn lưu tích hợp. Nó có body JSON rỗng (`{}`). Phản hồi với trạng thái `2xx` và một body JSON. - **Sự kiện gói đăng ký**: Các request `POST` liên tục với sự kiện trong body. Phản hồi `200` trong vòng 10 giây, sau đó thực hiện các tác vụ nặng theo cách bất đồng bộ. Chọn một chuỗi bí mật và lưu nó làm biến môi trường (ví dụ: `ADAPTY_WEBHOOK_SECRET`). Với mỗi request, hãy xác minh header `Authorization` khớp với nó, và từ chối request nếu không khớp — bạn sẽ nhập cùng bí mật này vào dashboard sau. ```javascript title="webhook.js" const app = express(); app.use(express.json()); const WEBHOOK_SECRET = process.env.ADAPTY_WEBHOOK_SECRET; app.post("/adapty/webhook", (req, res) => { // 1. Verify the shared secret Adapty echoes back. if (req.get("Authorization") !== WEBHOOK_SECRET) { return res.sendStatus(401); } // 2. Acknowledge fast, then process asynchronously. res.status(200).json({}); // 3. The verification request has an empty body — nothing to handle. const event = req.body; if (!event.event_type) return; switch (event.event_type) { case "subscription_started": case "subscription_renewed": case "trial_converted": // Grant or extend access. break; case "subscription_expired": case "subscription_refunded": // Revoke access. break; default: break; } }); app.listen(3000); ``` Triển khai endpoint lên một HTTPS URL công khai trước khi cài đặt tích hợp — Adapty gửi request xác minh ngay khi bạn lưu. ### Các sự kiện chính và payload \{#key-events-and-the-payload\} Mỗi sự kiện dùng chung cùng một cấu trúc bao ngoài. Các trường thay đổi tùy theo loại sự kiện, cửa hàng và các tùy chọn bạn đã bật. Dưới đây là sự kiện `subscription_started` đã được rút gọn: ```json title="Example event" { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": "UserIdInYourSystem", "event_type": "subscription_started", "event_datetime": "2024-11-15T10:45:36.181000+0000", "event_properties": { "store": "play_store", "currency": "USD", "price_usd": 4.99, "vendor_product_id": "onemonth_no_trial", "transaction_id": "0000000000000000", "original_transaction_id": "0000000000000000", "subscription_expires_at": "2024-12-15T10:45:36.181000+0000", "profile_event_id": "00000000-0000-0000-0000-000000000000" }, "event_api_version": 1 } ``` Các sự kiện bạn sẽ xử lý thường xuyên nhất: | Loại sự kiện | Kích hoạt khi | | --- | --- | | `subscription_started` | Người dùng bắt đầu gói đăng ký trả phí | | `subscription_renewed` | Gói đăng ký gia hạn và thanh toán thành công | | `subscription_renewal_cancelled` | Người dùng tắt tự động gia hạn (quyền truy cập duy trì đến khi hết hạn) | | `subscription_expired` | Quyền truy cập kết thúc sau khi gói đăng ký không được gia hạn | | `trial_started` | Người dùng bắt đầu dùng thử miễn phí | | `trial_converted` | Bản dùng thử chuyển sang gói đăng ký trả phí | | `billing_issue_detected` | Thanh toán gia hạn thất bại | | `subscription_refunded` | Giao dịch mua gói đăng ký được hoàn tiền | Để xem danh sách sự kiện đầy đủ và tất cả các trường, hãy xem [Loại sự kiện và trường webhook](https://adapty.io/docs/vi/webhook-event-types-and-fields.md). :::warning Đừng sắp xếp thứ tự sự kiện theo `event_datetime` — đây là thời gian nghiệp vụ của sự kiện, vì vậy các sự kiện có thể đến không theo thứ tự hoặc trùng timestamp. Hãy sắp xếp theo thời gian bạn nhận được, và loại trùng bằng `profile_event_id` hoặc các transaction ID. ::: ## Cài đặt webhook trong Adapty \{#configure-the-webhook-in-adapty\} 1. Mở [Integrations → Webhook](https://app.adapty.io/integrations/customwebhook) trong Adapty Dashboard. 2. Bật tích hợp. 3. Trong **Production endpoint URL**, nhập HTTPS URL của endpoint bạn đã triển khai. 4. Trong **Authorization header value for production endpoint**, nhập bí mật mà endpoint của bạn kiểm tra. Adapty gửi giá trị này trong header `Authorization` với mỗi request. Tùy chọn nhưng được khuyến nghị mạnh mẽ. 5. Để kiểm thử trong sandbox trước, hãy điền **Sandbox endpoint URL** và **Authorization header value** tương ứng. 6. Nhấn **Save**. Adapty ngay lập tức gửi request xác minh đến endpoint của bạn, endpoint phản hồi với `2xx` để hoàn tất thiết lập. Để chọn sự kiện cần gửi, ánh xạ tên sự kiện, hoặc bật các trường tùy chọn (giá dùng thử, sự kiện lịch sử, attribution, thuộc tính người dùng, Play Store token), hãy xem [Thiết lập tích hợp webhook](https://adapty.io/docs/vi/set-up-webhook-integration.md). ## Xây dựng với AI coding agent của bạn \{#build-it-with-your-ai-coding-agent\} Cung cấp cho AI coding agent của bạn hướng dẫn này và tài liệu tham khảo dưới dạng Markdown (thêm `.md` vào bất kỳ URL trang nào), cho nó biết stack của bạn và để nó tạo scaffolding cho handler: - [Loại sự kiện và trường webhook](https://adapty.io/docs/vi/webhook-event-types-and-fields.md) - [Thiết lập tích hợp webhook](https://adapty.io/docs/vi/set-up-webhook-integration.md) Ví dụ prompt: ``` Read these Adapty webhook docs, then write a webhook handler for my Express app: verify the Authorization header against ADAPTY_WEBHOOK_SECRET, answer the verification request, acknowledge events with 200, and grant or revoke access based on event_type. ``` Agent sẽ viết code handler, nhưng nó không thể triển khai endpoint hay cài đặt dashboard — hãy tự host endpoint và đặt URL cùng bí mật trong **Integrations → Webhook**. ## Kiểm thử webhook của bạn \{#test-your-webhook\} Kiểm thử trong sandbox trước khi đưa lên production: 1. Thiết lập sandbox endpoint và bí mật như mô tả ở trên. 2. Trong app sandbox của bạn, thực hiện mua hàng, bắt đầu dùng thử, hoặc hoàn tiền để kích hoạt sự kiện. 3. Mở phần **Last sent events** của tích hợp. Sự kiện được gửi thành công sẽ hiển thị trạng thái **Success**. Nếu sự kiện hiển thị **Sending failed**, máy chủ của bạn đã trả về trạng thái ngoài phạm vi 200–399 — di chuột qua trạng thái để xem chi tiết. Để xem toàn bộ quy trình kiểm thử, hãy xem [Kiểm thử tích hợp webhook](https://adapty.io/docs/vi/test-webhook.md). ## Giới hạn \{#limits\} - **Xác nhận trong vòng 10 giây**: Nếu Adapty không nhận được phản hồi kịp thời, nó coi lần thử đó là thất bại và thử lại. - **Thử lại**: Nếu trạng thái của bạn nằm ngoài phạm vi 200–404, Adapty thử lại với backoff theo cấp số nhân — tối đa 9 lần trong 24 giờ. - **Độ trễ hủy**: Sự kiện hủy có thể mất đến 2 giờ để đến. - **Một URL cho mỗi môi trường**: Để gửi sự kiện đến nhiều dịch vụ, hãy trỏ webhook đến backend của bạn và phân phối từ đó. --- # File: server-side-api-with-ai --- --- title: "Kiểm tra và cấp quyền truy cập gói đăng ký từ backend" description: "Sử dụng API phía máy chủ của Adapty để kiểm tra xem người dùng có gói đăng ký đang hoạt động hay không và cấp quyền truy cập thủ công, với sự hỗ trợ của AI coding agent." --- Từ backend của bạn, hãy sử dụng API phía máy chủ của Adapty để kiểm tra xem người dùng có gói đăng ký đang hoạt động hay không và cấp quyền truy cập thủ công. Hướng dẫn này đề cập đến hai lời gọi phổ biến nhất — `getProfile` và `grantAccessLevel` — và hướng dẫn cách để AI coding agent viết phần tích hợp cho stack của bạn. :::tip Đang dùng AI coding agent? Nhấn **Copy for LLM** bên dưới tiêu đề và dán toàn bộ trang này vào agent — nó có đủ các lời gọi, trường dữ liệu, và những điểm cần lưu ý. ::: ## Trước khi bắt đầu \{#before-you-start\} - **Secret API key**: Tìm trong [App settings → General](https://app.adapty.io/settings/general), ở trường **Secret key**. Key gắn với từng app cụ thể. Lưu vào biến môi trường (ví dụ: `ADAPTY_SECRET_KEY`) và gửi dưới dạng `Authorization: Api-Key {key}`. - **Base URL**: Tất cả các request đều gửi đến `https://api.adapty.io`. - **Cách xác định người dùng**: Gửi `adapty-customer-user-id` (ID người dùng của bạn — chỉ hoạt động nếu bạn xác định người dùng trong app) hoặc `adapty-profile-id` (ID hồ sơ người dùng của Adapty). Hai cái này có thể dùng thay thế nhau; chọn một trong hai. ## Kiểm tra gói đăng ký \{#check-a-subscription\} Để kiểm tra trạng thái, gọi `getProfile` với phương thức `GET` và truyền định danh người dùng qua header — không có request body. ```javascript title="check-access.js" const res = await fetch("https://api.adapty.io/api/v2/server-side-api/profile/", { headers: { "Authorization": `Api-Key ${process.env.ADAPTY_SECRET_KEY}`, "adapty-customer-user-id": userId, }, }); const { data } = await res.json(); function hasActiveAccess(profile, accessLevelId = "premium") { const level = profile.access_levels?.find(a => a.access_level_id === accessLevelId); if (!level) return false; if (level.is_in_grace_period) return true; if (!level.expires_at) return true; // lifetime / non-expiring return new Date(level.expires_at) > new Date(); // not expired yet } if (hasActiveAccess(data)) { // unlock premium features } ``` Khác với hồ sơ người dùng từ SDK, response phía máy chủ **không có trường `is_active`**. Bạn cần tự suy ra trạng thái từ `access_levels[].expires_at`: `null` nghĩa là quyền truy cập trọn đời, ngày trong tương lai nghĩa là đang hoạt động, và ngày trong quá khứ nghĩa là đã hết hạn. Hãy coi `is_in_grace_period` là vẫn đang hoạt động. Để xem đầy đủ các trường của Profile và access level, xem [getProfile](https://adapty.io/docs/vi/api-adapty/operations/getProfile.md). ## Cấp quyền truy cập thủ công \{#grant-access-manually\} Để mở khóa tính năng trả phí mà không cần mua hàng — mã promo, quyền truy cập cho nhà đầu tư hoặc beta tester, các trường hợp hỗ trợ — gọi `grantAccessLevel` với phương thức `POST`. ```javascript title="grant-access.js" await fetch("https://api.adapty.io/api/v2/server-side-api/purchase/profile/grant/access-level/", { method: "POST", headers: { "Authorization": `Api-Key ${process.env.ADAPTY_SECRET_KEY}`, "adapty-customer-user-id": userId, "Content-Type": "application/json", }, body: JSON.stringify({ access_level_id: "premium" }), // add "expires_at" for temporary access }); ``` Có hai điều cần lưu ý: - **Mức độ truy cập phải tồn tại sẵn** trong dashboard của bạn (**Access levels**) — `access_level_id` là định danh của nó, không phải tên mới. - **Các cấp quyền thủ công không xuất hiện trong analytics**. Chúng chỉ được gửi đến tích hợp webhook và Event Feed, vì vậy các biểu đồ doanh thu và chuyển đổi sẽ không phản ánh chúng. Để xem chi tiết về request và response, xem [grantAccessLevel](https://adapty.io/docs/vi/api-adapty/operations/grantAccessLevel.md). ## Xây dựng với AI coding agent của bạn \{#build-it-with-your-ai-coding-agent\} Cung cấp cho AI coding agent hướng dẫn này và API spec dưới dạng Markdown (thêm `.md` vào bất kỳ URL trang nào), cho nó biết stack của bạn, và để nó viết các lời gọi: - [OpenAPI spec](https://adapty.io/docs/vi/api-specs/adapty-api.yaml) - [getProfile](https://adapty.io/docs/vi/api-adapty/operations/getProfile.md) - [grantAccessLevel](https://adapty.io/docs/vi/api-adapty/operations/grantAccessLevel.md) Ví dụ prompt: ``` Using the Adapty server-side API spec, write backend functions to check whether a user has an active "premium" access level (GET /profile/, derive status from expires_at — there's no is_active field) and to grant it (grantAccessLevel). Authenticate with ADAPTY_SECRET_KEY and identify users by adapty-customer-user-id. ``` Agent sẽ viết code, nhưng nó không thể chạy backend của bạn hay thiết lập key — bạn cần tự cung cấp secret key và định danh người dùng. ## Giới hạn \{#limits\} - **Giới hạn tốc độ**: Tối đa 40.000 request mỗi phút cho mỗi app. - **Key theo từng app**: Mỗi key chỉ dùng được cho một app; hãy dùng đúng key cho đúng app. - **Bắt buộc phải có một định danh**: Mỗi request cần có `adapty-customer-user-id` hoặc `adapty-profile-id`. --- # File: test-purchases-in-sandbox --- --- title: "Kiểm thử Sandbox" description: "Kiểm thử in-app purchase trong môi trường sandbox để đảm bảo giao dịch diễn ra suôn sẻ." --- Sau khi đã cấu hình xong mọi thứ trong Adapty Dashboard và ứng dụng mobile, đã đến lúc thực hiện kiểm thử in-app purchase. **Lưu ý:** Không có công cụ kiểm thử nào tính phí người dùng khi họ thử mua sản phẩm. App Store không gửi email cho các giao dịch mua hoặc hoàn tiền được thực hiện trong môi trường kiểm thử. :::note **Các giao dịch sandbox bị loại khỏi tất cả các biểu đồ phân tích.** Chúng vẫn hiển thị trên các trang hồ sơ người dùng cá nhân và trong event feed. ::: :::info Để tiến hành kiểm thử in-app purchase, hãy đảm bảo: - Bạn đã hoàn thành hướng dẫn [quickstart](quickstart) về tích hợp cửa hàng, thêm sản phẩm và tích hợp Adapty SDK. - Sản phẩm của bạn đã được đánh dấu [**Ready to submit**](InvalidProductIdentifiers#step-2-check-products) trong App Store Connect. ::: ## Kiểm thử Sandbox \{#sandbox-testing\} <div style={{ maxWidth: '560px', margin: '0 auto 2rem', position: 'relative', aspectRatio: '16/9', width: '100%' }}> <iframe style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }} src="https://www.youtube.com/embed/hq4PRU-vuik?si=m5F5Sj6iLEJ-2q6n" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen /> </div> :::info Chúng tôi khuyến nghị kiểm thử in-app purchase trên thiết bị thật. Mặc dù có thể thực hiện mua hàng sandbox trên simulator, bạn cần thiết bị thật để kiểm thử đầy đủ tất cả các flow, bao gồm hộp thoại thanh toán và xác thực sinh trắc học. ::: Có hai cách chính để kiểm thử in-app purchase: - **Build trong Xcode và chạy trên thiết bị kiểm thử**: Tiện lợi cho lập trình viên và kỹ sư QA. - **Dùng tài khoản kiểm thử sandbox với TestFlight**: Phù hợp cho những người khác. Cả hai tùy chọn này đều được hướng dẫn chi tiết bên dưới. ### Bước 1. Tạo tài khoản kiểm thử Sandbox trong App Store Connect \{#step-1-create-sandbox-test-account-in-app-store-connect\} :::warning Hãy tạo tài khoản kiểm thử Sandbox mới để đảm bảo lịch sử mua hàng của bạn sạch. Nếu dùng lại tài khoản cũ, các sản phẩm đã mua trước đó vẫn còn hiệu lực và bạn sẽ không thể kiểm thử việc mua lại chúng. ::: Bạn có thể tạo tài khoản kiểm thử Sandbox mới chỉ trong vài thao tác: 1. Truy cập [**Users and Access** > **Sandbox** > **Test Accounts**](https://appstoreconnect.apple.com/access/users/sandbox) trong App Store Connect và nhấn **+**. <img src="/assets/shared/img/add-sandbox-user.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhập thông tin người dùng kiểm thử. Hãy chắc chắn xác định **Country or Region** mà bạn muốn kiểm thử vì điều này ảnh hưởng đến tình trạng sẵn có của sản phẩm theo khu vực và đơn vị tiền tệ giao dịch. :::tip - Nếu bạn dùng Gmail hoặc iCloud, bạn có thể tái sử dụng địa chỉ email hiện có với [plus sign subaddressing](https://www.wikihow.com/Use-Plus-Addressing-in-Gmail). - Bạn có thể dùng một địa chỉ email ngẫu nhiên thậm chí không tồn tại, nhưng hãy nhớ từ chối xác thực hai yếu tố (2FA) khi đăng nhập trên thiết bị kiểm thử sau này. ::: <img src="/assets/shared/img/57c3a7c-apple_new_test_account.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấn **Create**. ### Bước 2. Bật chế độ Developer \{#step-2-enable-the-developer-mode\} :::note Bỏ qua bước này nếu chế độ Developer **đã được bật** trên thiết bị kiểm thử của bạn hoặc nếu bạn **không có thiết bị Mac**. ::: Bạn sẽ cần một máy Mac đã cài Xcode và cáp kết nối thiết bị kiểm thử: 1. Mở Xcode trên máy Mac. Nếu bạn định kiểm thử in-app purchase với TestFlight, bạn chỉ cần có XCode được cài đặt; bạn không cần phải có ứng dụng ở đó. 2. Kết nối thiết bị kiểm thử với máy Mac bằng cáp. 3. Vào **Settings > Privacy & Security > Developer Mode** trên thiết bị kiểm thử và bật **Developer Mode**. ### Bước 3. Tải ứng dụng từ TestFlight \{#step-3-download-the-app-from-testflight\} :::info Bước này chỉ áp dụng nếu bạn đang kiểm thử với TestFlight. Nếu bạn đang build ứng dụng trong Xcode, hãy bỏ qua bước này. ::: Để biết chi tiết về cách gửi ứng dụng lên TestFlight, hãy xem [tài liệu của Apple](https://developer.apple.com/documentation/StoreKit/testing-in-app-purchases-with-sandbox#Prepare-for-sandbox-testing). Trước khi tải ứng dụng TestFlight, trên thiết bị kiểm thử của bạn, hãy đảm bảo bạn đã đăng nhập bằng Apple Account thật (production) của mình. Sau đó tải ứng dụng cần kiểm thử từ TestFlight. :::danger Đừng mở ứng dụng ngay sau khi tải về. Hãy tiếp tục với các bước tiếp theo. Nếu vô tình mở, hãy xóa ứng dụng khỏi thiết bị kiểm thử và tải lại. Nếu không, lịch sử mua hàng của bạn có thể không sạch và việc kiểm thử in-app purchase sẽ dẫn đến lỗi. ::: ### Bước 4. Chuyển sang tài khoản kiểm thử Sandbox \{#step-4-switch-to-sandbox-test-account\} <Details> <summary>Không dùng Mac? Đây là cách làm thay thế cho bạn</summary> Nếu bạn không làm việc trên macOS, bạn không thể chuyển sang tài khoản sandbox bằng Xcode. Tuy nhiên, bạn vẫn có thể thực hiện trực tiếp trên thiết bị kiểm thử của mình: 1. Vào **Settings > Your Apple Account > Media & Purchases** trên thiết bị kiểm thử. 2. Chọn **Sign Out** từ menu pop-up. 3. Mở ứng dụng đã tải từ TestFlight và thử mua một sản phẩm. 4. Khi được yêu cầu đăng nhập, nhập thông tin đăng nhập tài khoản sandbox để chuyển sang môi trường sandbox. </Details> Để chuyển sang tài khoản sandbox của bạn: 1. Vào **Settings > Your Apple Account > Media & Purchases** trên thiết bị kiểm thử. 2. Chọn **Sign Out** từ menu pop-up. 3. Vào **Settings > Developer**. Nếu tùy chọn **Developer** không hiển thị, hãy đảm bảo bạn đã [bật nó ở bước 2](#step-2-enable-the-developer-mode). <img src="/assets/shared/img/devmode.png" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống phần **Sandbox Apple Account** và nhấn **Sign In**. <img src="/assets/shared/img/sandbox-acc.png" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Đăng nhập bằng thông tin đăng nhập của tài khoản Apple Sandbox. ### Bước 5. Xóa lịch sử mua hàng \{#step-5-clear-purchase-history\} Nếu bạn vừa tạo tài khoản kiểm thử Sandbox mới và đã chuyển sang nó, bạn có thể bỏ qua bước này vì nó chỉ áp dụng cho việc kiểm thử lặp lại bằng cùng một tài khoản kiểm thử Sandbox. 1. Vào **Settings > Developer > Sandbox Apple Account** trên thiết bị kiểm thử. 2. Chọn **Manage** từ menu pop-up. 3. Vào **Account Settings** và nhấn **Clear Purchase History**. :::danger Bước này là bắt buộc mỗi khi bạn lặp lại việc kiểm thử bằng cùng một tài khoản kiểm thử Sandbox. Trong trường hợp này, bạn cũng sẽ cần [đăng xuất khỏi tài khoản kiểm thử Sandbox](#step-4-switch-to-sandbox-test-account), sau đó đăng nhập lại để xóa bộ nhớ cache lịch sử mua hàng trên thiết bị kiểm thử. ::: ### Bước 6. Build trong Xcode và chạy \{#step-6-build-in-xcode-and-run\} :::info Bước này chỉ áp dụng nếu bạn đang kiểm thử với bản build từ Xcode. Nếu bạn đang dùng TestFlight, hãy bỏ qua bước này. ::: 1. Kết nối thiết bị kiểm thử với máy Mac. 2. Mở Xcode. 3. Nhấn **Run** trong thanh công cụ hoặc chọn **Product > Run** để build và chạy ứng dụng trên thiết bị đã kết nối. Nếu build thành công, Xcode sẽ khởi chạy ứng dụng trên thiết bị của bạn và mở phiên gỡ lỗi trong khu vực debug. Ứng dụng của bạn giờ đã sẵn sàng để kiểm thử trên thiết bị. ### Bước 7. Thực hiện giao dịch mua kiểm thử \{#step-7-make-test-purchase\} Mở ứng dụng và thực hiện giao dịch mua kiểm thử thông qua một paywall. Sau khi hoàn tất, hãy xem bài viết về [xác nhận giao dịch mua kiểm thử](validate-test-purchases) để kiểm tra kết quả. ### Bước 8. Tiếp tục kiểm thử \{#step-8-keep-testing\} Môi trường kiểm thử của bạn đã được thiết lập xong. Nếu muốn kiểm thử lại, hãy [xóa lịch sử mua hàng của tài khoản sandbox](https://developer.apple.com/help/app-store-connect/test-in-app-purchases/manage-sandbox-apple-account-settings/). ## Các vấn đề khi kiểm thử \{#testing-issues\} Dưới đây là các vấn đề phổ biến bạn có thể gặp khi kiểm thử ứng dụng. ### Vấn đề với TestFlight \{#testflight-issues\} Bạn không thể xóa lịch sử mua hàng **nếu dùng TestFlight mà không có tài khoản kiểm thử Sandbox**, dẫn đến các vấn đề khác nhau và kết quả kiểm thử không chính xác. Nếu bạn vô tình quên [chuyển sang tài khoản kiểm thử Sandbox](#step-4-switch-to-sandbox-test-account) và đã mở ứng dụng dù chỉ một lần, TestFlight sẽ gán lịch sử mua hàng của bạn vào tài khoản Apple production, gây ra các vấn đề không mong muốn. Để khắc phục, hãy làm theo các bước sau: 1. Xóa ứng dụng khỏi thiết bị kiểm thử. 2. Làm theo các bước [Kiểm thử Sandbox](#sandbox-testing). :::note Quan trọng là không chỉ cài lại ứng dụng, mà còn phải chuyển sang tài khoản kiểm thử Sandbox, xóa lịch sử mua hàng và khởi chạy ứng dụng bằng tài khoản kiểm thử Sandbox. ::: ### Vấn đề với mức độ truy cập được chia sẻ \{#shared-access-levels-issues\} Nếu bạn lặp lại kiểm thử bằng cùng một tài khoản kiểm thử Sandbox, bạn có thể gặp hành vi bất ngờ với [mức độ truy cập được chia sẻ](sharing-paid-access-between-user-accounts) cho người dùng kiểm thử. Để kiểm tra xem người dùng có mức độ truy cập kế thừa hay không, hãy vào [Profiles & Segments](https://app.adapty.io/profiles/users) từ Adapty Dashboard và mở hồ sơ người dùng. <img src="/assets/shared/img/profile-access-level-origin.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Nếu người dùng có mức độ truy cập kế thừa, hãy làm theo các bước sau để có kết quả kiểm thử chính xác: 1. Xóa hồ sơ cha. 2. Xóa ứng dụng khỏi thiết bị kiểm thử. 3. [Tải ứng dụng từ TestFlight](#step-3-download-the-app-from-testflight). 4. [Chuyển sang tài khoản kiểm thử Sandbox](#step-4-switch-to-sandbox-test-account). 5. [Xóa lịch sử mua hàng](#step-5-clear-purchase-history). 6. [Mở ứng dụng và thực hiện giao dịch mua kiểm thử](#step-6-make-test-purchase). :::note Xóa lịch sử mua hàng là thao tác đặt lại giao dịch mua ở phía cửa hàng. Xóa hồ sơ cha chỉ xóa bản ghi ở phía Adapty. Để hiểu tại sao tài khoản được dùng lại vẫn giữ quyền truy cập và những thao tác đặt lại nào thực sự có hiệu quả, hãy xem [Đặt lại gói đăng ký của người kiểm thử](#resetting-a-testers-subscription). ::: ### Cập nhật ứng dụng trong TestFlight \{#updating-app-in-testflight\} Nếu ứng dụng TestFlight đã được cập nhật: 1. Xóa ứng dụng khỏi thiết bị kiểm thử. 2. [Tải ứng dụng từ TestFlight](#step-3-download-the-app-from-testflight). 3. [Chuyển sang tài khoản kiểm thử Sandbox](#step-4-switch-to-sandbox-test-account). 4. [Xóa lịch sử mua hàng](#step-5-clear-purchase-history). 5. [Mở ứng dụng và thực hiện giao dịch mua kiểm thử](#step-6-make-test-purchase). ## Đặt lại gói đăng ký của người kiểm thử \{#resetting-a-testers-subscription\} Trong môi trường sandbox, một giao dịch mua thuộc về **tài khoản Apple sandbox**, không phải hồ sơ người dùng Adapty. Các thao tác bạn thực hiện trên hồ sơ — xóa nó hoặc chỉnh sửa mức độ truy cập — không xóa giao dịch mua khỏi tài khoản cửa hàng. Lần cài đặt lại hoặc đồng bộ tiếp theo, SDK sẽ gán lại cùng giao dịch đó và người kiểm thử lại có quyền truy cập. Bảng dưới đây cho thấy mỗi thao tác đặt lại thay đổi điều gì và người kiểm thử thấy gì sau đó. | Thao tác | Hồ sơ người dùng Adapty | Tài khoản Apple sandbox | Quyền truy cập của người kiểm thử sau đó | | :-------------------------------------------------------------------------------- | :------------------------------------------------------ | :--------------------- | :------------------------------------------------------------------------------------------------- | | Xóa hồ sơ trong Adapty Dashboard | Đã xóa | Không thay đổi | **Trở lại** — khi cài lại, một hồ sơ mới sẽ gán lại cùng chuỗi giao dịch | | Xóa hồ sơ qua [Delete profile API](api-adapty/operations/deleteProfile) | Đã xóa | Không thay đổi | **Trở lại** — tương tự như xóa trong Dashboard | | Thêm ngày hết hạn trong quá khứ qua **Add access level** | Bị ghi đè ở lần đồng bộ tiếp theo | Không thay đổi | **Trở lại** ở lần gia hạn tiếp theo — gói đăng ký đang hoạt động sẽ áp dụng lại ngày hết hạn trong tương lai | | Gọi [Revoke access level API](api-adapty/operations/revokeAccessLevel) | Hết hạn ngay, kích hoạt `access_level_updated` (`is_active=false`) | Không thay đổi | **Trở lại** ở lần gia hạn hoặc cài lại tiếp theo — không phải cách đặt lại sandbox đáng tin cậy | | Hủy gói đăng ký trong tài khoản sandbox | Không thay đổi trực tiếp | Gói đăng ký đã bị hủy | Việc gia hạn dừng lại, quyền truy cập kết thúc khi chu kỳ hiện tại hết hạn, và người kiểm thử có thể mua lại sản phẩm | | Đăng nhập bằng tài khoản Apple sandbox mới | Hồ sơ mới | Tài khoản mới, trống | **Sạch** — được khuyến nghị cho kiểm thử lặp lại | ### Đặt lại người kiểm thử về trạng thái sạch \{#reset-a-tester-to-a-clean-state\} Để kiểm thử flow mua hàng lặp lại, hãy dùng một tài khoản Apple sandbox mới cho mỗi lần kiểm thử thay vì đặt lại hồ sơ. Làm theo [Bước 1](#step-1-create-sandbox-test-account-in-app-store-connect) để tạo tài khoản và [Bước 4](#step-4-switch-to-sandbox-test-account) để chuyển sang nó trên thiết bị. Nếu bạn tái sử dụng tài khoản sandbox hiện có, hãy [xóa lịch sử mua hàng của nó](#step-5-clear-purchase-history) trước — xóa hồ sơ Adapty không xóa được lịch sử đó. ### Xóa quyền truy cập của người kiểm thử hiện tại \{#remove-access-from-an-existing-tester\} Để xóa quyền truy cập của người kiểm thử, đừng đặt ngày hết hạn về quá khứ hoặc gọi Revoke access level API. Trong sandbox, gói đăng ký tự gia hạn sau vài phút. Mỗi lần gia hạn sẽ khôi phục lại ngày hết hạn trong tương lai trên cùng chuỗi giao dịch, vì vậy quyền truy cập sẽ tự động trở lại. Revoke access level API có kích hoạt sự kiện `access_level_updated` (`is_active=false`), nhưng lần gia hạn tiếp theo sẽ ghi đè nó. Để thực sự dừng quyền truy cập, hãy hủy gói đăng ký ở phía cửa hàng. Trên thiết bị kiểm thử, vào **Settings > Developer > Sandbox Apple Account**, chọn **Manage** và hủy gói đăng ký. Việc gia hạn sẽ dừng và quyền truy cập kết thúc khi chu kỳ hiện tại hết hạn. ### Tại sao xóa hồ sơ lại khiến quyền truy cập trở lại \{#why-deleting-the-profile-brings-access-back\} Khi người kiểm thử cài lại ứng dụng, Adapty nhận lịch sử mua hàng của tài khoản sandbox và liên kết lần cài đặt mới với giao dịch hiện có. Giao dịch mua được gắn với tài khoản cửa hàng, không phải với hồ sơ bạn đã xóa. - **Hồ sơ ẩn danh**: Việc cài lại mà không có `customer_user_id` luôn kế thừa mức độ truy cập của tài khoản cửa hàng, bất kể cài đặt [chia sẻ quyền truy cập có phí](sharing-paid-access-between-user-accounts) của bạn. - **Hồ sơ đã xác định**: Quyền truy cập có được chuyển sang `customer_user_id` mới hay không phụ thuộc vào cài đặt chia sẻ quyền truy cập có phí của bạn. Để biết cách Adapty liên kết các hồ sơ này thành một chuỗi, hãy xem [Cách hồ sơ hoạt động](how-profiles-work#parent-and-inheritor-profiles). ## Gói đăng ký kiểm thử \{#test-subscriptions\} Khi kiểm thử ứng dụng bằng tài khoản kiểm thử Sandbox, bạn có thể thiết lập tốc độ gia hạn gói đăng ký cho mỗi người kiểm thử trong sandbox. Tìm hiểu thêm về cách chỉnh sửa tốc độ gia hạn gói đăng ký trong [tài liệu chính thức của Apple](https://developer.apple.com/help/app-store-connect/test-in-app-purchases/manage-sandbox-apple-account-settings). Mặc định, gói đăng ký gia hạn tối đa 12 lần trước khi dừng, theo lịch sau: | Thời hạn gói đăng ký | 1 tuần | 1 tháng | 2 tháng | 3 tháng | 6 tháng | 1 năm | | :----------------------------- | :--------- | :--------- | :--------- | :--------- | :--------- | :--------- | | Tốc độ gia hạn gói đăng ký | 3 phút | 5 phút | 10 phút | 15 phút | 30 phút | 1 giờ | | Thời gian thử lại thanh toán | 10 phút | 10 phút | 10 phút | 10 phút | 10 phút | 10 phút | | Thời gian ân hạn thanh toán | 3 phút | 5 phút | 5 phút | 5 phút | 5 phút | 5 phút | :::note Lưu ý rằng các giao dịch kiểm thử có thể mất đến 10 phút để xuất hiện trong [Event feed](validate-test-purchases). ::: Hãy dùng sandbox để xác nhận rằng ứng dụng và backend của bạn xử lý đúng các lần gia hạn, thử lại thanh toán và thời gian ân hạn — không phải để dự đoán thời gian gia hạn trong môi trường production. Lịch gia hạn được tăng tốc và giới hạn ở trên không phản ánh thực tế production. Để phát lại giao dịch trên server phục vụ kiểm thử backend, hãy dùng [Set transaction API](api-adapty/operations/setTransaction). ## Kiểm thử ưu đãi \{#test-offers\} Việc kiểm thử ưu đãi yêu cầu tất cả biên lai của người dùng phải được xóa để tính đủ điều kiện hoạt động chính xác. Cách đáng tin cậy nhất để kiểm thử ưu đãi là dùng một [tài khoản kiểm thử Sandbox](#step-1-create-sandbox-test-account-in-app-store-connect) hoàn toàn mới. Việc kiểm thử lặp lại bằng cùng một tài khoản kiểm thử Sandbox có thể gây ra hành vi bất ngờ. :::danger Nếu bạn lặp lại kiểm thử bằng cùng một tài khoản kiểm thử Sandbox, hãy nhớ [xóa lịch sử mua hàng](#step-5-clear-purchase-history) để tránh các vấn đề liên quan đến điều kiện đủ điều kiện. ::: --- # File: local-sk-files --- --- title: "Kiểm thử StoreKit trong Xcode" description: "Kiểm thử in-app purchase trong môi trường sandbox để đảm bảo giao dịch diễn ra suôn sẻ." --- Kiểm thử StoreKit trong Xcode cho phép bạn kiểm thử in-app purchase cục bộ mà không cần thiết lập tài khoản sandbox. Để thực hiện kiểu kiểm thử này, bạn cần: 1. [Tạo sản phẩm trong Adapty](quickstart-products) và gán cho nó một **App Store product ID**. 2. Trong Xcode, tạo một [tệp cấu hình StoreKit](https://developer.apple.com/documentation/xcode/setting-up-storekit-testing-in-xcode) cục bộ và thêm sản phẩm vào đó. Product ID phải trùng với **App Store product ID** trong Adapty. 3. Thêm tệp cấu hình StoreKit vào build scheme và build ứng dụng. Khởi chạy trên emulator hoặc trên thiết bị thực. ## Có nên dùng kiểm thử StoreKit trong Xcode không? \{#should-i-use-storekit-testing-in-xcode\} Cách kiểm thử này tiện nhất nếu bạn là nhà phát triển ứng dụng muốn kiểm thử build nhanh hoặc muốn thử các tình huống mua hàng khác nhau bằng các tính năng của Xcode. Tuy nhiên, cần lưu ý rằng kiểu kiểm thử này là cục bộ, nên sẽ không có thay đổi nào hiển thị trên Adapty dashboard. Trước khi phát hành ứng dụng trong môi trường production, chúng tôi khuyến nghị bạn kiểm thử [làm việc với hồ sơ người dùng](ios-quickstart-identify) bằng [môi trường sandbox](test-purchases-in-sandbox). Bạn **nên** dùng kiểm thử StoreKit nếu muốn: - Kiểm thử logic mua hàng - Tái hiện các tình huống mua hàng khác nhau bằng công cụ Xcode (ví dụ: thanh toán bị hủy hoặc hoàn tiền) - Kiểm thử trên emulator Bạn **không nên** dùng kiểm thử StoreKit nếu muốn: - Kiểm thử logic liên quan đến hồ sơ người dùng - Xem các thao tác trong ứng dụng có hiển thị trên Adapty dashboard không - Chia sẻ ứng dụng với các nhóm không phải nhà phát triển để kiểm thử ## Bước 1. Tạo tệp cấu hình StoreKit \{#step-1-create-a-storekit-configuration-file\} Để tạo tệp cấu hình StoreKit trong Xcode: 1. Nhấp vào **File > New > File from template**. Sau đó, chọn **StoreKit Configuration File** và nhấp **Next**. 2. Đặt tên cho tệp. Sau đó, tùy thuộc vào việc bạn đã có sản phẩm trong App Store Connect chưa: - Chọn **Sync this file with an app in App Store Connect**: Để tạo tệp cấu hình chứa tất cả sản phẩm App Store Connect của bạn, giúp kiểm thử cục bộ. - Không chọn **Sync this file with an app in App Store Connect**: Để tạo tệp cấu hình trống, nơi bạn sẽ cần thêm sản phẩm thủ công. Nhấp **Next**. 3. Không thêm ứng dụng làm target. Chỉ cần tiếp tục. Nếu bạn đang làm việc với sản phẩm đã đồng bộ từ App Store Connect, chuyển đến [Bước 2](#step-2-add-the-configuration-file-to-the-build-scheme). 4. Nếu sản phẩm của bạn chưa được đồng bộ từ App Store Connect, nhấp **+** ở góc dưới bên trái và chọn loại sản phẩm. 5. Nhập tên nhóm gói đăng ký và nhấp **Next**. 6. Nhập tên tham chiếu. Trong trường **Product ID**, nhập **App Store product ID** của sản phẩm trong Adapty. 7. Cấu hình giá, ưu đãi và các thiết lập sản phẩm khác trong tệp cấu hình. Hoặc thêm nhiều sản phẩm hơn vào đó. ## Bước 2. Thêm tệp cấu hình vào build scheme \{#step-2-add-the-configuration-file-to-the-build-scheme\} Để build ứng dụng với tệp cấu hình này, bạn cần thêm nó vào build scheme. Thực hành tốt nhất là tách riêng scheme kiểm thử và scheme production, vì vậy chúng tôi đề xuất bạn tạo một scheme mới cho việc kiểm thử: 1. Ở trên cùng, nhấp vào tên ứng dụng và chọn **New scheme**. 2. Nhập tên cho scheme và nhấp **OK**. 3. Nhấp vào tên ứng dụng một lần nữa và chọn **Edit scheme**. Trong phần **StoreKit configuration**, chọn tệp cấu hình cục bộ của bạn để nó được sử dụng khi build. ## Bước 3. Build & kiểm thử \{#step-3-build--test\} Bây giờ, bạn có thể build ứng dụng và kiểm thử in-app purchase mà không cần kết nối đến backend của App Store. Bạn có thể mua sản phẩm và nhận mức độ truy cập cục bộ. Những thay đổi này sẽ không được phản ánh trên Adapty dashboard, nhưng bạn vẫn có thể kiểm thử việc mở khóa các tính năng trả phí cục bộ. [Đọc thêm](https://developer.apple.com/documentation/xcode/testing-in-app-purchases-with-storekit-transaction-manager-in-code) về các tính năng khác có sẵn khi kiểm thử StoreKit trong Xcode. --- # File: testing-on-android --- --- title: "Kiểm thử in-app purchase trong Google Play Store" description: "Kiểm thử mua gói đăng ký trên Android bằng Adapty." --- Kiểm thử in-app purchase (IAP) trong ứng dụng Android là bước quan trọng trước khi phát hành app ra công chúng. Kiểm thử sandbox là cách an toàn và hiệu quả để kiểm tra IAP mà không tốn tiền thật của người dùng. Trong hướng dẫn này, chúng ta sẽ cùng tìm hiểu quy trình kiểm thử sandbox IAP trên Google Play Store cho Android. :::note **Các giao dịch sandbox bị loại khỏi tất cả các biểu đồ phân tích.** Chúng vẫn hiển thị trên các trang hồ sơ người dùng cá nhân và trong event feed. ::: ## Môi trường kiểm thử \{#testing-environment\} Để đảm bảo hiệu suất tốt nhất cho ứng dụng Android, bạn nên kiểm thử trên thiết bị thật thay vì máy ảo. Mặc dù chúng tôi đã kiểm thử thành công trên máy ảo, Google vẫn khuyến nghị dùng thiết bị thật. Nếu bạn quyết định dùng máy ảo, hãy đảm bảo máy ảo đó đã cài Google Play. Điều này giúp ứng dụng của bạn hoạt động đúng cách. ## 1. Thiết lập tài khoản test để kiểm thử ứng dụng \{#1-set-up-test-account-for-app-testing\} Để dễ dàng kiểm thử trong các giai đoạn phát triển sau này, bạn cần tạo một tài khoản test cho việc kiểm thử in-app purchase. Đây sẽ là tài khoản đầu tiên bạn đăng nhập trên thiết bị Android dùng để test. Lưu ý rằng tài khoản chính trên thiết bị Android chỉ có thể thay đổi bằng cách đặt lại máy về trạng thái gốc (factory reset), thao tác này sẽ xóa toàn bộ dữ liệu. Vì vậy, hãy thiết lập tài khoản test đúng cách ngay từ đầu để tránh phải factory reset. :::important Cách thiết lập tài khoản test phụ thuộc vào thiết bị bạn đang dùng: - Nếu bạn có thiết bị dành riêng cho việc test, hãy tạo một **tài khoản test riêng (tài khoản Gmail mới)**. - Nếu bạn không có thiết bị riêng để test, bạn có thể dùng **tài khoản cá nhân** của mình và tạm thời bật **License testing** cho tài khoản đó. - Nếu bạn không có thiết bị Android nào, bạn có thể **tạo tài khoản test riêng và dùng với máy ảo**. Tuy nhiên, cách này không được khuyến nghị vì không giúp bạn phát hiện tất cả các vấn đề trên thiết bị thật. ::: ## 2. Bật License testing \{#2-enable-license-testing\} Sau khi thiết lập tài khoản test, bạn cần cấu hình license testing cho ứng dụng. Thực hiện các bước sau: 1. Trong thanh sidebar của Google Play Console, điều hướng đến **Settings** và chọn **License testing** trong phần **Monetization**. <img src="/assets/shared/img/android-license-testing.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Chọn danh sách license testers hiện có hoặc tạo danh sách mới. <img src="/assets/shared/img/android-testers.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Thêm tài khoản bạn sẽ dùng để test vào danh sách và lưu thay đổi. Nếu các thành viên trong nhóm cũng cần test ứng dụng, bạn có thể thêm email của họ vào danh sách để cả nhóm đều được cấp quyền truy cập. <img src="/assets/shared/img/android-list.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## 3. Tạo closed track và thêm tài khoản test vào đó \{#3-create-closed-track-and-add-test-account-to-it\} Để bắt đầu kiểm thử, bạn cần publish một phiên bản đã ký của ứng dụng lên closed track: 1. Mở ứng dụng của bạn và chọn **Test and release > Testing > Closed testing** trong menu. Tại đó, nhấn **Create track**. <img src="/assets/shared/img/android-closed-testing.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhập tên cho closed testing track và nhấn **Create track**. 3. Thêm danh sách testers vào track. 4. Trong phần **How testers join your test**, sao chép đường link và gửi đến thiết bị đã đăng nhập tài khoản test. Mở link trên thiết bị test để đăng ký người dùng đó làm tester. <img src="/assets/shared/img/android-link.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::warning Lưu ý những điều sau để đảm bảo kiểm thử thành công: - Mở URL opt-in sẽ đánh dấu tài khoản Play của bạn cho việc kiểm thử. Nếu bạn bỏ qua bước này, các sản phẩm sẽ không tải được. - Thông thường, các nhà phát triển sẽ dùng application ID khác cho bản build test. Điều này sẽ gây ra vấn đề vì Google Play Services dùng application ID để tìm in-app purchase của bạn. - Trong một số trường hợp, tài khoản test có thể mua được consumable nhưng không mua được gói đăng ký, nếu thiết bị test chưa có mã PIN. Lỗi này có thể hiển thị thông báo mơ hồ "Something went wrong". Hãy đảm bảo thiết bị test đã có mã PIN và đã đăng nhập vào Google Play Store. ::: ## 4. Tải APK đã ký lên closed track \{#4-upload-a-signed-apk-to-the-closed-track\} Tạo APK đã ký hoặc dùng Android App Bundle để tải APK đã ký lên closed track vừa tạo. Bạn không cần phải triển khai bản phát hành, chỉ cần tải APK lên là đủ. Bạn có thể tìm hiểu thêm về vấn đề này trong [bài viết hỗ trợ này](https://support.google.com/googleplay/android-developer/answer/9859348?visit_id=638929100639477968-3849460621&rd=1). :::important Nếu ứng dụng của bạn còn mới, bạn có thể cần phải mở khả dụng ứng dụng ở quốc gia hoặc khu vực của mình. Để thực hiện, vào **Testing > Closed testing**, nhấn vào test track của bạn, rồi đến **Countries/regions** để thêm các quốc gia và khu vực mong muốn. ::: ## 5. Kiểm thử in-app purchase \{#5-test-in-app-purchases\} Sau khi tải APK lên, hãy chờ vài phút để bản phát hành được xử lý. Sau đó, mở thiết bị test và đăng nhập bằng tài khoản email đã thêm vào danh sách Testers. Lúc này bạn có thể kiểm thử in-app purchase như trên ứng dụng thực tế. <img src="/assets/shared/img/a8d2da9-image.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Đọc thêm \{#read-more\} Tham khảo các tài nguyên sau để tìm hiểu thêm về kiểm thử in-app purchase trong ứng dụng Android: - [Chu kỳ gia hạn trong sandbox](https://developer.android.com/google/play/billing/test#subs) - [Kiểm thử sản phẩm mua một lần](https://developer.android.com/google/play/billing/test#one-time) --- # File: validate-test-purchases --- --- title: "Xác thực giao dịch mua thử nghiệm" description: "Xác thực giao dịch mua thử nghiệm trong Adapty để đảm bảo giao dịch diễn ra suôn sẻ." --- Trước khi phát hành ứng dụng di động lên môi trường production, việc kiểm thử in-app purchase một cách kỹ lưỡng là rất quan trọng. Hãy tham khảo các bài viết [Kiểm thử in-app purchase trên Apple App Store](test-purchases-in-sandbox) và [Kiểm thử in-app purchase trên Google Play Store](testing-on-android) để được hướng dẫn chi tiết. Sau khi bắt đầu kiểm thử, bạn cần xác nhận rằng các giao dịch mua thử nghiệm đã thành công. Mỗi khi thực hiện một giao dịch mua thử nghiệm trên thiết bị di động, hãy xem giao dịch tương ứng trong [**Event Feed**](https://app.adapty.io/event-feed) trên Adapty Dashboard. Nếu giao dịch không xuất hiện trong **Event Feed**, nghĩa là Adapty chưa ghi nhận được giao dịch đó. ## Giao dịch mua thử nghiệm thành công \{#test-purchase-is-successful\} Nếu giao dịch mua thử nghiệm thành công, sự kiện giao dịch tương ứng sẽ hiển thị trong **Event Feed**: <img src="/assets/shared/img/9ade2d5-event_feed_sandbox.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Nếu các giao dịch hoạt động đúng như mong đợi, hãy chuyển sang [Danh sách kiểm tra trước khi phát hành](release-checklist) và tiến hành phát hành ứng dụng. ## Giao dịch mua thử nghiệm không thành công \{#test-purchase-is-not-successful\} Nếu không thấy sự kiện giao dịch nào trong vòng 10 phút hoặc gặp lỗi trong ứng dụng di động, hãy tham khảo bài viết [Xử lý sự cố](troubleshooting-test-purchases) và các bài viết về xử lý lỗi [cho iOS](ios-sdk-error-handling), [cho Android](android-sdk-error-handling), [cho React Native](react-native-handle-errors), [cho Flutter](error-handling-on-flutter-react-native-unity), [cho Unity](unity-handle-errors), và [Kotlin Multiplatform](kmp-handle-errors) để tìm giải pháp phù hợp. <img src="/assets/shared/img/31a79b2-no_events.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: troubleshooting-test-purchases --- --- title: "Khắc phục sự cố mua hàng thử nghiệm" description: "Khắc phục sự cố mua hàng thử nghiệm trong Adapty và giải quyết các vấn đề giao dịch in-app phổ biến." --- Nếu bạn gặp sự cố giao dịch, trước tiên hãy đảm bảo bạn đã hoàn thành tất cả các bước trong [danh sách kiểm tra phát hành](release-checklist). Nếu đã hoàn thành tất cả các bước mà vẫn gặp sự cố, hãy làm theo hướng dẫn dưới đây để giải quyết: ## Ứng dụng di động trả về lỗi \{#an-error-is-returned-in-the-mobile-app\} Tham khảo danh sách lỗi theo nền tảng của bạn: [dành cho iOS](ios-sdk-error-handling), [dành cho Android](android-sdk-error-handling), [dành cho React Native](react-native-troubleshoot-purchases), [Flutter](error-handling-on-flutter-react-native-unity), và [Unity](unity-troubleshoot-purchases), rồi làm theo khuyến nghị của chúng tôi để khắc phục sự cố. ## Giao dịch không xuất hiện trong Event Feed dù ứng dụng không trả về lỗi \{#transaction-is-absent-from-the-event-feed-although-no-error-is-returned-in-the-mobile-app\} Để khắc phục sự cố này, hãy kiểm tra các điểm sau: 1. **Dành cho iOS**: Đảm bảo bạn dùng thiết bị thật thay vì máy ảo simulator. 2. Đảm bảo `Bundle ID`/`Package name` của ứng dụng khớp với giá trị trong [**App settings**](https://app.adapty.io/settings/general). 3. Đảm bảo `PUBLIC_SDK_KEY` trong ứng dụng khớp với **Public SDK key** trên Adapty Dashboard: [**App settings** -> tab **General** -> mục **API keys**](https://app.adapty.io/settings/general). 4. Đảm bảo bạn đang dùng tài khoản sandbox — không phải [file cấu hình StoreKit cục bộ](local-sk-files). Nếu trước đây bạn đã dùng file cấu hình StoreKit cục bộ để kiểm thử, hãy đảm bảo bạn không dùng nó trong bản build hiện tại. ## Hồ sơ thử nghiệm không có sự kiện nào \{#no-event-is-present-in-my-testing-profile\} Đây là hành vi bình thường. Một bản ghi hồ sơ người dùng mới sẽ được tự động tạo trong Adapty khi: - Người dùng chạy ứng dụng lần đầu tiên - Người dùng đăng xuất khỏi ứng dụng **Lý do xảy ra:** Tất cả giao dịch và sự kiện đều gắn với hồ sơ người dùng đã tạo ra giao dịch đầu tiên. Điều này giúp toàn bộ lịch sử giao dịch (dùng thử, mua hàng, gia hạn) được liên kết với cùng một hồ sơ. **Những gì bạn sẽ thấy:** Các bản ghi hồ sơ người dùng mới (gọi là "hồ sơ không gốc") có thể xuất hiện mà không có sự kiện nào nhưng vẫn giữ nguyên mức độ truy cập. Bạn có thể thấy sự kiện `access_level_updated`. Đây là hành vi bình thường. **Khi kiểm thử:** Để tránh tạo nhiều hồ sơ, hãy tạo tài khoản thử nghiệm mới (Sandbox Apple ID) mỗi lần bạn cài đặt lại ứng dụng. Xem thêm chi tiết tại [Tạo hồ sơ](how-profiles-work#profile-creation). Dưới đây là ví dụ về một hồ sơ không gốc. Lưu ý rằng không có sự kiện nào trong **User history** nhưng vẫn có mức độ truy cập. <img src="/assets/shared/img/98d0dad-non-original_profile.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Giá không phản ánh đúng giá đã đặt trong App Store Connect \{#prices-do-not-reflect-the-actual-prices-set-in-app-store-connect\} Trong cả Sandbox và TestFlight (vốn dùng môi trường sandbox cho in-app purchase), điều quan trọng là xác minh luồng mua hàng hoạt động đúng, thay vì tập trung vào độ chính xác của giá. Cần lưu ý rằng API của Apple đôi khi có thể cung cấp dữ liệu không chính xác, đặc biệt khi các thiết bị hoặc tài khoản được cấu hình với các khu vực khác nhau. Vì giá đến trực tiếp từ Store và backend Adapty không ảnh hưởng đến giá mua hàng theo bất kỳ cách nào, bạn có thể bỏ qua sự không chính xác về giá khi kiểm thử mua hàng qua Adapty. Do đó, hãy ưu tiên kiểm thử luồng mua hàng thay vì độ chính xác của giá để đảm bảo nó hoạt động đúng như mong đợi. ## Thời gian giao dịch trong Event Feed không chính xác \{#the-transaction-time-in-the-event-feed-is-incorrect\} **Event Feed** sử dụng múi giờ được đặt trong **App Settings**. Để đồng bộ múi giờ của sự kiện với giờ địa phương của bạn, hãy điều chỉnh **Reporting timezone** trong [**App settings** -> tab **General**](https://app.adapty.io/settings/general). ## Paywall và sản phẩm mất nhiều thời gian để tải \{#paywalls-and-products-take-a-long-time-to-load\} Sự cố này có thể xảy ra nếu tài khoản thử nghiệm của bạn có lịch sử giao dịch dài. Chúng tôi khuyến nghị tạo tài khoản thử nghiệm mới mỗi lần, như đã nêu trong phần [Tạo Tài khoản Thử nghiệm Sandbox (Sandbox Apple ID) trong App Store Connect](test-purchases-in-sandbox#step-1-create-sandbox-test-account-in-app-store-connect). Nếu bạn không thể tạo tài khoản mới, bạn có thể xóa lịch sử giao dịch trên tài khoản hiện tại bằng cách thực hiện các bước sau trên thiết bị iOS: 1. Mở **Settings** và nhấn **App Store**. 2. Nhấn **Sandbox Apple ID** của bạn. 3. Trong cửa sổ popup, chọn **Manage**. 4. Trên trang **Account Settings**, nhấn **Clear Purchase History**. Để biết thêm chi tiết, hãy xem [tài liệu dành cho nhà phát triển Apple](https://developer.apple.com/documentation/storekit/testing-in-app-purchases-with-sandbox). --- # File: test-devices --- --- title: "Thiết bị kiểm thử" description: "Tìm hiểu cách quản lý thiết bị kiểm thử trong Adapty để kiểm thử ứng dụng hiệu quả." --- Để phục vụ kiểm thử, bạn có thể đánh dấu thiết bị của mình là thiết bị kiểm thử — điều này sẽ tắt bộ nhớ đệm và đảm bảo các thay đổi được phản ánh ngay lập tức. :::note Thiết bị kiểm thử được hỗ trợ từ các phiên bản SDK cụ thể: - iOS: 2.11.1 - Android: 2.11.3 - React Native: 2.11.1 Hỗ trợ cho Flutter và Unity sẽ được bổ sung sau. ::: ## Đánh dấu thiết bị là thiết bị kiểm thử \{#mark-your-device-as-test\} 1. Mở [**App settings**](https://app.adapty.io/settings/general) trong Adapty Dashboard. 2. Kéo xuống phần **Test devices** trong tab **General**. <img src="/assets/shared/img/14c581d-test_device_add.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp vào nút **Add test device**. <img src="/assets/shared/img/f86d5e2-test_users_add_device.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trong cửa sổ **Add test device**, nhập: | Trường | Mô tả | |:-----------------------------------------| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Test device name** | Tên của thiết bị kiểm thử để bạn dễ nhận biết. | | **ID used to identify this test device** | Chọn loại định danh bạn muốn dùng để xác định thiết bị kiểm thử. Xem khuyến nghị của chúng tôi trong phần [Nên dùng định danh nào](test-devices#which-identifier-you-should-use) bên dưới để chọn tùy chọn phù hợp nhất. | | **ID value** | Nhập giá trị của định danh. | 5. Nhớ nhấp vào nút **Add test device** để lưu thay đổi. ## Nên dùng định danh nào \{#which-identifier-you-should-use\} Để xác định một thiết bị, bạn có thể dùng nhiều loại định danh khác nhau. Chúng tôi khuyến nghị: - **Customer User ID** cho cả thiết bị iOS và Android nếu bạn <InlineTooltip tooltip="xác định người dùng trong Adapty">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. Đây là lựa chọn tốt nhất, đặc biệt khi bạn có nhiều hơn một thiết bị kiểm thử cho cùng một tài khoản trong ứng dụng. Nếu Customer User ID được dùng làm **ID used to identify this test device**, tất cả các thiết bị được kết nối với tài khoản này sẽ được đánh dấu là thiết bị kiểm thử. - **IDFA (iOS)** và **Advertising ID (Android)**: Những định danh quảng cáo này là lựa chọn lý tưởng cho thiết bị iOS và Android tương ứng nếu bạn đã xin phép người dùng để truy cập chúng. Dù bạn đã có Customer User ID, bạn vẫn có thể ưu tiên dùng định danh quảng cáo nếu bạn chuyển đổi giữa các tài khoản trong ứng dụng khi kiểm thử. Ngoài ra, những định danh này rất hữu ích khi cùng một tài khoản có cả thiết bị kiểm thử và thiết bị cá nhân mà bạn không muốn đánh dấu thiết bị cá nhân là thiết bị kiểm thử. Còn có các tùy chọn khác như Adapty Profile ID, IDFV và Android ID — ít tiện lợi hơn nhưng vẫn có thể dùng nếu bạn không thể sử dụng Customer User ID, IDFA hoặc Advertising ID. Hãy cùng xem xét chi tiết tất cả các tùy chọn có thể. ### Định danh cho tất cả các nền tảng \{#identifiers-for-all-platforms\} | Định danh | Cách dùng | |----------|-----| | Customer User ID | <p>Một định danh duy nhất do bạn thiết lập để xác định người dùng trong hệ thống của bạn. Có thể là email của người dùng, ID nội bộ của bạn, hoặc bất kỳ chuỗi nào khác. Để dùng tùy chọn này, bạn phải <InlineTooltip tooltip="Xác định người dùng trong Adapty">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>.</p><p></p><p>Đây là lựa chọn tốt nhất để xác định thiết bị kiểm thử, đặc biệt nếu bạn dùng nhiều thiết bị cho cùng một tài khoản. Tất cả các thiết bị có tài khoản này đều sẽ được coi là thiết bị kiểm thử.</p> | | Adapty profile ID | <p>Một định danh duy nhất cho [hồ sơ người dùng](profiles-crm) trong Adapty.</p><p></p><p>Dùng tùy chọn này nếu bạn không thể dùng Customer User ID, IDFA cho iOS hoặc Advertising ID cho Android. Lưu ý rằng Adapty Profile ID có thể thay đổi nếu bạn cài lại ứng dụng hoặc đăng nhập lại.</p> | #### Cách lấy Customer User ID và Adapty profile ID \{#how-to-obtain-customer-user-id-and-adapty-profile-id\} Cả hai định danh đều có thể lấy được trong phần chi tiết **Profile** trên Adapty Dashboard: 1. Tìm hồ sơ người dùng trong tab [**Adapty Profiles** -> **Event feed**](https://app.adapty.io/event-feed). :::note Để tìm đúng hồ sơ, hãy thực hiện một loại giao dịch hiếm. Khi giao dịch xuất hiện trong [**Event Feed**](https://app.adapty.io/event-feed), bạn sẽ dễ dàng nhận ra nó. ::: 2. Sao chép giá trị của trường **Customer user ID** và **Adapty ID** trong phần chi tiết hồ sơ: <img src="/assets/shared/img/345d308-test_users_CUID_adapty_ID.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Định danh Apple \{#apple-identifiers\} | Định danh | Cách dùng | |----------|-----| | IDFA | <p>Identifier for Advertisers (IDFA) là định danh thiết bị duy nhất do Apple gán cho thiết bị của người dùng.</p><p></p><p>Đây là lựa chọn lý tưởng cho thiết bị iOS vì nó không tự thay đổi, mặc dù bạn có thể đặt lại thủ công.</p><p>**Lưu ý**: Từ khi iOS 14.5 ra mắt, nhà quảng cáo phải xin sự đồng ý của người dùng để truy cập IDFA. Hãy đảm bảo ứng dụng của bạn đang yêu cầu sự đồng ý và bạn đã cấp quyền trên thiết bị kiểm thử của mình.</p> | | IDFV | Identifier for Vendors (IDFV) là định danh chữ-số duy nhất do Apple gán cho tất cả các ứng dụng trên cùng một thiết bị từ cùng một nhà phát hành/vendor. Nó có thể thay đổi nếu bạn cài lại hoặc cập nhật ứng dụng. | #### Cách lấy IDFA \{#how-to-obtain-the-idfa\} Apple không cung cấp IDFA theo mặc định. Lấy nó từ phần attribution trong hồ sơ người dùng trên Adapty Dashboard: 1. Tìm hồ sơ người dùng trong tab [**Adapty Profiles** -> **Event feed**](https://app.adapty.io/event-feed). :::note Để tìm đúng hồ sơ, hãy thực hiện một loại giao dịch hiếm. Khi giao dịch xuất hiện trong [**Event Feed**](https://app.adapty.io/event-feed), bạn sẽ dễ dàng nhận ra nó. ::: 2. Mở chi tiết hồ sơ và sao chép giá trị trường **IDFA** trong phần **Attributes**: <img src="/assets/shared/img/ce4a63f-test_users_idfa.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Ngoài ra, bạn có thể [tìm ứng dụng trên App Store sẽ hiển thị IDFA của bạn](https://www.apple.com/us/search/idfa?src=globalnav). #### Cách lấy Identifier for Vendors (IDFV) \{#how-to-obtain-the-identifier-for-vendors-idfv\} Để lấy IDFV, hãy yêu cầu developer của bạn gọi phương thức sau trong ứng dụng và hiển thị định danh nhận được ra log hoặc debug panel. ```swift showLineNumbers title="Swift" UIDevice.current.identifierForVendor ``` ### Định danh Google \{#google-identifiers\} | Định danh | Cách dùng | |----------|-----| | Advertising ID | <p>Advertising ID là định danh thiết bị duy nhất do Google gán cho thiết bị của người dùng.</p><p>Đây là lựa chọn lý tưởng cho thiết bị Android vì nó không tự thay đổi, mặc dù bạn có thể đặt lại thủ công.</p><p> **Lưu ý**: Để dùng tùy chọn này, hãy tắt **Opt out of Ads Personalization** trong cài đặt **Ads** nếu bạn đang dùng Android 12 trở lên.</p>| | Android ID | Android ID là định danh duy nhất cho mỗi tổ hợp khóa ký ứng dụng, người dùng và thiết bị. Có sẵn trên Android 8.0 trở lên. | #### Cách lấy Advertising ID \{#how-to-obtain-advertising-id\} Để tìm Advertising ID của thiết bị: 1. Mở ứng dụng **Settings** trên thiết bị Android của bạn. 2. Nhấp vào **Google**. 3. Chọn **Ads** trong **Services**. Advertising ID của bạn sẽ hiển thị ở cuối màn hình. #### Cách lấy Android ID \{#how-to-obtain-android-id\} Để lấy Android ID, hãy yêu cầu developer của bạn truy vấn [ANDROID_ID](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID) bằng phương thức sau trong ứng dụng và hiển thị định danh nhận được ra log hoặc debug panel. ```kotlin showLineNumbers title="Kotlin/Java" android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID); ``` --- # File: release-checklist --- --- title: "Danh sách kiểm tra trước khi phát hành" description: "Làm theo danh sách kiểm tra của Adapty để đảm bảo quá trình cập nhật ứng dụng diễn ra suôn sẻ." --- Chúng tôi rất vui khi bạn quyết định sử dụng Adapty! Hy vọng quá trình tích hợp diễn ra suôn sẻ. Hướng dẫn này sẽ đưa bạn qua các bước để đảm bảo ứng dụng sẵn sàng phát hành lên cửa hàng, và bạn có thể yên tâm rằng flow thanh toán hoạt động đúng. ## Các yêu cầu cần có trước \{#pre-flight-essentials\} Những gì bạn cần trước khi bắt đầu kiểm tra: - Thiết bị thật với tài khoản sandbox - Quyền truy cập vào Adapty Dashboard - Quyền truy cập vào App Store Connect / Google Play Console :::note Mặc dù có thể thực hiện mua hàng sandbox trên máy ảo, nhưng bạn cần thiết bị thật để kiểm tra đầy đủ tất cả các flow, bao gồm cả hộp thoại thanh toán và xác thực sinh trắc học. ::: <Button id="test-purchases-in-sandbox"> Hướng dẫn kiểm tra cho App Store </Button> <Button id="testing-on-android"> Hướng dẫn kiểm tra cho Google Play </Button> ## Kiểm tra chung \{#universal-validations\} - [ ] **Kết nối cửa hàng**: Đảm bảo bạn đã kết nối Adapty với App Store và/hoặc Google Play: - [ ] [App Store](initial_ios) - [ ] [Google Play](initial-android) - [ ] **Gửi sự kiện gói đăng ký**: Xác nhận rằng thông báo từ máy chủ đã được thiết lập: - [ ] [Thông báo máy chủ App Store](enable-app-store-server-notifications) - [ ] [Thông báo nhà phát triển theo thời gian thực (RTDN)](enable-real-time-developer-notifications-rtdn) - [ ] **Nhận diện hồ sơ người dùng**: Kiểm tra logic nhận diện người dùng và đảm bảo các giao dịch mua được gắn đúng hồ sơ người dùng: - [ ] [Kiểm tra logic nhận diện trong code ứng dụng của bạn có khớp với trường hợp sử dụng không](ios-quickstart-identify) - [ ] [Đảm bảo bạn hiểu logic cha/kế thừa khi chia sẻ quyền truy cập trả phí giữa các hồ sơ người dùng](sharing-paid-access-between-user-accounts) - [ ] **Ưu đãi**: Nếu ứng dụng có ưu đãi cho App Store, hãy đảm bảo bạn đã [thêm In-app purchase key](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers) vào cả trường chính lẫn phần **App Store promotional offers**. - [ ] **Thu thập dữ liệu**: Đảm bảo tuân thủ quyền riêng tư: - [ ] Nếu bạn cần tuân thủ các quy định về quyền riêng tư như GDPR hoặc CCPA, hoặc ứng dụng dành cho trẻ em, hãy kiểm soát việc [bật IDFA và thu thập/chia sẻ IP](sdk-installation-ios#data-policies). - [ ] Nếu ứng dụng sử dụng AppTrackingTransparency, hãy đảm bảo bạn đang [gửi trạng thái ủy quyền cho Adapty](ios-deal-with-att). - [ ] **Nhãn quyền riêng tư**: [Tìm hiểu thêm](apple-app-privacy) về dữ liệu Adapty thu thập và các flag bạn cần thiết lập để kiểm duyệt. ## Kiểm tra giao dịch mua \{#purchase-validations\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Trước khi ra mắt, hãy đảm bảo rằng các giao dịch mua trong ứng dụng hoạt động chính xác và paywall của bạn sẵn sàng để kiểm duyệt cửa hàng. Cách kiểm tra in-app purchase phụ thuộc vào cách bạn triển khai: - Bạn hiển thị paywall được tạo trong Adapty Paywall Builder - Bạn đã tự triển khai paywall và sử dụng phương thức `makePurchase` bên trong để xử lý giao dịch - Bạn sử dụng Adapty ở chế độ observer (với Adapty Paywall Builder hoặc paywall tùy chỉnh của bạn) <Tabs groupId="paywall" queryString> <TabItem value="builder" label="Adapty Paywall Builder" default> **Mục tiêu**: Adapty render paywall, người dùng có thể mua sản phẩm, quyền truy cập được mở khóa và flow khôi phục hoạt động đúng. - [ ] Ứng dụng của bạn [hiển thị paywall](ios-present-paywalls) từ đúng placement bạn sẽ phát hành. - [ ] Paywall hiển thị trên màn hình. Nếu tải mất quá nhiều thời gian (ví dụ: khi bạn hoặc người dùng gặp kết nối internet không ổn định), hãy cân nhắc [điều chỉnh fetch policy](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). - [ ] Paywall hiển thị đúng biến thể (đối tượng/ngôn ngữ nếu có). Bạn có thể [thay đổi mức độ ưu tiên đối tượng](change-audience-priority) nếu cần. - [ ] Sản phẩm và giá hiển thị trên paywall. Lưu ý rằng API của Apple đôi khi có thể cung cấp giá không chính xác trong quá trình kiểm tra (đặc biệt với các cấu hình khu vực khác nhau), vì vậy hãy ưu tiên kiểm tra chức năng flow mua hàng hơn là độ chính xác của giá vì Adapty không ảnh hưởng đến giá của cửa hàng. - [ ] Giao dịch mua sandbox hoàn tất thành công. Callback mua hàng thành công được nhận. - [ ] Quyền truy cập được mở khóa và duy trì. Xác nhận rằng [quyền truy cập trả phí được cấp dựa trên hồ sơ người dùng Adapty hiện tại](ios-check-subscription-status#connect-profile-with-paywall-logic). - [ ] Sau khi mua, hồ sơ người dùng Adapty có mức độ truy cập đang hoạt động. - [ ] Các tính năng trả phí được mở khóa khi hồ sơ người dùng chứa mức độ truy cập đó (không chỉ dựa vào callback mua hàng). - [ ] Khôi phục giao dịch hoạt động. Khi bạn cài đặt lại ứng dụng hoặc cài đặt trên thiết bị mới, tính năng khôi phục giao dịch tự động hoạt động theo cài đặt [Chia sẻ quyền truy cập trả phí](sharing-paid-access-between-user-accounts). Nếu bạn không có xác thực backend, giao dịch sẽ được khôi phục tự động bất kể cài đặt. Trong các trường hợp khác, hãy đảm bảo người dùng có thể khôi phục giao dịch sau khi cài đặt lại ứng dụng. - [ ] Yêu cầu kiểm duyệt cửa hàng: - [ ] Nút **Restore purchases** có trên paywall. Bạn có thể thêm nút này trong paywall builder, và nó sẽ tự động xử lý việc khôi phục giao dịch khi được nhấn. - [ ] Điều khoản sử dụng + Chính sách bảo mật có thể truy cập từ màn hình paywall, và nhấp vào các liên kết này sẽ mở chúng trong trình duyệt. </TabItem> <TabItem value="makepurchase" label="Custom paywall (makePurchase)" default> **Mục tiêu**: Bạn render giao diện; Adapty xử lý giao dịch, cập nhật hồ sơ người dùng và khôi phục. - [ ] ID sản phẩm không được hardcode trong code ứng dụng. Bạn chỉ hardcode ID [placement](placements). - [ ] Ứng dụng của bạn [lấy sản phẩm](fetch-paywalls-and-products) từ đúng placement bạn sẽ phát hành. - [ ] Danh sách sản phẩm tải thành công. Nếu tải mất quá nhiều thời gian (ví dụ: khi bạn hoặc người dùng gặp kết nối internet không ổn định), hãy cân nhắc [điều chỉnh fetch policy](fetch-paywalls-and-products#fetch-paywall-information). - [ ] Các sản phẩm được lấy về khớp với biến thể mong đợi (đối tượng/ngôn ngữ nếu có). Bạn có thể [thay đổi mức độ ưu tiên đối tượng](change-audience-priority) nếu cần. - [ ] Sản phẩm và giá hiển thị trên paywall. Lưu ý rằng API của Apple đôi khi có thể cung cấp giá không chính xác trong quá trình kiểm tra (đặc biệt với các cấu hình khu vực khác nhau), vì vậy hãy ưu tiên kiểm tra chức năng flow mua hàng hơn là độ chính xác của giá vì Adapty không ảnh hưởng đến giá của cửa hàng. - [ ] Giao dịch mua sandbox với [makePurchase](making-purchases) hoàn tất thành công: - [ ] Kết quả mua hàng thành công được xử lý. - [ ] Các kết quả đang chờ/thất bại/đã hủy được xử lý một cách hợp lý. - [ ] Nếu bạn [sử dụng Remote Config](present-remote-config-paywalls), các giá trị của nó được lấy đúng cho paywall của bạn. - [ ] Khi paywall được hiển thị, phương thức [`logShowFlow` (iOS SDK v4+) / `logShowPaywall`](present-remote-config-paywalls#track-paywall-view-events) được gọi. - [ ] Giao dịch mua sandbox hoàn tất thành công. Callback mua hàng thành công được nhận. - [ ] Quyền truy cập được mở khóa và duy trì. Xác nhận rằng [quyền truy cập trả phí được cấp dựa trên hồ sơ người dùng Adapty hiện tại](ios-check-subscription-status#connect-profile-with-paywall-logic). - [ ] Sau khi mua, hồ sơ người dùng Adapty có mức độ truy cập đang hoạt động. - [ ] Các tính năng trả phí được mở khóa khi hồ sơ người dùng chứa mức độ truy cập đó (không chỉ dựa vào callback mua hàng). - [ ] Khôi phục giao dịch hoạt động. Khi bạn cài đặt lại ứng dụng hoặc cài đặt trên thiết bị mới, tính năng khôi phục giao dịch tự động hoạt động theo cài đặt [Chia sẻ quyền truy cập trả phí](sharing-paid-access-between-user-accounts). Nếu bạn không có xác thực backend, giao dịch sẽ được khôi phục tự động bất kể cài đặt. Trong các trường hợp khác, hãy đảm bảo người dùng có thể khôi phục giao dịch sau khi cài đặt lại ứng dụng. - [ ] Yêu cầu kiểm duyệt cửa hàng: - [ ] Nút **Restore purchases** có thể truy cập và [xử lý việc khôi phục](restore-purchase). - [ ] Điều khoản sử dụng + Chính sách bảo mật có thể truy cập từ màn hình paywall, và nhấp vào các liên kết này sẽ mở chúng trong trình duyệt. </TabItem> <TabItem value="observer" label="Observer mode"> **Mục tiêu**: Bạn tự xử lý giao dịch, cập nhật hồ sơ người dùng và khôi phục; Adapty nhận báo cáo giao dịch. - [ ] **Ứng dụng của bạn hoàn tất giao dịch bằng flow mua hàng của riêng mình** (StoreKit / BillingClient / backend): - [ ] Giao dịch mua sandbox thành công trong giao diện cửa hàng. - [ ] Các kết quả đang chờ/thất bại/đã hủy được xử lý hợp lý trong ứng dụng. - [ ] **Giao dịch được báo cáo cho Adapty**. - [ ] Chế độ observer được [bật trong code ứng dụng](implement-observer-mode). - [ ] Giao dịch mua hiển thị trong Event Feed của Adapty. - [ ] Gia hạn, hủy và hoàn tiền được phản ánh theo thời gian (khi áp dụng). - [ ] **Lượt xem paywall được theo dõi**. Phương thức [`logShowFlow` (iOS SDK v4+) / `logShowPaywall`](present-remote-config-paywalls#track-paywall-view-events) được gọi khi paywall được hiển thị. - [ ] **Khôi phục giao dịch hoạt động cho triển khai của bạn**. Cài đặt lại ứng dụng hoặc chuyển sang thiết bị khác sẽ khôi phục quyền truy cập đúng cách. - [ ] **Yêu cầu kiểm duyệt cửa hàng**: - [ ] Hành động **Restore purchases** có thể truy cập và kích hoạt flow khôi phục của bạn. - [ ] Điều khoản sử dụng + Chính sách bảo mật có thể truy cập từ màn hình paywall hoặc mua hàng và mở trong trình duyệt. </TabItem> </Tabs> Nếu bạn có bất kỳ câu hỏi nào về việc tích hợp Adapty SDK, hãy sử dụng chatbot AI ở góc dưới bên phải hoặc liên hệ với chúng tôi tại [support@adapty.io](mailto:support@adapty.io). --- # File: submit-app-to-app-store --- --- title: "Gửi ứng dụng iOS của bạn lên App Store" description: "Tải bản build lên App Store Connect và gửi ứng dụng iOS có gói đăng ký của bạn để Apple xét duyệt." --- Sau khi tích hợp Adapty đã được kiểm tra và hoạt động ổn định, bạn đã sẵn sàng tải bản build lên App Store Connect và gửi ứng dụng để Apple xét duyệt. :::tip Trước khi gửi, hãy đảm bảo bạn đã hoàn thành [Danh sách kiểm tra trước khi phát hành](release-checklist) để xác minh tích hợp Adapty, luồng mua hàng và các yêu cầu xét duyệt của cửa hàng. ::: ## Tải bản build lên App Store Connect \{#upload-your-build-to-app-store-connect\} ### Bước 1. Lưu trữ ứng dụng trong Xcode và tải lên App Store Connect \{#step-1-archive-your-app-in-xcode-and-upload-it-to-app-store-connect\} 1. Trong Xcode, đặt đích build thành **Any iOS Device (arm64)**. <img src="/assets/shared/img/build-target.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> 2. Chọn **Product** > **Archive** từ thanh menu phía trên. <img src="/assets/shared/img/xcode-archive.webp" style={{ border: '1px solid #727272', width: '500px', display: 'block', margin: '0 auto' }} /> 3. Chờ quá trình lưu trữ hoàn tất. Cửa sổ **Organizer** sẽ tự động mở. Chọn bản lưu trữ của bạn và nhấp **Distribute App**. <img src="/assets/shared/img/distribute-app.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> 4. Chọn **App Store Connect** làm phương thức phân phối. Làm theo các bước hướng dẫn để hoàn tất việc tải lên. :::note Quá trình tải lên có thể thất bại nếu thiếu các tài nguyên bắt buộc, chẳng hạn như biểu tượng ứng dụng hoặc màn hình khởi động. Kiểm tra nhật ký lỗi trong Xcode để biết thêm chi tiết. ::: <img src="/assets/shared/img/distribution-method.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> ### Bước 2. Kiểm tra bản build trong App Store Connect \{#step-2-check-the-build-in-app-store-connect\} 1. Truy cập [App Store Connect](https://appstoreconnect.apple.com) và mở ứng dụng của bạn. 2. Cuộn đến phần **Build**. Đảm bảo rằng bản build bạn vừa tải lên xuất hiện ở đó. :::note Có thể mất vài phút để bản build xuất hiện trong App Store Connect sau khi tải lên. ::: <img src="/assets/shared/img/app-store-build.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> ## Gửi ứng dụng và sản phẩm để xét duyệt \{#submit-your-app-and-products-for-review\} Sau khi bản build xuất hiện trong phần **Build**, hãy đính kèm các gói đăng ký in-app và gửi ứng dụng để Apple xét duyệt. ### Bước 1. Đính kèm sản phẩm vào bản gửi \{#step-1-attach-products-to-the-submission\} Mỗi gói đăng ký phải có trạng thái **Ready to Submit** trong App Store Connect trước khi bạn có thể đính kèm. Nếu một gói đăng ký vẫn ở trạng thái nháp hoặc thiếu thông tin metadata, nó sẽ không xuất hiện trong danh sách. 1. Trên cùng trang đó, cuộn xuống phần **In-App Purchases and Subscriptions**. 2. Nhấp **Select in-app purchases or subscriptions**. <img src="/assets/shared/img/app-store-select-products.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> 3. Chọn tất cả sản phẩm bạn muốn đưa vào bản gửi này và nhấp **Done**. ### Bước 2. Gửi để xét duyệt \{#step-2-submit-for-review\} 1. Điền đầy đủ tất cả các trường bắt buộc trên trang (mô tả, ảnh chụp màn hình, từ khóa, v.v.). 2. Trong phần **App Store Version Release**, chọn cách bạn muốn phát hành ứng dụng: tự động, thủ công hoặc theo lịch sau khi được phê duyệt. 3. Nhấp **Add for Review**, sau đó nhấp **Submit to App Review**. Apple xét duyệt ứng dụng trong vòng 1–2 ngày, tuy nhiên thời gian xét duyệt có thể thay đổi. ## Xác minh ứng dụng trên môi trường production \{#verify-your-app-in-production\} Sau khi Apple phê duyệt ứng dụng của bạn: 1. Thực hiện một giao dịch mua thực tế (hoặc chờ người dùng đầu tiên của bạn mua hàng). 2. Mở [**Event Feed**](https://app.adapty.io/event-feed) trong Adapty Dashboard và xác nhận rằng các sự kiện giao dịch production xuất hiện. 3. Kiểm tra xem các sự kiện gói đăng ký (gia hạn, hủy) có hoạt động đúng không — điều này phụ thuộc vào việc [thông báo từ máy chủ App Store](enable-app-store-server-notifications) đã được cấu hình. Nếu sự kiện production không xuất hiện, hãy kiểm tra [cấu hình kết nối App Store](app-store-connection-configuration) của bạn. ## Các bước tiếp theo \{#next-steps\} Ứng dụng của bạn đã ra mắt. Bắt đầu tăng doanh thu từ gói đăng ký: - **[A/B testing](ab-tests)**: Thử nghiệm với các paywall khác nhau để tìm ra cách chuyển đổi tốt nhất. - **[Analytics](charts)**: Theo dõi các chỉ số gói đăng ký như MRR, churn và tỷ lệ chuyển đổi. - **Tích hợp**: Gửi sự kiện gói đăng ký đến các nền tảng [analytics](analytics-integration) và [attribution](attribution-integration). --- # File: general --- --- title: "Cài đặt ứng dụng" description: "Khám phá các cài đặt và cấu hình chung trong Adapty để sử dụng liền mạch." --- Bạn có thể điều hướng đến tab General của trang App Settings để quản lý hành vi, giao diện và chia sẻ doanh thu của ứng dụng. Tại đây, bạn có thể tùy chỉnh tên và biểu tượng ứng dụng, quản lý các khóa Adapty SDK và API, thiết lập trạng thái Chương trình Doanh nghiệp Nhỏ, và chọn múi giờ cho analytics và biểu đồ của ứng dụng. ## 1. Chi tiết ứng dụng \{#1-app-details\} <img src="/assets/shared/img/8fa2929-CleanShot_2023-04-21_at_15.16.222x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chọn tên và biểu tượng duy nhất đại diện cho ứng dụng của bạn trong giao diện Adapty. Lưu ý rằng tên và biểu tượng ứng dụng sẽ không ảnh hưởng đến tên và biểu tượng ứng dụng trên App Store hoặc Google Play. Ngoài ra, hãy chắc chắn chọn Danh mục ứng dụng phù hợp phản ánh đúng mục đích và nội dung của ứng dụng. Điều này sẽ giúp người dùng khám phá ứng dụng của bạn và đảm bảo nó xuất hiện trong các danh mục cửa hàng ứng dụng phù hợp. ## 2\. Thành viên Chương trình Doanh nghiệp Nhỏ và Phí Dịch vụ Giảm \{#2-member-of-small-business-program-and-reduced-service-fee\} <img src="/assets/shared/img/825e2be-CleanShot_2023-04-19_at_13.43.292x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Nếu tổ chức của bạn đã đăng ký [Chương trình Doanh nghiệp Nhỏ](app-store-small-business-program) của Apple hoặc [chương trình Phí Dịch vụ Giảm](google-reduced-service-fee) của Google, ứng dụng của bạn sẽ được hưởng mức hoa hồng cửa hàng giảm. Hãy thông báo cho Adapty nếu ứng dụng của bạn đã đăng ký chương trình hoa hồng giảm. Để đảm bảo tính toán chính xác, hãy chỉ định trạng thái của các chương trình này trong phần "Reduced Store Fee". Cài đặt phí giảm chỉ áp dụng cho các giao dịch trong tương lai. Hãy thay đổi trạng thái của bạn **trước khi** nó có hiệu lực, và Adapty sẽ điều chỉnh tỷ lệ hoa hồng. :::warning * Nếu bạn gia hạn tham gia chương trình phí giảm, hãy **thêm thêm một khoảng thời gian đủ điều kiện**. * Nếu bạn mất tư cách thành viên chương trình, hãy **thay đổi ngày hết hạn** của khoảng thời gian đủ điều kiện hiện tại. ::: Các bài viết sau đây khám phá chủ đề này sâu hơn: * [App Store Small Business Program](app-store-small-business-program) * [Google Reduced Service Fee](google-reduced-service-fee) ## 3\. Múi giờ báo cáo \{#3-reporting-timezone\} <img src="/assets/shared/img/47227f9-CleanShot_2023-04-19_at_13.45.302x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Chọn múi giờ tương ứng với vị trí bạn đang ở, hoặc nơi analytics và biểu đồ của ứng dụng có liên quan nhất. Chúng tôi khuyến nghị sử dụng cùng múi giờ với tài khoản App Store Connect hoặc Google Play Console để đảm bảo tính nhất quán. Lưu ý rằng cài đặt múi giờ này không ảnh hưởng đến các tích hợp bên thứ ba trong hệ thống Adapty, vốn sử dụng múi giờ UTC. Bạn có thể truy cập cài đặt múi giờ trong phần Reported timezone của tab General trên trang App Settings. Bạn cũng có thể chọn đặt cùng một múi giờ cho tất cả các ứng dụng trong tài khoản Adapty của mình bằng cách đánh dấu vào ô tương ứng. ## 4\. Định nghĩa lượt cài đặt cho analytics \{#4-installs-definition-for-analytics\} Chọn điều gì được định nghĩa là một sự kiện cài đặt mới trong analytics: | Cơ sở | Mô tả | |------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | New device_ids | <p>(Khuyến nghị) Mỗi lần cài đặt ứng dụng từ cửa hàng trên một thiết bị được tính là một lượt cài đặt mới. Điều này bao gồm cả lần cài đặt đầu tiên và cài đặt lại.</p><p>Lượt cài đặt được tính theo ID thiết bị và không bị ảnh hưởng bởi xác thực người dùng. Tạo hồ sơ người dùng (khi kích hoạt SDK hoặc đăng xuất), đăng nhập hoặc nâng cấp ứng dụng không tạo ra thêm sự kiện cài đặt.</p><p>Ví dụ: nếu cùng một ứng dụng được cài đặt trên 5 thiết bị khác nhau, bạn sẽ thấy 5 lượt cài đặt trong analytics.</p> | | New customer_user_ids | <p>Tùy chọn này dành cho các ứng dụng <InlineTooltip tooltip="xác định người dùng trong Adapty">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), [Unity](unity-identifying-users), [Kotlin Multiplatform](kmp-quickstart-identify), [Capacitor](capacitor-quickstart-identify)</InlineTooltip>. </p><p>Đối với người dùng đã đăng nhập, chỉ lần cài đặt đầu tiên gắn với customer user ID mới được tính là một lượt cài đặt. Cài đặt trên các thiết bị bổ sung không được tính là lượt cài đặt mới. </p><p>Người dùng ẩn danh (người dùng chưa đăng nhập) không được tính trong analytics. </p><p>Cài đặt lại ứng dụng hoặc đăng nhập lại không tạo thêm lượt cài đặt.</p> <p>Các cửa hàng ứng dụng và nền tảng attribution (như App Store Connect, Google Play Console và AppsFlyer) sử dụng phương pháp dựa trên thiết bị để đếm lượt cài đặt. Nếu bạn đếm lượt cài đặt theo customer user ID trong Adapty, số lượt cài đặt có thể khác với các dịch vụ bên ngoài này.</p><p>⚠️ Nếu bạn không xác định người dùng trong Adapty, sẽ không có lượt cài đặt nào được tính khi bật tùy chọn này.</p> | | New profiles in Adapty | (Legacy) Mỗi lần cài đặt, cài đặt lại ứng dụng và các hồ sơ người dùng ẩn danh được tạo ra trong quá trình đăng xuất đều được tính là lượt cài đặt mới. | Lưu ý rằng tùy chọn này chỉ ảnh hưởng đến trang [**Analytics**](https://app.adapty.io/analytics) và không tác động đến trang [**Overview**](https://app.adapty.io/overview), nơi bạn có thể cấu hình chế độ xem riêng. ## 5. Logic tăng giá trên App Store \{#5-app-store-price-increase-logic\} Để duy trì dữ liệu chính xác và tránh sự khác biệt giữa analytics Adapty và kết quả từ App Store Connect, điều quan trọng là phải chọn tùy chọn phù hợp khi điều chỉnh các cấu hình liên quan đến tăng giá trong App Store Connect. Vì vậy, bạn có thể chọn logic sẽ được áp dụng cho việc tăng giá gói đăng ký trong Adapty: <img src="/assets/shared/img/b766c8b-CleanShot_2023-07-18_at_19.28.18_22x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> - **Giá gói đăng ký cho người dùng hiện tại được giữ nguyên:** Bằng cách chọn tùy chọn này, giá hiện tại sẽ được giữ lại cho những người đăng ký hiện có, ngay cả khi bạn thay đổi giá trong App Store Connect. Điều này có nghĩa là người đăng ký hiện tại sẽ tiếp tục bị tính phí theo giá gói đăng ký ban đầu của họ. - **Khi giá gói đăng ký thay đổi trong App Store Connect, nó sẽ thay đổi cho cả người đăng ký hiện tại:** Nếu bạn chọn tùy chọn này, mọi thay đổi về giá được thực hiện trong App Store Connect cũng sẽ được áp dụng cho người đăng ký hiện có. Điều này có nghĩa là người đăng ký hiện tại sẽ bị tính phí theo giá mới phản ánh mức giá được cập nhật trong App Store Connect. :::warning Điều quan trọng cần lưu ý là tùy chọn được chọn không chỉ ảnh hưởng đến analytics trong Adapty mà còn tác động đến các tích hợp và hành vi xử lý giao dịch tổng thể. ::: Hãy đảm bảo rằng bạn chọn tùy chọn phù hợp với cách tiếp cận mong muốn của mình khi xử lý giá gói đăng ký cho người đăng ký hiện tại. Điều này sẽ giúp duy trì dữ liệu chính xác và đồng bộ hóa giữa analytics Adapty và kết quả thu được từ App Store Connect. ## 6. Chia sẻ mức độ truy cập có trả phí giữa các tài khoản người dùng \{#6-sharing-paid-access-between-user-accounts\} :::link Bài viết chính: [Chia sẻ mức độ truy cập có trả phí giữa các tài khoản người dùng](sharing-paid-access-between-user-accounts) ::: Cài đặt **Sharing paid access between user accounts** xác định Adapty sẽ làm gì khi có nhiều hơn một [hồ sơ người dùng](identifying-users) cố gắng truy cập cùng một giao dịch mua. Bạn có thể chỉ định cài đặt chia sẻ quyền truy cập riêng biệt cho [môi trường sandbox](test-purchases-in-sandbox). **Enabled (mặc định)** Người dùng đã xác định (những người có [Customer User ID](identifying-users#set-customer-user-id-on-configuration)) có thể chia sẻ cùng một [mức độ truy cập](access-level) do Adapty cung cấp nếu thiết bị của họ đăng nhập vào cùng một Apple/Google ID. Điều này hữu ích khi người dùng cài lại ứng dụng và đăng nhập bằng email khác — họ vẫn có thể truy cập vào giao dịch mua trước đó. Với tùy chọn này, nhiều người dùng đã xác định có thể dùng chung một mức độ truy cập. Dù mức độ truy cập được chia sẻ, tất cả các giao dịch trong quá khứ và tương lai vẫn được ghi lại dưới dạng sự kiện trong Customer User ID gốc để đảm bảo tính nhất quán của dữ liệu phân tích và lưu giữ toàn bộ lịch sử giao dịch — bao gồm thời gian dùng thử, mua gói đăng ký, gia hạn, v.v., đều được liên kết với cùng một hồ sơ người dùng. **Transfer access to new user** Người dùng đã xác định vẫn có thể tiếp tục truy cập [mức độ truy cập](access-level) do Adapty cung cấp, ngay cả khi họ đăng nhập bằng [Customer User ID](identifying-users#set-customer-user-id-on-configuration) khác hoặc cài lại ứng dụng, miễn là thiết bị đăng nhập vào cùng một Apple/Google ID. Khác với tùy chọn trước, Adapty sẽ chuyển giao dịch mua giữa các người dùng đã xác định. Điều này đảm bảo nội dung đã mua vẫn khả dụng, nhưng chỉ một người dùng có thể truy cập tại một thời điểm. Ví dụ: nếu UserA mua gói đăng ký và UserB đăng nhập trên cùng thiết bị đó và khôi phục giao dịch, UserB sẽ được cấp quyền truy cập gói đăng ký đó, còn UserA sẽ bị thu hồi. Nếu một trong hai người dùng (mới hoặc cũ) chưa được xác định, mức độ truy cập vẫn sẽ được chia sẻ giữa các hồ sơ người dùng đó trong Adapty. Dù mức độ truy cập được chuyển giao, tất cả các giao dịch trong quá khứ và tương lai vẫn được ghi lại dưới dạng sự kiện trong Customer User ID gốc để đảm bảo tính nhất quán của dữ liệu phân tích và lưu giữ toàn bộ lịch sử giao dịch — bao gồm thời gian dùng thử, mua gói đăng ký, gia hạn, v.v., đều được liên kết với cùng một hồ sơ người dùng. Sau khi chuyển sang **Transfer access to new user**, mức độ truy cập sẽ không được chuyển ngay lập tức giữa các hồ sơ người dùng. Quá trình chuyển giao cho từng mức độ truy cập cụ thể chỉ được kích hoạt khi Adapty nhận được sự kiện từ cửa hàng, chẳng hạn như gia hạn gói đăng ký, khôi phục, hoặc khi xác thực giao dịch. **Disabled** Hồ sơ người dùng đã xác định đầu tiên được cấp mức độ truy cập sẽ giữ nó mãi mãi. Đây là lựa chọn tốt nhất nếu logic nghiệp vụ của bạn yêu cầu giao dịch mua phải được gắn với một Customer User ID duy nhất. Lưu ý rằng mức độ truy cập vẫn được chia sẻ giữa các người dùng ẩn danh. Bạn có thể "gỡ liên kết" giao dịch mua bằng cách [xóa hồ sơ người dùng của chủ sở hữu](https://adapty.io/docs/vi/api-adapty/operations/deleteProfile). Sau khi xóa, mức độ truy cập sẽ khả dụng cho hồ sơ người dùng đầu tiên yêu cầu nó, dù là ẩn danh hay đã xác định. Việc tắt chia sẻ chỉ ảnh hưởng đến người dùng mới. Các gói đăng ký đã được chia sẻ giữa người dùng sẽ tiếp tục được chia sẻ ngay cả sau khi tắt tùy chọn này. :::warning Apple và Google yêu cầu in-app purchase phải được chia sẻ hoặc chuyển giao giữa các người dùng vì họ dựa vào Apple/Google ID để liên kết giao dịch mua. Nếu không có chia sẻ, việc khôi phục giao dịch mua có thể không hoạt động sau khi cài lại ứng dụng. Tắt chia sẻ có thể khiến người dùng không thể lấy lại quyền truy cập sau khi đăng nhập. Chúng tôi khuyến nghị chỉ tắt chia sẻ nếu người dùng của bạn **bắt buộc phải đăng nhập** trước khi thực hiện giao dịch mua. Nếu không, một người dùng đã xác định có thể mua gói đăng ký, đăng nhập vào tài khoản khác và mất quyền truy cập vĩnh viễn. ::: ### Tôi nên chọn cài đặt nào? \{#which-setting-should-i-choose\} | Ứng dụng của tôi... | Tùy chọn nên chọn | | ------------------------------------------------------------ | ------------------------------------------------------------ | | Không có hệ thống đăng nhập và chỉ sử dụng ID hồ sơ người dùng ẩn danh của Adapty. | Dùng tùy chọn mặc định, vì mức độ truy cập luôn được chia sẻ giữa các ID hồ sơ người dùng ẩn danh cho cả ba tùy chọn. | | Có hệ thống đăng nhập tùy chọn và cho phép khách hàng mua trước khi tạo tài khoản. | Chọn **Transfer access to new user** để đảm bảo những khách hàng mua khi chưa có tài khoản vẫn có thể khôi phục giao dịch sau này. | | Yêu cầu khách hàng tạo tài khoản trước khi mua, nhưng cho phép giao dịch mua được liên kết với nhiều Customer User ID. | Chọn **Transfer access to new user** để đảm bảo chỉ một Customer User ID có quyền truy cập tại một thời điểm, đồng thời vẫn cho phép người dùng đăng nhập bằng Customer User ID khác mà không mất quyền truy cập đã trả phí. | | Yêu cầu khách hàng tạo tài khoản trước khi mua, với quy tắc nghiêm ngặt ràng buộc giao dịch mua với một Customer User ID duy nhất. | Chọn **Disabled** để đảm bảo giao dịch không bao giờ được chuyển giao giữa các tài khoản. | ## 7. Khóa SDK và API \{#7-sdk-and-api-keys\} Sử dụng Public SDK key để tích hợp Adapty SDK vào ứng dụng của bạn, và Secret Key để truy cập Server API của Adapty. Bạn có thể tạo khóa mới hoặc thu hồi khóa hiện có khi cần. Để tạo token cho Developer CLI, hãy vào **Settings → Developer API**. Xem [Authentication](developer-cli-authentication). ## 8. Thiết bị kiểm thử \{#8-test-devices\} Chỉ định các thiết bị sẽ được sử dụng để kiểm thử nhằm đảm bảo chúng nhận được cập nhật tức thì cho các thay đổi về paywall hoặc placement, bỏ qua bất kỳ độ trễ bộ nhớ đệm nào. Để biết thêm thông tin, xem [Testing devices](test-devices). ## 9. Tính nhất quán biến thể xuyên placement \{#9-cross-placement-variation-stickiness\} Xác định thời gian bao lâu sau khi hoàn thành một test, người dùng vẫn được phục vụ với các biến thể trong test đó. Điều này ảnh hưởng đến độ chính xác của analytics và trải nghiệm người dùng — vì việc hiển thị cho người dùng một ưu đãi khác với những gì họ đã thấy trước đó có thể ảnh hưởng đến quyết định mua hàng của họ. Thời gian nhất quán tối đa và mặc định là 90 ngày. :::warning Hãy lưu ý những điều sau: - Thay đổi cài đặt này sẽ ảnh hưởng đến tất cả những người dùng đã nhận được một biến thể trước đó. Họ sẽ ngay lập tức đủ điều kiện cho một paywall mới khi thấy một placement, điều này có thể làm hỏng kết quả của các A/B test đang chạy. - Nếu thời gian nhất quán đã hết cho một người dùng, họ có thể được phục vụ với một paywall hoặc A/B test mới. Tuy nhiên, ngay cả khi đó, họ sẽ không thể tham gia vào bất kỳ cross-placement test nào khác. ::: ## 10. Xóa ứng dụng \{#10-delete-the-app\} Nếu bạn không còn cần một ứng dụng nữa, bạn có thể xóa nó khỏi Adapty. :::warning Xin lưu ý rằng hành động này không thể hoàn tác, và bạn sẽ không thể khôi phục ứng dụng hoặc dữ liệu của nó. ::: --- # File: ios-settings --- --- title: "Thông tin xác thực Apple App Store" description: "Cấu hình cài đặt iOS trong Adapty để quản lý gói đăng ký liền mạch." --- Để cấu hình thông tin xác thực App Store và đảm bảo SDK iOS của Adapty hoạt động tối ưu, hãy truy cập tab [iOS SDK](https://app.adapty.io/settings/ios-sdk) trong trang App Settings của Adapty Dashboard. Sau đó, cấu hình các thông số sau: <img src="/assets/shared/img/3d4087e-CleanShot_2023-06-26_at_13.27.042x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> | Trường | Mô tả | |----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Bundle ID** | [Bundle ID](app-store-connection-configuration#step-1-provide-bundle-id-and-apple-app-id) của ứng dụng. | | **In-app purchase API (StoreKit 2)** | [Khóa](app-store-connection-configuration#step-2-provide-issuer-id-and-key-id) để xác thực bảo mật và kiểm tra lịch sử giao dịch in-app purchase. | | **App Store Server Notifications** | URL dùng để bật [thông báo server2server](enable-app-store-server-notifications) từ App Store nhằm theo dõi và phản hồi các thay đổi trạng thái gói đăng ký của người dùng. | | **App Store Promotional Offers** | Khóa gói đăng ký dùng để tạo [ưu đãi](generate-in-app-purchase-key) trong Adapty cho các sản phẩm cụ thể. | | **Apple app ID** | ID ứng dụng trên App Store. Để tìm, hãy mở trang ứng dụng trong App Store Connect, chọn trang **App Information** từ menu bên trái và sao chép **Apple ID**. | | **App Store Connect shared secret (LEGACY)** | <p>**Khóa cũ dành cho Adapty SDK trước phiên bản v2.9.0**</p><p></p><p>[Khóa](app-store-connection-configuration#step-5-enter-app-store-shared-secret) dùng để xác thực biên lai và ngăn chặn gian lận trong ứng dụng.</p> | --- # File: google-play-store-connection-configuration --- --- title: "Cấu hình tích hợp Google Play Store" description: "Cấu hình kết nối Google Play Store trong Adapty để xử lý in-app purchase suôn sẻ." --- Phần này mô tả quy trình tích hợp ứng dụng di động của bạn được bán qua Google Play với Adapty. Bạn cần nhập dữ liệu cấu hình ứng dụng từ Play Store vào Adapty Dashboard. Bước này rất quan trọng để xác thực các giao dịch mua và nhận cập nhật gói đăng ký từ Play Store trong Adapty. Bạn có thể hoàn tất quá trình này trong lần onboarding đầu tiên hoặc thay đổi sau trong **App Settings** của Adapty Dashboard. :::danger Chỉ được phép thay đổi cấu hình trước khi bạn phát hành ứng dụng di động tích hợp Adapty paywall. Việc thay đổi sau khi phát hành sẽ phá vỡ tích hợp và các paywall sẽ ngừng hiển thị trong ứng dụng di động của bạn. ::: ## Bước 1. Cung cấp Package name \{#step-1-provide-package-name\} Package name là định danh duy nhất của ứng dụng trên Google Play Store. Đây là yêu cầu bắt buộc cho các chức năng cơ bản của Adapty, chẳng hạn như xử lý gói đăng ký. 1. Mở [Google Play Developer Console](https://play.google.com/console/u/0/developers). 2. Chọn ứng dụng mà bạn cần lấy ID. Cửa sổ **Dashboard** sẽ mở ra. <img src="/assets/shared/img/7889edb-package_name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Tìm product ID bên dưới tên ứng dụng và sao chép nó. 4. Mở [**App settings**](https://app.adapty.io/settings/android-sdk) từ menu trên cùng của Adapty. <img src="/assets/shared/img/b00066c-package_name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Trong tab **Android SDK** của cửa sổ **App settings**, dán **Package name** vừa sao chép vào. ## Bước 2. Tải lên tệp khóa tài khoản \{#step-2-upload-the-account-key-file\} 1. Tải lên tệp khóa riêng tư của tài khoản dịch vụ ở định dạng JSON mà bạn đã tạo ở bước [Tạo tệp khóa tài khoản dịch vụ](create-service-account) vào khu vực **Service account key file**. <img src="/assets/shared/img/20fdba1-service_key_file.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Đừng quên nhấn nút **Save** để xác nhận các thay đổi. **Tiếp theo** - [Bật Real-time developer notifications (RTDN) trong Google Play Console](enable-real-time-developer-notifications-rtdn) --- # File: enable-real-time-developer-notifications-rtdn --- --- title: "Bật thông báo nhà phát triển theo thời gian thực (RTDN) trong Google Play Console" description: "Luôn được thông báo về các sự kiện quan trọng và đảm bảo độ chính xác của dữ liệu bằng cách bật Thông báo nhà phát triển theo thời gian thực (RTDN) trong Google Play Console cho Adapty. Tìm hiểu cách thiết lập RTDN để nhận cập nhật tức thì về hoàn tiền và các sự kiện quan trọng khác từ Play Store" --- Việc thiết lập thông báo nhà phát triển theo thời gian thực (RTDN) rất quan trọng để đảm bảo độ chính xác của dữ liệu, vì nó cho phép bạn nhận cập nhật tức thì từ Play Store, bao gồm thông tin về hoàn tiền và các sự kiện khác. ## Bật thông báo \{#enable-notifications\} 1. Đảm bảo bạn đã bật **Google Cloud Pub/Sub**. Mở [liên kết này](https://console.cloud.google.com/flows/enableapi?apiid=pubsub) và chọn dự án ứng dụng của bạn. Nếu chưa bật **Google Cloud Pub/Sub**, bạn phải thực hiện tại đây. <img src="/assets/shared/img/pubsub.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Vào [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) từ menu trên cùng của Adapty và sao chép nội dung trong trường **Enable Pub/Sub API** bên cạnh tiêu đề **Google Play RTDN topic name**. <img src="/assets/shared/img/a72ff2d-copy_topic.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <p> </p> :::note Nếu nội dung trong trường **Enable Pub/Sub API** có định dạng sai (định dạng đúng bắt đầu bằng `projects/...`), hãy tham khảo phần [Sửa định dạng sai trong trường Enable Pub/Sub API](enable-real-time-developer-notifications-rtdn#fixing-incorrect-format-in-enable-pubsub-api-field) để được hỗ trợ. ::: 3. Mở [Google Play Console](https://play.google.com/console/), chọn ứng dụng của bạn, rồi vào **Monetize with Play** -> **Monetization setup**. Trong phần **Google Play Billing**, chọn hộp kiểm **Enable real-time notifications**. 4. Dán nội dung của trường **Enable Pub/Sub API** mà bạn đã sao chép trong **App Settings** của Adapty vào trường **Topic name**. 5. Nhấp **Save changes** trong Google Play Console. <img src="/assets/shared/img/e55ba0e-paste_topic_name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Kiểm tra thông báo \{#test-notifications\} Để kiểm tra xem bạn đã đăng ký nhận thông báo nhà phát triển theo thời gian thực thành công chưa: 1. Lưu các thay đổi trong cài đặt Google Play Console. 2. Bên dưới **Topic name** trong Google Play Console, nhấp **Send test notification**. <img src="/assets/shared/img/rtdn-test.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Vào [**App settings > Android SDK**](https://app.adapty.io/settings/android-sdk) trong Adapty. Nếu thông báo kiểm tra đã được gửi, bạn sẽ thấy trạng thái của nó phía trên tên topic. <img src="/assets/shared/img/rtdn-adapty-test.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sửa định dạng sai trong trường Enable Pub/Sub API \{#fixing-incorrect-format-in-enable-pubsub-api-field\} Nếu nội dung trong trường **Enable Pub/Sub API** có định dạng sai (định dạng đúng bắt đầu bằng `projects/...`), hãy làm theo các bước sau để khắc phục sự cố: ### 1. Xác minh việc bật API và phân quyền \{#1-verify-api-enablement-and-permissions\} Hãy đảm bảo cẩn thận rằng tất cả các API cần thiết đã được bật và quyền đã được cấp đúng cho service account. Dù bạn đã hoàn thành các bước này rồi, vẫn nên thực hiện lại để chắc chắn không bỏ sót bước nào. Lặp lại các bước trong các phần sau: 1. [Bật Developer APIs trong Google Play Console](enabling-of-devepoler-api) 2. [Tạo service account trong Google Cloud Console](create-service-account) 3. [Cấp quyền cho service account trong Google Play Console](grant-permissions-to-service-account) 4. [Tạo file khóa service account trong Google Play Console](create-service-account-key-file) 5. [Cấu hình tích hợp Google Play Store](google-play-store-connection-configuration) ### 2. Điều chỉnh chính sách Domain \{#2-adjust-domain-policies\} Thay đổi chính sách **Domain restricted contacts** và **Domain restricted sharing**: 1. Mở [Google Cloud Console](https://console.cloud.google.com/) và chọn dự án mà bạn đã tạo service account để quản lý ứng dụng. 2. Trong phần **Quick Access**, chọn **IAM & Admin**. <img src="/assets/shared/img/google-cloud-IAM-and-Admin.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Ở khung bên trái, chọn **Organization Policies**. 4. Tìm chính sách **Domain restricted contacts**. <img src="/assets/shared/img/google-cloud-policy-action.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp vào nút dấu ba chấm trong cột **Actions** và chọn **Edit policy**. 6. Trong cửa sổ chỉnh sửa chính sách: 1. Dưới **Policy source**, chọn radio button **Override parent's policy**. 2. Dưới **Policy enforcement**, chọn radio button **Replace**. 3. Dưới **Rules**, nhấp nút **ADD A RULE**. <img src="/assets/shared/img/google-cloud-edit-policy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Dưới **New rule** -> **Policy values**, chọn **Allow All**. <img src="/assets/shared/img/google-cloud-allow-all-policy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp **SET POLICY**. 7. Lặp lại các bước 4-6 cho chính sách **Domain restricted sharing**. Cuối cùng, tạo lại nội dung của trường **Enable Pub/Sub API** bên cạnh tiêu đề **Google Play RTDN topic name**. Trường này sẽ có định dạng đúng. Hãy nhớ chuyển **Policy source** trở lại **Inherit parent's policy** cho các chính sách đã cập nhật sau khi bạn đã bật thành công Thông báo nhà phát triển theo thời gian thực (RTDN). ## Chuyển tiếp sự kiện thô \{#raw-events-forwarding\} Đôi khi bạn vẫn muốn nhận các sự kiện S2S thô từ Google. Để tiếp tục nhận chúng khi sử dụng Adapty, chỉ cần thêm endpoint của bạn vào trường **URL for forwarding raw Google events**, và chúng tôi sẽ chuyển tiếp các sự kiện thô nguyên bản từ Google. <img src="/assets/shared/img/e388892-001774-September-22-GhkjOFbT.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- **Tiếp theo** Thiết lập Adapty SDK cho: - [Android](sdk-installation-android) - [React Native](sdk-installation-reactnative) - [Flutter](sdk-installation-flutter) - [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform) - [Unity](sdk-installation-unity) --- # File: apple-search-ads --- --- title: "Apple Ads" description: "Tích hợp Apple Ads với Adapty để tối ưu hóa chuyển đổi gói đăng ký." --- :::important Tích hợp Apple Ads trong **App settings** chỉ dùng cho phân tích cơ bản và cho các tích hợp SplitMetrics Acquire và Asapty. [Adapty Ads Manager](adapty-ads-manager) sử dụng một kết nối riêng. Kết nối tài khoản Apple Ads của bạn trong [Adapty Ads Manager](adapty-ads-manager-get-started). ::: Adapty có thể giúp bạn lấy dữ liệu attribution từ Apple Ads và phân tích các chỉ số của bạn theo phân khúc chiến dịch và từ khóa. Adapty tự động thu thập dữ liệu attribution cho Apple Ads thông qua SDK và AdServices Framework. Sau khi thiết lập tích hợp Apple Ads, Adapty sẽ bắt đầu nhận dữ liệu attribution từ Apple Ads. Bạn có thể dễ dàng truy cập và xem dữ liệu này trên trang hồ sơ người dùng. <img src="/assets/shared/img/ba4a3e9-CleanShot_2023-08-21_at_15.14.592x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Thiết lập tích hợp \{#set-up-integration\} ### Kết nối Adapty với framework AdServices \{#connect-adapty-to-the-adservices-framework\} Apple Ads thông qua [AdServices](https://developer.apple.com/documentation/adservices) yêu cầu một số cấu hình trong Adapty Dashboard, và bạn cũng cần bật tính năng này ở phía ứng dụng. Để thiết lập Apple Ads sử dụng framework AdServices qua Adapty, hãy làm theo các bước sau: #### Bước 1: Lấy public key \{#step-1-obtain-public-key\} Trong Adapty Dashboard, điều hướng đến [Settings -> Apple Ads.](https://app.adapty.io/settings/apple-search-ads) Tìm public key đã được tạo sẵn (Adapty cung cấp một cặp key cho bạn) và sao chép nó. <img src="/assets/shared/img/baa5998-CleanShot_2023-08-21_at_14.55.542x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::note Nếu bạn đang sử dụng dịch vụ thay thế hoặc giải pháp riêng của mình cho attribution trên Apple Ads, bạn có thể tải lên private key của riêng bạn. ::: #### Bước 2: Cấu hình quản lý người dùng trên Apple Ads \{#step-2-configure-user-management-on-apple-ads\} Trong [tài khoản Apple Ads](https://ads.apple.com/app-store) của bạn, hãy vào trang **Settings > User Management**. Để Adapty có thể lấy dữ liệu attribution, bạn cần mời một tài khoản Apple ID khác và cấp cho nó quyền truy cập API Account Manager. Bạn có thể dùng bất kỳ tài khoản nào bạn có quyền truy cập hoặc tạo một tài khoản mới chỉ dành cho mục đích này. Điều quan trọng là bạn phải có thể đăng nhập vào Apple Ads bằng Apple ID đó. <img src="/assets/shared/img/ec183b2-kdjsfldsfjkdsfdfd.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> #### Bước 3: Tạo thông tin xác thực API \{#step-3-generate-api-credentials\} Tiếp theo, đăng nhập vào tài khoản vừa thêm trong Apple Ads. Truy cập Settings -> API trong giao diện Apple Ads. Dán khóa công khai đã sao chép trước đó vào trường được chỉ định. Tạo thông tin xác thực API mới. #### Bước 4: Cấu hình Adapty với thông tin xác thực Apple Ads \{#step-4-configure-adapty-with-apple-ads-credentials\} Sao chép các trường Client ID, Team ID và Key ID từ cài đặt Apple Ads. Trong Adapty Dashboard, dán các thông tin xác thực này vào các trường tương ứng. <img src="/assets/shared/img/7356113-CleanShot_2023-08-21_at_15.08.512x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Kết nối ứng dụng với mạng AdServices \{#connect-your-app-to-the-adservices-network\} Sau khi hoàn tất [thiết lập AdServices framework](#connect-the-adservices-framework), Adapty sẽ tự động bắt đầu thu thập dữ liệu attribution Apple Search Ad. Bạn không cần thêm bất kỳ đoạn code SDK nào. Đối với ứng dụng iOS, dữ liệu attribution này sẽ **luôn** được ưu tiên hơn dữ liệu từ các nguồn khác. Nếu không muốn hành vi này, hãy *tắt* attribution ASA theo hướng dẫn bên dưới. ## Tắt tích hợp \{#disable-integration\} Để tắt attribution Apple Search Ads, mở tab [**App Settings** -> **Apple Search Ads**](https://app.adapty.io/settings/apple-search-ads), và gạt tắt công tắc **Receive Apple Search Ads attribution**. <img src="/assets/shared/img/asa-disable.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::warning Xin lưu ý rằng việc tắt tính năng này sẽ hoàn toàn ngừng nhận dữ liệu analytics từ ASA. Kết quả là, ASA sẽ không còn được sử dụng trong analytics hoặc gửi đến các tích hợp. Ngoài ra, SplitMetrics Acquire và Asapty sẽ ngừng hoạt động vì chúng dựa vào attribution của ASA để hoạt động đúng. Attribution đã nhận trước khi thay đổi này sẽ không bị ảnh hưởng. ::: ## Tải lên key của riêng bạn \{#uploading-your-own-keys\} :::note Tùy chọn Các bước này không bắt buộc cho attribution Apple Ads, chỉ cần thiết khi làm việc với các dịch vụ khác như Asapty hoặc giải pháp của riêng bạn. ::: Bạn có thể dùng cặp khóa public-private của riêng mình nếu đang sử dụng dịch vụ khác hoặc giải pháp riêng cho attribution ASA. ### Bước 1 \{#step-1\} Tạo private key trong Terminal ```text showLineNumbers title="Text" openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem ``` Tải lên trong Adapty Settings -> Apple Ads (nút Upload private key) ### Bước 2 \{#step-2\} Tạo public key trong Terminal ```text showLineNumbers title="Text" openssl ec -in private-key.pem -pubout -out public-key.pem ``` Bạn có thể dùng public key này trong cài đặt Apple Ads của tài khoản có vai trò API Account Manager. Vì vậy, bạn có thể sử dụng các giá trị Client ID, Team ID và Key ID đã tạo cho Adapty và các dịch vụ khác. --- # File: account --- --- title: "Thông tin tài khoản & Thanh toán" description: "Quản lý tài khoản Adapty của bạn và tối ưu cài đặt để theo dõi gói đăng ký tốt hơn." --- Trang **Account** cho phép bạn quản lý hồ sơ người dùng, thành viên nhóm và thanh toán. Trang này có ba tab: - [Chung](#general-settings) - [Gói đăng ký & Thanh toán](#billing-info) - [Thành viên](#members) Để truy cập cài đặt tài khoản, nhấp vào **Account** ở góc trên bên phải hoặc truy cập [app.adapty.io/account](https://app.adapty.io/account). <img src="/assets/shared/img/account-info.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Cài đặt chung \{#general-settings\} Tab General chứa hồ sơ người dùng, cài đặt tài khoản, tùy chọn hiển thị và cấu hình báo cáo. - **Profile**: Nhập họ, tên và tên công ty. Tên công ty có thể dài tối đa 256 ký tự. - **Account settings**: Xem địa chỉ email đã đăng ký và thay đổi mật khẩu. - **Date & Time formats**: Chọn cách hiển thị ngày và giờ trong Adapty: - **American format**: January 31, 2022 và giờ 12 tiếng (AM/PM) - **European format**: 31 January, 2022 và giờ 24 tiếng (16:00) - **Email reports**: Thiết lập báo cáo hàng ngày, hàng tuần hoặc hàng tháng cho một hoặc tất cả ứng dụng của bạn. Nhận báo cáo tổng hợp cho tất cả ứng dụng cùng lúc, hoặc nhận báo cáo chi tiết cho từng ứng dụng được chọn. <img src="/assets/shared/img/account-info.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Gói đăng ký & Thanh toán \{#billing-info\} Tab **Subscription & Billing** cho phép bạn quản lý thông tin thanh toán và quyền truy cập tính năng: - Thêm hoặc cập nhật thông tin thanh toán - Xem lại thông tin hóa đơn - Mua thêm các tính năng trả phí Tìm hiểu thêm về [tính năng và bảng giá](https://adapty.io/pricing). ## Thành viên \{#members\} Bạn có thể quản lý thành viên nhóm trong cài đặt tài khoản. Để thêm thành viên, hãy mời họ qua email và gán vai trò cho họ. Đọc thêm về cách quản lý thành viên nhóm và quyền truy cập của họ [tại đây](members-settings). --- # File: members-settings --- --- title: "Thành viên" description: "Quản lý cài đặt và quyền hạn của thành viên trên dashboard của Adapty." --- :::note Trang này nói về các thành viên trên Adapty Dashboard Nếu bạn muốn cấp các mức độ truy cập khác nhau cho người dùng ứng dụng, hãy xem [Mức độ truy cập](access-level). ::: Hệ thống thành viên trên Adapty Dashboard cho phép bạn cấp các mức quyền truy cập khác nhau vào Adapty và chỉ định ứng dụng cho từng thành viên. ## Vai trò \{#roles\} Các vai trò sau đây có sẵn cho các thành viên trong Adapty Dashboard: | Vai trò | Truy cập Thanh toán | Thêm thành viên mới | Thay đổi bất kỳ | Truy cập tất cả mục | |-------------|---------------------|---------------------|-----------------|------------------------| | Owner | ✅ | ✅ | ✅ | ✅ | | Admin | ❌ | ✅ | ✅ | ✅ | | Developer | ❌ | ❌ | ✅ | ❌ | | Viewer | ❌ | ❌ | ❌ | ✅ | | Support | ❌ | ❌ | ❌ | ❌ | | ASA manager | ❌ | ❌ | ❌ | ❌ | - **Owner:** Owner là người tạo tài khoản Adapty ban đầu và có mức độ truy cập cũng như quyền kiểm soát cao nhất. Owner có toàn quyền truy cập vào phần thanh toán của Adapty, cho phép quản lý thông tin thanh toán và các gói đăng ký. Ngoài ra, chỉ Owner và Admin mới có thể chỉ định quyền truy cập ứng dụng cho thành viên mới. Mỗi tài khoản Adapty chỉ có thể có một Owner. - **Admin:** Thành viên với vai trò Admin có toàn quyền truy cập vào các ứng dụng được chỉ định. Họ có thể thực hiện nhiều tác vụ quản lý, bao gồm tạo và chỉnh sửa paywall, thực hiện A/B test, phân tích analytics và quản lý thành viên trong các ứng dụng đó. - **Developer**: Thành viên với vai trò Developer có toàn quyền truy cập vào tất cả các thực thể, ngoại trừ analytics và quản lý thành viên. Họ không thể truy cập bất kỳ cài đặt thanh toán nào. Vai trò này dành cho những người thiết lập paywall, A/B test và các thực thể khác, đồng thời tích hợp Adapty vào ứng dụng, nhưng không cần xem dữ liệu tài chính. - **Viewer:** Thành viên với vai trò Viewer chỉ có quyền đọc trên các ứng dụng được chỉ định. Họ có thể xem thông tin nhưng không thể tạo hoặc chỉnh sửa paywall, A/B test và các tính năng khác, mời người dùng mới, tạo ứng dụng mới hay thay đổi cài đặt ứng dụng. - **Support:** Thành viên với vai trò Support chỉ có quyền truy cập vào hồ sơ người dùng trong các ứng dụng được chỉ định. Tuy nhiên, họ không thể thực hiện các thao tác như thêm thành viên mới hay truy cập các mục khác trong Adapty. Vai trò này đặc biệt phù hợp với các nhóm hỗ trợ hoặc những người cần giúp khách hàng giải quyết các vấn đề liên quan đến gói đăng ký. - **ASA manager**: Thành viên với vai trò ASA manager chỉ có quyền truy cập vào dashboard [Adapty Ads Manager](adapty-ads-manager). ## Thêm thành viên \{#add-a-member\} Trong Adapty, bạn có thể mời tối đa 256 thành viên vào nhóm. Việc thêm thành viên mới hoàn toàn miễn phí. :::note Bạn chỉ có thể mời các địa chỉ email chưa được đăng ký trong Adapty. Nếu đồng nghiệp của bạn đã có tài khoản riêng, hãy mời một địa chỉ email khác hoặc liên hệ bộ phận hỗ trợ của Adapty để xóa tài khoản hiện tại của họ. ::: Để thêm một thành viên vào nhóm: 1. Nhấp vào **Account** ở góc trên bên phải và mở tab **Members**. 2. Nhấp vào **Invite member**. <img src="/assets/shared/img/invite-member.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhập địa chỉ email của thành viên. 4. Chọn một [vai trò](#roles) từ danh sách. 5. Chọn các ứng dụng cần cấp quyền truy cập. 6. (Tùy chọn) Bật **Always allow access to new apps** để tự động cấp quyền truy cập cho các ứng dụng trong tương lai. 7. Nhấp vào **Save**. <img src="/assets/shared/img/add-member.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Chuyển giao quyền sở hữu tài khoản \{#transfer-account-ownership\} Nếu bạn cần chuyển giao toàn bộ **quyền sở hữu tài khoản**, hãy liên hệ đội ngũ hỗ trợ của chúng tôi tại [support@adapty.io](mailto:support@adapty.io). Nếu bạn cần chuyển giao **quyền sở hữu ứng dụng**, hãy đọc [hướng dẫn chuyên biệt](transfer-apps) để biết thêm thông tin. --- # File: set-up-app-store-connect --- --- title: "Thiết lập App Store Connect" description: "Hướng dẫn cho nhà phát triển lần đầu đăng ký Apple Developer Program và thiết lập App Store Connect để bán in-app purchase." --- Nếu bạn đang **xây dựng ứng dụng iOS đầu tiên**, bạn cần thiết lập tài khoản Apple Developer và App Store Connect trước khi tích hợp Adapty. :::note Nếu bạn đã có tài khoản Apple Developer và ứng dụng đã được đăng ký trong App Store Connect, bạn có thể bỏ qua hướng dẫn này và chuyển thẳng đến [Tích hợp ban đầu với App Store](initial_ios). ::: ## Bước 1. Đăng ký Apple Developer Program \{#step-1-enroll-in-apple-developer-program\} Để phân phối ứng dụng trên App Store và bán in-app purchase, bạn phải tham gia [Apple Developer Program](https://developer.apple.com/programs/). ### Chọn loại đăng ký \{#choose-enrollment-type\} Apple cung cấp hai loại đăng ký: | | Cá nhân | Tổ chức | |----------------------------------|--------------------|-----------------------------------| | **Dành cho** | Nhà phát triển độc lập | Công ty, nhóm, tổ chức phi lợi nhuận | | **Yêu cầu D-U-N-S Number** | Không | Có | | **Ứng dụng được đăng tải dưới** | Tên cá nhân của bạn | Tên tổ chức của bạn | | **Quản lý nhóm** | Không có | Có | :::tip Nếu bạn đăng ký với tư cách tổ chức, bạn cần có **D-U-N-S Number** — mã định danh doanh nghiệp gồm chín chữ số do Dun & Bradstreet cấp. Bạn có thể [kiểm tra xem tổ chức của mình đã có chưa](https://developer.apple.com/enroll/duns-lookup/) hoặc đăng ký mới — đường link ở cuối trang tra cứu. Việc nhận D-U-N-S Number có thể mất đến 5 ngày làm việc. ::: ### Đăng ký \{#enroll\} 1. Truy cập [trang đăng ký Apple Developer Program](https://developer.apple.com/programs/enroll/). 2. Đăng nhập bằng Apple ID của bạn. Nếu chưa có, hãy tạo tài khoản trước. 3. Làm theo các bước phù hợp với loại đăng ký của bạn (cá nhân hoặc tổ chức). 4. Thanh toán phí hàng năm. Sau khi Apple xử lý đơn đăng ký, bạn sẽ được cấp quyền truy cập vào [App Store Connect](https://appstoreconnect.apple.com). Thông thường quá trình đăng ký mất đến 48 giờ. Với tổ chức, có thể lâu hơn nếu cần xác minh D-U-N-S. ## Bước 2. Thiết lập ứng dụng trong App Store Connect \{#step-2-set-up-your-app-in-app-store-connect\} Trước khi có thể bán in-app purchase, bạn cần hoàn tất các bước thiết lập ban đầu trong App Store Connect, bao gồm ký thỏa thuận, thêm thông tin thanh toán và đăng ký ứng dụng. ### Ký Paid Applications Agreement \{#sign-the-paid-applications-agreement\} Apple yêu cầu bạn ký Paid Applications Agreement trước khi bán trên App Store. Điều này áp dụng cho cả ứng dụng trả phí lẫn in-app purchase trong ứng dụng miễn phí. 1. Vào trang **Business** trong [App Store Connect](https://appstoreconnect.apple.com/business). 2. Tìm thỏa thuận **Paid Apps** và nhấn **Review and Agree**. 3. Điền đầy đủ các thông tin yêu cầu: - **Banking information**: Thêm tài khoản ngân hàng để Apple chuyển tiền thanh toán cho bạn. - **Tax information**: Điền các mẫu thuế cho các quốc gia bạn muốn bán. - **Contact information**: Cung cấp thông tin liên hệ của bạn. :::important Bạn phải hoàn tất cả ba phần (ngân hàng, thuế, liên hệ) để thỏa thuận có hiệu lực. Cho đến khi thỏa thuận có hiệu lực, bạn không thể bán in-app purchase. ::: ### Tạo Bundle ID \{#create-a-bundle-id\} Bundle ID giúp xác định duy nhất ứng dụng của bạn trong hệ sinh thái Apple. Bạn cần nó để đăng ký ứng dụng trong App Store Connect và cấu hình tích hợp Adapty. 1. Mở [Apple Developer portal](https://developer.apple.com/account). 2. Vào **Certificates, Identifiers & Profiles** → **Identifiers**. 3. Nhấn **+** để đăng ký định danh mới. 4. Chọn **App IDs** và nhấn **Continue**. 5. Chọn **App** làm loại và nhấn **Continue**. 6. Điền các trường thông tin: - **Description**: Tên giúp bạn nhận biết Bundle ID này (ví dụ: "My Subscription App"). - **Bundle ID**: Chọn **Explicit** và nhập mã định danh duy nhất theo định dạng reverse-domain (ví dụ: `com.yourcompany.yourapp`). 7. Trong phần **Capabilities**, cuộn xuống và chọn **In-App Purchase**. 8. Nhấn **Continue**, rồi **Register**. ### Đăng ký ứng dụng trong App Store Connect \{#register-your-app-in-app-store-connect\} 1. Vào trang **Apps** trong [App Store Connect](https://appstoreconnect.apple.com/apps). 2. Nhấn **+** → **New App**. 3. Điền các trường bắt buộc: - **Platforms**: Chọn **iOS**. - **Name**: Tên ứng dụng sẽ hiển thị trên App Store. - **Primary language**: Ngôn ngữ mặc định cho metadata của ứng dụng. - **Bundle ID**: Chọn Bundle ID bạn đã tạo ở bước trước. - **SKU**: Mã định danh duy nhất cho ứng dụng (người dùng không nhìn thấy). Ví dụ: `my_subscription_app_2025`. 4. Nhấn **Create**. Ứng dụng của bạn đã được đăng ký trong App Store Connect và sẵn sàng để tích hợp Adapty. ## Tiếp theo \{#whats-next\} - [Tích hợp ban đầu với App Store](initial_ios): Kết nối ứng dụng App Store của bạn với Adapty - [Tích hợp SDK](quickstart-sdk): Tích hợp Adapty SDK vào code ứng dụng - [Kiểm thử trong Sandbox](test-purchases-in-sandbox): Kiểm tra in-app purchase trước khi phát hành - [Nộp ứng dụng iOS lên App Store](submit-app-to-app-store): Tải bản build lên và nộp để Apple xét duyệt - [App Store Small Business Program](app-store-small-business-program): Giảm hoa hồng App Store từ 30% xuống 15% --- # File: app-store-products --- --- title: "Sản phẩm trên App Store" description: "Quản lý sản phẩm App Store hiệu quả bằng các công cụ gói đăng ký của Adapty." --- Trang này hướng dẫn cách tạo sản phẩm trong App Store Connect. Mặc dù thông tin này không trực tiếp liên quan đến chức năng của Adapty, nhưng đây là tài liệu hữu ích nếu bạn gặp khó khăn khi tạo sản phẩm trong tài khoản App Store Connect của mình. Để tạo sản phẩm và liên kết với Adapty: 1. Mở **App Store Connect**. Vào mục [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) trong menu bên trái. <img src="/assets/shared/img/148c3b5-subscriptions.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nếu bạn chưa tạo nhóm gói đăng ký, hãy nhấn nút **Create** dưới tiêu đề **Subscription Groups** để bắt đầu. [Subscription Groups](https://developer.apple.com/help/app-store-connect/manage-subscriptions/offer-auto-renewable-subscriptions) trong App Store Connect giúp phân loại và quản lý các sản phẩm của bạn, cho phép người dùng chuyển đổi giữa các gói một cách liền mạch. Lưu ý rằng không thể tạo gói đăng ký nằm ngoài một nhóm. 3. Trong cửa sổ **Create Subscription Group** vừa mở, nhập tên nhóm gói đăng ký mới vào trường **Reference Name**. Reference Name là nhãn hoặc định danh do bạn đặt, giúp bạn phân biệt và quản lý các nhóm gói đăng ký khác nhau trong ứng dụng. Reference Name không hiển thị với người dùng; đây chủ yếu là để bạn sử dụng nội bộ và sắp xếp tổ chức. Nó giúp bạn dễ dàng nhận biết và tham chiếu đến các nhóm gói đăng ký cụ thể khi quản lý trong giao diện App Store Connect. Điều này đặc biệt hữu ích nếu bạn có nhiều gói đăng ký hoặc muốn phân loại chúng theo cách phù hợp với cấu trúc ứng dụng của mình. <img src="/assets/shared/img/3f93c44-create_subscription_group.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấn nút **Create** để xác nhận tạo nhóm gói đăng ký. 5. Nhóm gói đăng ký được tạo và mở ra. Bây giờ bạn có thể tạo các gói đăng ký trong nhóm. Nhấn nút **Create** dưới tiêu đề **Subscriptions**. Nếu bạn thêm gói đăng ký mới vào nhóm đã có, hãy nhấn nút **Plus** bên cạnh tiêu đề **Subscriptions**. <img src="/assets/shared/img/22fc643-add_subscription.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Trong cửa sổ **Create Subscription** vừa mở, nhập tên vào trường **Reference Name** và mã định danh duy nhất của gói đăng ký vào trường **Product ID**. Reference Name là định danh độc nhất trong App Store Connect cho in-app purchase của bạn. Người dùng sẽ không thấy tên này trên App Store. Chúng tôi khuyên bạn nên sử dụng mô tả rõ ràng, dễ đọc, phản ánh chính xác gói đăng ký bạn định tạo. Lưu ý rằng tên này không được vượt quá 64 ký tự. Product ID là định danh dạng chữ-số duy nhất, cần thiết để truy cập sản phẩm trong quá trình phát triển và đồng bộ hóa với Adapty — dịch vụ quản lý in-app purchase. Product ID chỉ được chứa ký tự chữ-số, dấu chấm và dấu gạch dưới. <img src="/assets/shared/img/04aca55-create_subscription.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Nhấn nút **Create** để xác nhận tạo gói đăng ký. 8. Gói đăng ký được tạo và mở ra. Bây giờ hãy chọn thời hạn của gói đăng ký trong danh sách **Subscription Duration**. Dù thời hạn đã được đề cập trong tên gói đăng ký, bạn vẫn cần điền vào trường **Subscription Duration**. <img src="/assets/shared/img/f56cf0f-subscription_duration.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 9. Tiếp theo, hãy thiết lập giá cho gói đăng ký. Nhấn nút **Add Subscription Price** dưới tiêu đề Subscription Prices. Bạn có thể cần cuộn xuống để tìm thấy nút này. 10. Trong cửa sổ **Subscription Price** vừa mở, chọn quốc gia cơ sở trong danh sách **Country or Region** và đơn vị tiền tệ cơ sở trong danh sách **Price**. Sau đó, Apple sẽ tự động tính giá cho tất cả 175 quốc gia hoặc khu vực dựa trên mức giá cơ sở này và tỷ giá hối đoái mới nhất. <img src="/assets/shared/img/de1cec8-subscription_price.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 11. Nhấn nút **Next**. Trong cửa sổ **Price by Country or Region** vừa mở, bạn sẽ thấy giá đã được tính lại tự động cho tất cả các quốc gia. Bạn có thể thay đổi nếu muốn. <img src="/assets/shared/img/2a047a6-price_by_country.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 12. Sau khi cập nhật giá theo khu vực, nhấn nút **Next** ở cuối cửa sổ để tiếp tục. 13. Trong cửa sổ **Confirm Subscription Price?** vừa mở, hãy xem xét kỹ các mức giá cuối cùng. Để chỉnh sửa giá, bạn có thể nhấn nút **Back** để quay lại cửa sổ **Price by Country or Region** và cập nhật lại. Khi đã hài lòng với các mức giá, nhấn nút **Confirm**. <img src="/assets/shared/img/d2b2031-confirm_prices.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 14. Sau khi đóng cửa sổ **Confirm Subscription Price?**, nhớ nhấn nút **Save** trong cửa sổ gói đăng ký của bạn. Nếu không, gói đăng ký sẽ không được tạo và toàn bộ dữ liệu đã nhập sẽ bị mất. Lưu ý rằng các bước trên tập trung vào việc cấu hình Auto-Renewable Subscription. Tuy nhiên, nếu bạn muốn thiết lập các loại in-app purchase khác, hãy nhấn vào tab **In-App Purchases** trên thanh sidebar thay vì "Subscriptions". Đây là nơi bạn có thể quản lý và tạo nhiều loại in-app purchase khác nhau. <img src="/assets/shared/img/5663d85-in-app_purchases.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Thêm sản phẩm vào Adapty \{#add-products-to-adapty\} Sau khi hoàn tất việc thêm in-app purchase, gói đăng ký và ưu đãi trong App Store Connect, bước tiếp theo là [thêm các sản phẩm này vào Adapty](create-product). --- # File: apple-app-privacy --- --- title: "Quyền riêng tư ứng dụng Apple" description: "Tìm hiểu về chính sách quyền riêng tư ứng dụng của Apple và tác động của chúng đến ứng dụng đăng ký của bạn." --- Apple yêu cầu khai báo quyền riêng tư cho tất cả ứng dụng mới và các bản cập nhật ứng dụng, cả trong phần **App Privacy** của App Store Connect lẫn trong file manifest của ứng dụng. Adapty là một dependency bên thứ ba trong ứng dụng của bạn, vì vậy bạn cần khai báo cách sử dụng Adapty liên quan đến dữ liệu người dùng. ## File manifest quyền riêng tư ứng dụng Apple \{#apple-app-privacy-manifest\} [File privacy manifest](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests), có tên `PrivacyInfo.xcprivacy`, mô tả dữ liệu riêng tư mà ứng dụng của bạn sử dụng và lý do. Mỗi chủ sở hữu ứng dụng đều phải tạo file manifest cho ứng dụng của mình. Ngoài ra, nếu bạn tích hợp thêm các SDK khác, hãy đảm bảo rằng các file manifest của những SDK nằm trong danh sách [SDKs that require a privacy manifest and signature](https://developer.apple.com/support/third-party-SDK-requirements/) đã được đưa vào. Khi bạn build ứng dụng, Xcode sẽ lấy tất cả các file manifest này và gộp chúng lại thành một. Mặc dù Adapty không có trong danh sách [SDKs that require a privacy manifest and signature](https://developer.apple.com/support/third-party-SDK-requirements/), nhưng Adapty SDK từ phiên bản 2.10.2 trở lên đã bao gồm file này để tiện cho bạn. Hãy đảm bảo cập nhật SDK để có được file manifest. Mặc dù Adapty không yêu cầu bất kỳ dữ liệu nào phải được đưa vào file manifest (còn gọi là báo cáo quyền riêng tư ứng dụng), nhưng nếu bạn đang dùng `customerUserId` của Adapty để theo dõi, bạn cần khai báo điều đó trong file manifest như sau: 1. Thêm một dictionary vào mảng `NSPrivacyCollectedDataTypes` trong file thông tin quyền riêng tư của bạn. 2. Thêm các key `NSPrivacyCollectedDataType`, `NSPrivacyCollectedDataTypeLinked`, và `NSPrivacyCollectedDataTypeTracking` vào dictionary. 3. Thêm chuỗi `NSPrivacyCollectedDataTypeUserID` (định danh của loại dữ liệu `UserID` trong [Danh sách các danh mục và loại dữ liệu cần khai báo trong file manifest](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests#Describe-the-data-your-app-or-third-party-SDK-collects)) cho key `NSPrivacyCollectedDataType` trong dictionary `NSPrivacyCollectedDataTypes` của bạn. 4. Thêm `true` cho các key `NSPrivacyCollectedDataTypeTracking` và `NSPrivacyCollectedDataTypeLinked` trong dictionary `NSPrivacyCollectedDataTypes` của bạn. 5. Dùng chuỗi `NSPrivacyCollectedDataTypePurposeProductPersonalization` làm giá trị cho key `NSPrivacyCollectedDataTypePurposes` trong dictionary `NSPrivacyCollectedDataTypes` của bạn. Nếu bạn nhắm mục tiêu paywall đến các đối tượng với thuộc tính tùy chỉnh, hãy cân nhắc kỹ những thuộc tính tùy chỉnh bạn sử dụng và xem chúng có khớp với [các danh mục và loại dữ liệu cần khai báo trong file manifest](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests) hay không. Nếu có, hãy lặp lại các bước trên cho từng loại dữ liệu. Sau khi khai báo tất cả các loại và danh mục dữ liệu bạn thu thập, hãy tạo báo cáo quyền riêng tư cho ứng dụng của bạn như mô tả trong [tài liệu Apple](https://developer.apple.com/documentation/bundleresources/describing-data-use-in-privacy-manifests#Create-your-apps-privacy-report). ## Khai báo quyền riêng tư ứng dụng Apple trong App Store Connect \{#apple-app-privacy-disclosure-in-app-store-connect\} 1. Trong [App Store Connect](https://appstoreconnect.apple.com/), mở ứng dụng của bạn và vào **App Privacy**. Nhấp **Get Started**. <img src="/assets/shared/img/app-privacy-get-started.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Chọn **Yes, we collect data from this app** và nhấp **Next**. <img src="/assets/shared/img/app-privacy-data-collection.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Các loại dữ liệu \{#data-types\} Bảng dưới đây liệt kê các loại dữ liệu mà Apple yêu cầu bạn khai báo và cho biết loại nào Adapty cần. **Phần này chỉ đề cập đến Adapty.** Nếu ứng dụng của bạn thu thập thêm dữ liệu qua các SDK khác hoặc code của bạn, hãy chọn thêm những loại dữ liệu đó. ✅ = Adapty yêu cầu 👀 = Có thể cần thiết \(xem chi tiết bên dưới\) ❌ = Adapty không yêu cầu — chọn nếu ứng dụng của bạn thu thập dữ liệu này qua các phương tiện khác | Loại dữ liệu | Yêu cầu | Ghi chú | |--------------------------------------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------| | Identifiers | ✅ | <p>Nếu bạn nhận dạng người dùng bằng customerUserId, hãy chọn 'User ID'.</p><p></p><p>Adapty thu thập IDFA, vì vậy bạn phải chọn 'Device ID'.</p> | | Purchases | ✅ | Adapty thu thập lịch sử mua hàng của người dùng. | | Contact Info, bao gồm tên, số điện thoại hoặc địa chỉ email | 👀 | Bắt buộc nếu bạn truyền dữ liệu cá nhân như tên, số điện thoại hoặc địa chỉ email bằng phương thức **`updateProfile`**. | | Usage Data | 👀 | Nếu bạn đang dùng các SDK analytics như Amplitude, Mixpanel, AppMetrica hoặc Firebase, điều này có thể được yêu cầu. | | Location | ❌ | Adapty không thu thập dữ liệu vị trí chính xác. Chọn nếu ứng dụng của bạn thu thập. | | Health & Fitness | ❌ | Adapty không thu thập dữ liệu sức khỏe hoặc thể dục. Chọn nếu ứng dụng của bạn thu thập. | | Sensitive Info | ❌ | Adapty không thu thập thông tin nhạy cảm. Chọn nếu ứng dụng của bạn thu thập. | | User Content | ❌ | Adapty không thu thập nội dung người dùng. Chọn nếu ứng dụng của bạn thu thập. | | Diagnostics | ❌ | Adapty không thu thập dữ liệu chẩn đoán. Chọn nếu ứng dụng của bạn thu thập. | | Browsing History | ❌ | Adapty không thu thập lịch sử duyệt web. Chọn nếu ứng dụng của bạn thu thập. | | Search History | ❌ | Adapty không thu thập lịch sử tìm kiếm. Chọn nếu ứng dụng của bạn thu thập. | | Contacts | ❌ | Adapty không thu thập danh sách liên hệ. Chọn nếu ứng dụng của bạn thu thập. | | Financial Info | ❌ | Adapty không thu thập thông tin tài chính. Chọn nếu ứng dụng của bạn thu thập. | ### Các loại dữ liệu bắt buộc \{#required-data-types\} #### Purchases \{#purchases\} Khi sử dụng Adapty, bạn phải khai báo rằng ứng dụng của bạn thu thập **Purchase History**. <img src="/assets/shared/img/feb3b9f-CleanShot_2023-08-25_at_12.32.552x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> #### Identifiers \{#identifiers\} Khi sử dụng Adapty, bạn phải khai báo các identifier sau: - **Device ID** — Adapty thu thập IDFA. - **User ID** — bắt buộc nếu bạn nhận dạng người dùng bằng **`customerUserId`**. <img src="/assets/shared/img/93f3daa-CleanShot_2023-08-25_at_12.35.272x.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Mục đích sử dụng dữ liệu \{#data-usage\} Sau khi lưu **Data types**, bạn sẽ cần chỉ rõ dữ liệu được sử dụng như thế nào: 1. Nhấp **Set up purchase history** trong khối **Purchases**. <img src="/assets/shared/img/purchase-privacy.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Khi Apple hỏi dữ liệu lịch sử mua hàng được sử dụng như thế nào, hãy chọn các mục sau cho Adapty: - **Analytics** — Adapty sử dụng lịch sử mua hàng cho analytics doanh thu, cohort và các chỉ số. - **Product Personalization** — Adapty sử dụng dữ liệu mua hàng để phân khúc đối tượng và nhắm mục tiêu paywall. - **App Functionality** — Adapty xác thực các giao dịch mua, quản lý mức độ truy cập và theo dõi trạng thái gói đăng ký. Chọn thêm các mục đích khác nếu ứng dụng của bạn sử dụng dữ liệu mua hàng theo những cách khác (ví dụ: nếu bạn gửi sự kiện mua hàng đến các nền tảng quảng cáo qua tích hợp Adapty). <img src="/assets/shared/img/purchase-history.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp **Next**. 4. Đối với cả **Device ID** và **User ID** (nếu được sử dụng): 1. Nhấp **Set up user/device ID** trong khối **User/Device ID**. 2. Khi Apple hỏi dữ liệu identifier được sử dụng như thế nào, hãy chọn các mục sau cho Adapty: - **App Functionality** — Adapty sử dụng identifier để quản lý hồ sơ người dùng, liên kết các giao dịch mua và theo dõi mức độ truy cập. Nếu bạn gửi dữ liệu attribution đến các nền tảng bên thứ ba qua tích hợp Adapty (như AppsFlyer hoặc Adjust), hãy chọn thêm **Third-Party Advertising**. Chọn thêm các mục đích khác nếu ứng dụng của bạn sử dụng identifier theo những cách khác. <img src="/assets/shared/img/user-id-privacy.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp **Next**. --- # File: apple-family-sharing --- --- title: "Apple family sharing" description: "Bật Apple Family Sharing trong Adapty để hỗ trợ gói đăng ký được chia sẻ." --- Tính năng chia sẻ gia đình của Apple cho phép phân phối in-app purchase cho các thành viên trong gia đình, mang đến cho người dùng của các ứng dụng hướng đến nhóm — như dịch vụ phát video trực tuyến và ứng dụng trẻ em — một cách tiện lợi để chia sẻ gói đăng ký mà không cần dùng chung Apple ID. Bằng cách cho phép tối đa năm thành viên gia đình sử dụng một gói đăng ký, [Family Sharing](https://developer.apple.com/documentation/storekit/supporting-family-sharing-in-your-app) có thể giúp tăng mức độ gắn kết và giữ chân người dùng cho ứng dụng của bạn. Trong hướng dẫn này, chúng tôi sẽ hướng dẫn cách đăng ký gói đăng ký vào Family Sharing và giải thích cách Adapty quản lý các giao dịch mua được chia sẻ trong gia đình. Để bắt đầu bật Family Sharing cho một sản phẩm cụ thể, hãy truy cập [App Store Connect](https://appstoreconnect.apple.com/). Family Sharing mặc định bị tắt cho cả in-app purchase mới lẫn hiện có, vì vậy bạn cần bật riêng cho từng in-app purchase. Bạn có thể làm điều này dễ dàng bằng cách vào **trang ứng dụng**, điều hướng đến trang in-app purchase tương ứng và chọn tùy chọn **Turn On** trong phần Family Sharing. Hãy lưu ý rằng một khi bạn đã bật Family Sharing cho một sản phẩm, **không thể tắt lại nữa**, vì điều này sẽ ảnh hưởng đến trải nghiệm của những người dùng đã chia sẻ gói đăng ký với các thành viên trong gia đình. Ngoài ra, xin lưu ý rằng chỉ có non-consumable và gói đăng ký mới có thể được chia sẻ. <img src="/assets/shared/img/6db165a-CleanShot_2023-03-28_at_17.15.342x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trên hộp thoại hiển thị, chỉ cần nhấp vào nút **Confirm** để hoàn tất quá trình thiết lập. Sau đó, phần Family Sharing sẽ cập nhật hiển thị thông báo "This subscription can be shared by everyone in a family group." Điều này xác nhận rằng gói đăng ký hiện đã được bật cho Family Sharing và có thể được chia sẻ với tối đa năm thành viên gia đình. Adapty giúp bạn hỗ trợ Family Sharing dễ dàng mà không cần thêm bất kỳ thao tác nào. Bạn chỉ cần [cấu hình sản phẩm](app-store-products) từ App Store, và khi **bật** **Family Sharing** từ App Store Connect, tính năng này sẽ tự động khả dụng trong **Adapty** và được nhận dưới dạng sự kiện trên webhook. :::note Xin lưu ý rằng Family Sharing không được hỗ trợ trong môi trường sandbox. ::: Một điều cần lưu ý là khi người dùng mua gói đăng ký và chia sẻ với các thành viên gia đình, sẽ có **độ trễ lên đến một giờ** trước khi gói đăng ký đó khả dụng với họ. Apple thiết kế độ trễ này để người dùng có thời gian thay đổi ý định và hủy chia sẻ nếu muốn. Tuy nhiên, nếu gói đăng ký được gia hạn, sẽ không có độ trễ khi cung cấp cho các thành viên gia đình. Khi người dùng mua một sản phẩm in-app hỗ trợ Family Sharing, giao dịch sẽ xuất hiện trong biên lai của họ như thường, nhưng có thêm một trường mới là `in_app_ownership_type` với giá trị `PURCHASED.` Ngoài ra, một giao dịch mới sẽ được tạo cho tất cả thành viên gia đình, với `web_order_line_item_id` và `original_transaction_id` khác với giao dịch gốc, cùng trường `in_app_ownership_type` có giá trị `FAMILY_SHARED.` Để đảm bảo tính chính xác trong tính toán doanh thu, chỉ các giao dịch có `in_app_ownership_type` là `PURCHASED` mới được tính trong Adapty analytics. Các giao dịch `FAMILY_SHARED` bị loại trừ khỏi chỉ số doanh thu và chuyển đổi. **Các sự kiện được gửi cho giao dịch Family Sharing.** Giao dịch `FAMILY_SHARED` chỉ kích hoạt sự kiện **Access level updated**. Các sự kiện gói đăng ký theo từng sản phẩm không kích hoạt cho các thành viên gia đình. | Sự kiện | `FAMILY_SHARED` | `PURCHASED` | | --- | --- | --- | | **Access level updated** | Có | Có | | **Subscription started** | Không | Có | | **Trial started** | Không | Có | | **Subscription renewed** | Không | Có | | **Subscription expired** | Không | Có | | **Subscription refunded** | Không | Có | | **Billing issue detected** | Không | Có | Nếu hệ thống analytics của bạn dựa trên sự kiện **Subscription started**, các thành viên gia đình sẽ không xuất hiện ở đó. Hãy dùng **Access level updated** để phát hiện các thành viên gia đình đang hoạt động. Để xác định các thành viên gia đình khác trong Adapty, bạn có thể tìm thấy họ trong chi tiết sự kiện. Trước tiên, hãy tìm giao dịch mua gia đình gốc. Sau đó, kiểm tra chi tiết sự kiện của giao dịch đó, đặc biệt chú ý đến cùng sản phẩm, ngày mua và ngày hết hạn. Bằng cách phân tích chi tiết sự kiện, bạn có thể xác định các giao dịch thành viên gia đình khác liên quan đến giao dịch mua gốc. --- # File: app-store-small-business-program --- --- title: "App Store Small Business Program" description: "Tìm hiểu về Chương trình Doanh nghiệp Nhỏ của Apple, tác động của nó đến doanh thu và phân tích của Adapty" --- :::link Xem chương trình tương ứng trên Play Store tại [Google Reduced Service Fee](google-reduced-service-fee). ::: Các tổ chức có doanh thu từ App Store tối đa 1 triệu USD mỗi năm đủ điều kiện tham gia [Chương trình Doanh nghiệp Nhỏ](https://developer.apple.com/app-store/small-business-program/) của Apple. Nếu đăng ký, mức hoa hồng chuẩn 30% của cửa hàng sẽ được giảm xuống còn **15%**. Thành viên của chương trình cần **thay đổi cài đặt trong Adapty** để đảm bảo tính toán doanh thu chính xác và xử lý sự kiện tích hợp đúng cách. Bài viết này mô tả: * [Cách thiết lập Adapty](#configure-adapty) nếu ứng dụng của bạn đã đăng ký Chương trình Doanh nghiệp Nhỏ * [Cách đăng ký chương trình](#apply-for-the-program) nếu bạn muốn giảm hoa hồng cửa hàng ## Cấu hình Adapty \{#configure-adapty\} Adapty có thể áp dụng mức hoa hồng giảm vào [analytics](analytics) và [integration events](analytics-integration) của bạn. Để bật tính năng này, hãy chỉ định trạng thái Chương trình Doanh nghiệp Nhỏ cho từng ứng dụng. :::warning Hãy cấu hình trạng thái SBP trong Adapty **ngay khi nhận được phê duyệt**. Các thay đổi muộn không thể ghi đè lại các webhook event đã được gửi ([chi tiết](#retroactive-setting-changes)). ::: 1. Mở [**App Settings** → **General**](https://app.adapty.io/account) 2. Tìm mục **Small Business Program**. 3. Nhấn **Add period**. 4. Chọn ngày bắt đầu tham gia. 5. Chọn ngày kết thúc, hoặc bật tùy chọn **At the current moment** để gia hạn trạng thái này vô thời hạn. Nếu bạn [mất điều kiện tham gia](#losing-eligibility) sau này, bạn có thể sửa ngày kết thúc. 6. Nhấn **Apply**. Nếu tổ chức của bạn vẫn đủ điều kiện tham gia chương trình, tư cách thành viên sẽ được chuyển tiếp sang năm dương lịch tiếp theo. Tuy nhiên, trạng thái thành viên chỉ áp dụng **cho khoảng thời gian bạn chỉ định**. * Nhấn **Add period** để thêm giai đoạn thành viên mới. * Để gia hạn trạng thái này vô thời hạn, hãy bật tùy chọn **At the current moment**. Để xác minh cấu hình, mở [biểu đồ Revenue](revenue) và chọn **Proceeds after store commission**. Xác nhận rằng doanh thu hiển thị phản ánh mức hoa hồng đã giảm. ## Đăng ký chương trình \{#apply-for-the-program\} ### Yêu cầu điều kiện \{#eligibility-requirements\} Apple xác định điều kiện tham gia SBP dựa trên **doanh thu hàng năm** của bạn — doanh số bán hàng của năm dương lịch trước **sau** khi trừ hoa hồng cửa hàng và thuế. Để đủ điều kiện, tổng doanh thu hàng năm của tổ chức bạn và các <InlineTooltip tooltip="Tài khoản Nhà phát triển liên kết">Các tài khoản mà bạn hoặc tổ chức của bạn có quyền sở hữu đa số (>50%) hoặc có thẩm quyền ra quyết định.</InlineTooltip> không được vượt quá 1 triệu USD. Các tổ chức mới thành lập đương nhiên đủ điều kiện để đăng ký chương trình. ### Trước khi đăng ký \{#before-you-apply\} Hãy đảm bảo rằng bạn: - Là Account Holder trong Apple Developer Program - Đã chấp nhận hợp đồng Paid Applications mới nhất trong App Store Connect - Có thể liệt kê tất cả Tài khoản Nhà phát triển liên kết của mình ### Đăng ký \{#enrollment\} 1. Truy cập [trang đăng ký App Store Small Business Program](https://developer.apple.com/app-store/small-business-program/). 2. Nhấn **Enroll** và đăng nhập bằng tài khoản Apple Developer của bạn. 3. Xem lại thông tin đã được điền sẵn (tên, email, Team ID) và gửi. ### Xét duyệt \{#review\} Quá trình xét duyệt có thể mất hơn một tháng. Nếu bạn đủ điều kiện, bạn sẽ nhận được email phê duyệt từ Apple. Sau khi được chấp thuận, sẽ có một thời gian chờ. Mức hoa hồng giảm có hiệu lực vào ngày thứ 15 của [kỳ tài chính tiếp theo](https://adapty.io/apple-fiscal-calendar/) của Apple. Nó không áp dụng cho các giao dịch trước đó. ### Mất điều kiện tham gia \{#losing-eligibility\} Khi tổng doanh thu của bạn trong năm dương lịch hiện tại vượt quá 1 triệu USD, bạn sẽ mất tư cách thành viên và Apple sẽ bắt đầu áp dụng mức hoa hồng bán hàng chuẩn 30%. :::important Nếu doanh nghiệp của bạn rời khỏi Chương trình Doanh nghiệp Nhỏ, hãy **ngay lập tức thay đổi ngày kết thúc** trong cài đặt. Nếu không, Adapty sẽ tiếp tục tính hoa hồng theo mức giảm. ::: Bạn có thể tái đủ điều kiện tham gia chương trình **vào năm sau** khi doanh thu hàng năm của bạn giảm xuống dưới 1 triệu USD. Đọc [điều khoản chương trình chính thức](https://developer.apple.com/app-store/small-business-program/) để biết thêm chi tiết. ## Thay đổi cài đặt có hiệu lực hồi tố \{#retroactive-setting-changes\} Khi bạn thay đổi trạng thái hoa hồng giảm trong Adapty với ngày có hiệu lực hồi tố, mức hoa hồng mới sẽ xuất hiện trên dữ liệu của Adapty theo các lịch khác nhau: | Nơi mức hoa hồng xuất hiện | Điều gì xảy ra sau khi bạn thay đổi mức hoa hồng | | --- | --- | | Analytics dashboard (Revenue, Proceeds, MRR, ARR) | Adapty áp dụng mức hoa hồng mới trong vòng 24 giờ, khi quá trình tính toán lại hàng ngày chạy. | | Xuất dữ liệu S3, GCS và BigQuery | Adapty áp dụng mức hoa hồng mới vào lần xuất dữ liệu theo lịch tiếp theo. | | Các sự kiện webhook đã được gửi | Adapty không thể chỉnh sửa các sự kiện webhook sau khi đã gửi. Chúng vẫn giữ mức hoa hồng cũ. | Nếu kho dữ liệu của bạn lưu trữ doanh thu từ các sự kiện webhook, những bản ghi đó vẫn giữ mức hoa hồng cũ. Để đối chiếu, hãy truy xuất dữ liệu của khoảng thời gian bị ảnh hưởng từ analytics dashboard, hoặc tạo một bản xuất mới sang S3, GCS hoặc BigQuery. --- # File: android-products --- --- title: "Sản phẩm trên Play Store" description: "Quản lý sản phẩm Android với Adapty, tối ưu hóa in-app purchase và các chiến lược kiếm tiền." --- Trang này hướng dẫn cách tạo sản phẩm trên Play Store. Mặc dù thông tin này có thể không liên quan trực tiếp đến chức năng của Adapty, nhưng đây là tài nguyên hữu ích nếu bạn gặp khó khăn khi tạo sản phẩm trong Google Play Console. Sản phẩm là các mặt hàng hoặc dịch vụ kỹ thuật số mà bạn cung cấp trong ứng dụng trên Play Store, thường được bán cho người dùng. Chúng có thể bao gồm các in-app product như sản phẩm mua một lần, gói đăng ký, hoặc các hàng hóa kỹ thuật số khác mà người dùng có thể mua trong khi sử dụng ứng dụng. Trong [hệ thống thanh toán của Google](https://developer.android.com/google/play/billing/compatibility), gói đăng ký có thể bao gồm nhiều base plan, mỗi base plan cung cấp các mức giảm giá hoặc ưu đãi khác nhau. Cấu trúc này gồm ba thành phần chính: - **Subscriptions (Gói đăng ký):** Đây là các tập hợp quyền lợi mà người dùng có thể tận hưởng trong một khoảng thời gian nhất định (các mặt hàng được bán). Ví dụ: "Gói Gold" cung cấp tính năng cao cấp cho người đăng ký. - **Base plans:** Đây là các cấu hình cụ thể về chu kỳ thanh toán, loại gia hạn và giá cả (cách bán các mặt hàng). Ví dụ: "hàng năm với tự động gia hạn" hoặc "hàng tháng trả trước." - **Offers (Ưu đãi):** Đây là các mức giảm giá dành cho người dùng đủ điều kiện, điều chỉnh giá của base plan. Ví dụ: "dùng thử miễn phí 14 ngày cho người dùng mới." ## Cách tạo sản phẩm trên Play Store? \{#how-to-create-a-product-in-play-store\} Sản phẩm là các mặt hàng hoặc dịch vụ kỹ thuật số mà bạn cung cấp trong ứng dụng, thường được bán cho người dùng. Chúng có thể bao gồm các in-app product như sản phẩm mua một lần, gói đăng ký, hoặc các hàng hóa kỹ thuật số khác mà người dùng có thể mua trong khi sử dụng ứng dụng. Để thiết lập sản phẩm cho thiết bị Android: 1. Mở mục [**Monetize** -> **Subscriptions**](https://console.cloud.google.com/iam-admin/serviceaccounts) hoặc [**Monetize** -> **In-app products**](https://console.cloud.google.com/iam-admin/serviceaccounts) trong menu bên trái của Google Play Console. <img src="/assets/shared/img/6eff1d1-subscription_GP.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấn nút **Create subscription**. <img src="/assets/shared/img/af7fe02-create_subscription_GP.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Create subscription** vừa mở, nhập ID gói đăng ký vào trường **Product ID** và tên gói đăng ký vào trường **Name**. Product ID phải là duy nhất, phải bắt đầu bằng số hoặc chữ thường, và có thể chứa dấu gạch dưới (\_) và dấu chấm (.). ID này được dùng để truy cập sản phẩm trong quá trình phát triển và đồng bộ hóa với Adapty. Sau khi một Product ID được gán cho sản phẩm trong Google Play Console, nó không thể dùng lại cho bất kỳ ứng dụng nào khác, kể cả khi sản phẩm bị xóa. Khi đặt tên cho Product ID, nên theo một định dạng chuẩn hóa. Chúng tôi khuyến nghị dùng cách đặt tên ngắn gọn hơn theo dạng `<tên gói đăng ký>.<mức độ truy cập>`. Sau đó, bạn có thể kiểm soát thời hạn và chu kỳ thanh toán thông qua các base plan như hàng tuần, hàng tháng, v.v. Trường Name chỉ dùng để tham khảo nội bộ, sẽ hiển thị trên trang Google Play Store của bạn, vì vậy hãy thoải mái dùng bất kỳ tên mô tả nào phù hợp. Giới hạn 55 ký tự. 4. Nhấn nút **Create** để xác nhận việc tạo gói đăng ký. :::note Sản phẩm gói đăng ký Google Play trong Adapty Sản phẩm trong Adapty tương ứng với Base Plan của gói đăng ký Google Play vì đây là những sản phẩm khách hàng có thể mua. Adapty xử lý liền mạch việc migrate các gói đăng ký Google Play hiện có cùng với các base plan tương ứng trong sản phẩm mà không yêu cầu thêm thao tác nào từ phía bạn. Tuy nhiên, khi bạn thêm sản phẩm mới trong Adapty, bạn sẽ cần cung cấp cả base plan ID lẫn product ID. ::: ### Tạo base plan \{#create-a-base-plan\} Đối với sản phẩm gói đăng ký, bạn cần thêm một base plan. Base plan xác định chu kỳ thanh toán, giá cả và loại gia hạn để khách hàng mua gói đăng ký. Lưu ý rằng khách hàng không mua trực tiếp sản phẩm gói đăng ký. Thay vào đó, họ luôn mua một base plan trong gói đăng ký. Để tạo một base plan: 1. Mở mục [**Monetize** -> **Subscriptions**](https://console.cloud.google.com/iam-admin/serviceaccounts) trong menu bên trái của Google Play Console. Sau đó, tìm gói đăng ký mà bạn muốn thêm base plan. 2. Nhấn nút **View subscription** bên cạnh gói đăng ký. <img src="/assets/shared/img/4072a2a-subscriptions_GP.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Sau khi chi tiết gói đăng ký mở ra, nhấn nút **Add base plan** bên dưới tiêu đề **Base plans and offers**. Bạn có thể cần cuộn xuống để tìm nút này. <img src="/assets/shared/img/b493b60-add_base_plan.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trong cửa sổ **Add base plan** vừa mở, nhập mã định danh duy nhất cho base plan vào trường **Plan ID**. ID phải bắt đầu bằng số hoặc chữ thường, và có thể chứa số (0-9), chữ thường (a-z) và dấu gạch ngang (-), rồi hoàn thiện các trường bắt buộc. <img src="/assets/shared/img/8146763-CleanShot_2023-07-20_at_16.51.412x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Chỉ định giá theo từng khu vực. <img src="/assets/shared/img/8b26e1d-prices.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Nhấn nút **Save** để hoàn tất thiết lập. 7. Nhấn nút **Activate** để kích hoạt base plan. Lưu ý rằng trong Adapty, sản phẩm gói đăng ký chỉ có thể có một base plan duy nhất với thời hạn và loại gia hạn nhất quán. ### Sản phẩm dự phòng \{#fallback-products\} :::warning Hỗ trợ base plan không tương thích ngược Các phiên bản SDK Adapty cũ hơn không hỗ trợ các tính năng của Google Billing Library v5+, cụ thể là nhiều base plan trên mỗi sản phẩm gói đăng ký và các ưu đãi. Chỉ những base plan được đánh dấu là **[backwards compatible](https://support.google.com/googleplay/android-developer/answer/12124625?hl=en#backwards_compatible)** trong Google Play Console mới có thể truy cập với các phiên bản SDK này. Lưu ý rằng chỉ một base plan mỗi gói đăng ký có thể được đánh dấu là tương thích ngược. ::: <img src="/assets/shared/img/b5e70cb-CleanShot_2023-07-20_at_17.03.252x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Để tận dụng tối đa các cấu hình và tính năng gói đăng ký Google nâng cao trong Adapty, chúng tôi cung cấp khả năng thiết lập sản phẩm dự phòng tương thích ngược. Sản phẩm dự phòng này chỉ được sử dụng cho các ứng dụng dùng phiên bản SDK Adapty cũ hơn. Khi tạo sản phẩm Google Play, bạn có thể chỉ định liệu sản phẩm có nên được đánh dấu là tương thích ngược trong Play Console hay không. Adapty sử dụng thông tin này để xác định liệu sản phẩm có thể được mua bởi các phiên bản SDK cũ hơn (phiên bản 2.5 trở xuống) hay không. Giả sử bạn có gói đăng ký tên `subscription.premium` cung cấp hai base plan: hàng tuần (tương thích ngược) và hàng tháng. Nếu bạn thêm sản phẩm `subscription.premium:weekly` vào Adapty, bạn không cần chỉ định sản phẩm tương thích ngược. Tuy nhiên, với sản phẩm `subscription.premium:monthly`, bạn sẽ cần chỉ định một sản phẩm tương thích ngược. Nếu không làm vậy, người dùng có thể vô tình mua sản phẩm `subscription.premium:weekly` trong Google Billing Library thứ 4. Để giải quyết trường hợp này, bạn nên tạo một sản phẩm riêng có base plan cũng là hàng tháng và được đánh dấu là tương thích ngược. Điều này đảm bảo rằng người dùng chọn tùy chọn `subscription.premium:monthly` sẽ được tính phí đúng theo chu kỳ dự kiến. ## Thêm sản phẩm vào Adapty \{#add-products-to-adapty\} Sau khi hoàn tất việc thêm in-app purchase, gói đăng ký và ưu đãi trong App Store Connect, bước tiếp theo là [thêm các sản phẩm này vào Adapty](create-product). --- # File: google-play-data-safety --- --- title: "Google Play Data Safety" description: "Đảm bảo tuân thủ chính sách Google Play Data Safety trong Adapty." --- Phần Data Safety trên Google Play cung cấp cho nhà phát triển ứng dụng một cách đơn giản để thông báo cho người dùng về dữ liệu mà ứng dụng thu thập hoặc chia sẻ, đồng thời làm nổi bật các biện pháp bảo mật và quyền riêng tư quan trọng. Thông tin này giúp người dùng đưa ra quyết định sáng suốt hơn khi chọn ứng dụng để tải về và sử dụng. Dưới đây là hướng dẫn ngắn gọn về dữ liệu mà Adapty thu thập, giúp bạn cung cấp thông tin cần thiết cho Google Play. ## Thu thập dữ liệu và bảo mật \{#data-collection-and-security\} <img src="/assets/shared/img/3508c24-image4.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> **Ứng dụng của bạn có thu thập hoặc chia sẻ bất kỳ loại dữ liệu người dùng nào theo yêu cầu không?** Chọn 'Yes' vì Adapty thu thập lịch sử mua hàng của khách hàng. **Toàn bộ dữ liệu người dùng mà ứng dụng thu thập có được mã hóa trong quá trình truyền không?** Chọn 'Yes' vì Adapty mã hóa dữ liệu trong quá trình truyền. **Bạn có cung cấp cách để người dùng yêu cầu xóa dữ liệu của họ không?** Nếu chọn 'Yes', hãy đảm bảo khách hàng của bạn có cách liên hệ với đội ngũ hỗ trợ để yêu cầu xóa dữ liệu. Bạn có thể xóa khách hàng trực tiếp từ Adapty dashboard hoặc thông qua REST API. ## Loại dữ liệu \{#data-types\} Dưới đây là danh sách các loại dữ liệu mà Google yêu cầu báo cáo, cùng với thông tin về việc Adapty có thu thập từng loại dữ liệu cụ thể hay không. | Loại dữ liệu | Chi tiết | | :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Location | Adapty không thu thập | | Health and Fitness | Adapty không thu thập | | Photos and Videos | Adapty không thu thập | | Files and Docs | Adapty không thu thập | | Calendar | Adapty không thu thập | | Contacts | Adapty không thu thập | | User Content | Adapty không thu thập | | Browsing History | Adapty không thu thập | | Search History | Adapty không thu thập | | App Info and Performance | Adapty không thu thập | | Web Browsing | Adapty không thu thập | | Contact Info | Adapty không thu thập | | Financial Info | Adapty thu thập lịch sử mua hàng của người dùng | | Personal Info and Identifiers | Adapty thu thập User ID và một số thông tin nhận dạng khác bao gồm tên, địa chỉ email, số điện thoại, v.v., nếu bạn chủ động truyền chúng vào Adapty SDK. | | Device and other identifiers | Adapty thu thập dữ liệu về ID thiết bị. | ## Mục đích và cách xử lý dữ liệu \{#data-usage-and-handling\} ### User ID \{#user-ids\} **1. Dữ liệu này được thu thập, chia sẻ hay cả hai?** Dữ liệu này được Adapty thu thập. Nếu bạn đang sử dụng các tích hợp giữa Adapty và bên thứ ba không được coi là nhà cung cấp dịch vụ, bạn có thể cần khai báo thêm "Shared" ở đây. **2. Dữ liệu này có được xử lý tạm thời không?** Chọn 'No'. **3. Dữ liệu này có bắt buộc đối với ứng dụng của bạn không, hay người dùng có thể lựa chọn không cho thu thập?** Việc thu thập dữ liệu này là bắt buộc và không thể tắt. <img src="/assets/shared/img/2c60161-image5.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> **4. Tại sao dữ liệu người dùng này được thu thập? / Tại sao dữ liệu người dùng này được chia sẻ?** Chọn các ô 'App functionality' và 'Analytics'. <img src="/assets/shared/img/07a3c9e-image2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Financial Info \{#financial-info\} Nếu bạn đang sử dụng Adapty, bạn phải khai báo rằng ứng dụng của mình thu thập thông tin 'Purchase history' trong phần Data types trên Google Play Console. <img src="/assets/shared/img/1057870-image7.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### Device or other IDs \{#device-or-other-ids\} <img src="/assets/shared/img/d10f132-CleanShot_2023-03-01_at_17.55.312x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <img src="/assets/shared/img/ccb1a2a-image5.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Các bước tiếp theo \{#next-steps\} Sau khi hoàn thành các lựa chọn về data safety, Google sẽ hiển thị bản xem trước phần quyền riêng tư của ứng dụng bạn. Nếu bạn đã chọn "Financial Info" và "Device or other IDs" như đề cập ở trên, thông tin quyền riêng tư sẽ hiển thị tương tự như ví dụ sau: <img src="/assets/shared/img/e8d9b73-image3.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Nếu bạn đã sẵn sàng gửi ứng dụng để App Review, hãy tham khảo tài liệu [Release Checklist](release-checklist) của chúng tôi để biết thêm hướng dẫn chuẩn bị gửi ứng dụng. --- # File: google-reduced-service-fee --- --- title: "Phí Dịch Vụ Giảm của Google" description: "Tìm hiểu về Phí Dịch Vụ Giảm của Google, tác động của nó đến doanh thu và phân tích của Adapty" --- :::link Để xem chương trình tương ứng trên App Store, hãy xem [Chương trình Doanh nghiệp Nhỏ của App Store](app-store-small-business-program). ::: [Chương trình Phí Dịch Vụ Giảm](https://support.google.com/googleplay/android-developer/answer/112622?hl=en) của Google Play giảm hoa hồng trên 1 triệu USD đầu tiên trong thu nhập hàng năm từ 30% xuống còn **15%**. Thu nhập vượt quá 1 triệu USD trong cùng năm dương lịch sẽ bị tính theo mức chuẩn 30%. :::note Kể từ ngày 1 tháng 1 năm 2022, Google tính 15% trên tất cả các gói đăng ký tự gia hạn bất kể chương trình này. Phí Dịch Vụ Giảm chủ yếu có lợi cho in-app purchase không phải gói đăng ký và ứng dụng trả phí. ::: Các thành viên của chương trình phải **thay đổi cài đặt Adapty** để đảm bảo tính toán doanh thu chính xác và xử lý sự kiện tích hợp đúng cách. Bài viết này mô tả: * [Cách thiết lập Adapty](#configure-adapty) nếu ứng dụng của bạn đã đăng ký chương trình Phí Dịch Vụ Giảm * [Cách đăng ký chương trình](#enroll-in-the-program) nếu bạn muốn giảm hoa hồng cửa hàng ## Cấu hình Adapty \{#configure-adapty\} Adapty có thể áp dụng mức hoa hồng giảm cho [analytics](analytics) và [sự kiện tích hợp](analytics-integration) của bạn. Để bật tính năng này, hãy chỉ định trạng thái Phí Dịch Vụ Giảm của bạn theo từng ứng dụng. :::warning Cấu hình trạng thái Phí Dịch Vụ Giảm trong Adapty **ngay khi bạn đăng ký**. Những thay đổi muộn không thể ghi đè lại các sự kiện webhook đã được gửi ([chi tiết](#retroactive-setting-changes)). ::: 1. Mở [**App Settings** → **General**](https://app.adapty.io/account). 2. Tìm mục **Reduced Service Fee**. 3. Nhấp **Add period**. 4. Chọn ngày bắt đầu tham gia. 5. Chọn ngày kết thúc, hoặc bật tùy chọn **At the current moment** để kéo dài trạng thái này vô thời hạn. Nếu [thu nhập hàng năm của bạn vượt quá 1 triệu USD](#exceeding-the-threshold), bạn có thể sửa ngày kết thúc. 6. Nhấp **Apply**. Trạng thái thành viên chỉ áp dụng **cho khoảng thời gian bạn chỉ định**. Chương trình được đặt lại mỗi năm dương lịch. * Nhấp **Add period** để thêm khoảng thời gian thành viên mới. * Để kéo dài trạng thái này vô thời hạn, hãy bật tùy chọn **At the current moment**. Để xác minh cấu hình, mở [biểu đồ Doanh thu](revenue) và chọn **Proceeds after store commission**. Xác nhận rằng doanh thu hiển thị phản ánh mức hoa hồng giảm. ## Đăng ký chương trình \{#enroll-in-the-program\} ### Điều kiện tham gia \{#eligibility-requirements\} Google xác định điều kiện tham gia dựa trên **thu nhập hàng năm** của bạn trên tất cả các tài khoản trong <InlineTooltip tooltip="Nhóm Tài khoản">Một nhóm tài khoản nhà phát triển mà thu nhập được tính gộp lại. Bạn phải chỉ định Tài khoản Nhà phát triển của mình là Tài khoản Nhà phát triển Chính và liên kết các tài khoản liên kết với nhóm.</InlineTooltip>. Mức 15% áp dụng cho 1 triệu USD đầu tiên trong tổng thu nhập hàng năm. Bất kỳ thu nhập nào vượt quá ngưỡng đó sẽ bị tính 30%. ### Trước khi đăng ký \{#before-you-enroll\} Đảm bảo rằng bạn: - Đã thiết lập [hồ sơ thanh toán](https://support.google.com/googleplay/android-developer/answer/10632485) - Có thể liệt kê tất cả các Tài khoản Nhà phát triển Liên kết của bạn ### Đăng ký \{#enrollment\} 1. Truy cập [Google Play Console](https://play.google.com/console/). 2. Tạo một Nhóm Tài khoản và đặt Tài khoản Nhà phát triển của bạn làm Tài khoản Nhà phát triển Chính. 3. Liên kết bất kỳ Tài khoản Nhà phát triển Liên kết nào với nhóm. 4. Chấp nhận các điều khoản của chương trình Phí Dịch Vụ Giảm. Sau khi hoàn thành các bước này, Google sẽ tự động đăng ký bạn. Không có quy trình xem xét thủ công hay email phê duyệt. Để biết hướng dẫn chi tiết, xem [hướng dẫn đăng ký](https://support.google.com/googleplay/android-developer/answer/10632485) của Google. ### Vượt ngưỡng \{#exceeding-the-threshold\} Khi tổng thu nhập hàng năm của bạn vượt quá 1 triệu USD, Google sẽ tính 30% trên phần vượt ngưỡng cho phần còn lại của năm dương lịch đó. :::important Nếu thu nhập hàng năm của bạn vượt quá 1 triệu USD, hãy **thay đổi ngày kết thúc** trong cài đặt Adapty **ngay lập tức**. Nếu không, Adapty sẽ tiếp tục tính hoa hồng theo mức giảm. ::: Chương trình được đặt lại mỗi năm dương lịch. Nếu thu nhập của bạn vượt quá 1 triệu USD trong một năm, mức 15% sẽ tự động áp dụng lại cho 1 triệu USD đầu tiên của bạn vào năm sau. Không cần đăng ký lại. Đọc [điều khoản chương trình chính thức](https://support.google.com/googleplay/android-developer/answer/112622?hl=en) để biết thêm chi tiết. ## Thay đổi cài đặt hồi tố \{#retroactive-setting-changes\} Khi bạn thay đổi trạng thái hoa hồng giảm trong Adapty với ngày có hiệu lực hồi tố, mức hoa hồng mới sẽ xuất hiện trên dữ liệu của Adapty theo các lịch khác nhau: | Nơi mức hoa hồng xuất hiện | Điều gì xảy ra sau khi bạn thay đổi mức hoa hồng | | --- | --- | | Analytics dashboard (Revenue, Proceeds, MRR, ARR) | Adapty áp dụng mức hoa hồng mới trong vòng 24 giờ, khi quá trình tính toán lại hàng ngày chạy. | | Xuất dữ liệu S3, GCS và BigQuery | Adapty áp dụng mức hoa hồng mới vào lần xuất dữ liệu theo lịch tiếp theo. | | Các sự kiện webhook đã được gửi | Adapty không thể chỉnh sửa các sự kiện webhook sau khi đã gửi. Chúng vẫn giữ mức hoa hồng cũ. | Nếu kho dữ liệu của bạn lưu trữ doanh thu từ các sự kiện webhook, những bản ghi đó vẫn giữ mức hoa hồng cũ. Để đối chiếu, hãy truy xuất dữ liệu của khoảng thời gian bị ảnh hưởng từ analytics dashboard, hoặc tạo một bản xuất mới sang S3, GCS hoặc BigQuery. --- # File: google-play-quota-increase --- --- title: "Yêu cầu tăng hạn mức Google Play Developer API" description: "Yêu cầu tăng hạn mức cho Google Play Developer API nếu bạn vượt quá giới hạn mặc định trong quá trình import lịch sử hoặc với lượng subscriber lớn." --- Adapty sử dụng [Google Play Developer API](https://developers.google.com/android-publisher) để xác thực giao dịch mua và đồng bộ dữ liệu gói đăng ký. Hạn mức mặc định của API này là 3.000 truy vấn mỗi phút. Nếu ứng dụng của bạn vượt quá giới hạn này, Google sẽ gửi email thông báo cho bạn. Điều này thường xảy ra trong quá trình [import dữ liệu lịch sử](importing-historical-data-to-adapty) hoặc với các ứng dụng có nhiều subscriber đang hoạt động. Để tránh gián đoạn, hãy yêu cầu tăng hạn mức từ Google trước khi chạy một lần import lớn hoặc nếu bạn nhận được thông báo vượt hạn mức. ## Trước khi bắt đầu \{#before-you-start\} Hãy bật [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn) nếu bạn chưa làm. RTDN gửi các cập nhật gói đăng ký qua push notification thay vì polling, giúp giảm mức tiêu thụ API. Google có thể từ chối yêu cầu tăng hạn mức nếu RTDN chưa được bật. ## Thu thập thông tin cần thiết \{#gather-required-information\} Trước khi mở form yêu cầu, hãy thu thập các giá trị sau: - **Developer Account ID**: Để tìm, trong [Google Play Console](https://play.google.com/console/), vào **Settings > Developer account > Account details**. ID được hiển thị ở đầu trang. - **App package name**: Tên package của ứng dụng Android (ví dụ: `com.example.app`). Tìm trong Google Play Console trên trang **Dashboard** của ứng dụng. - **Google Cloud project number**: Để tìm, trong [Google Cloud Console](https://console.cloud.google.com/), chọn dự án của bạn. Số dự án nằm trên trang **Dashboard**. ## Yêu cầu tăng hạn mức \{#request-the-quota-increase\} 1. Mở [form yêu cầu tăng hạn mức Google Play Developer API](https://support.google.com/googleplay/android-developer/contact/apiqr). 2. Nhập Developer Account ID, app package name và Google Cloud project number của bạn. 3. Chọn API và quota bucket cần tăng. Nếu bạn nhận được email từ Google về việc vượt hạn mức, email đó sẽ chỉ rõ bucket nào. 4. Trong trường justification, hãy giải thích rằng bạn đang sử dụng dịch vụ quản lý gói đăng ký của bên thứ ba cần quyền truy cập API để xác thực giao dịch mua và đồng bộ dữ liệu gói đăng ký. 5. Đối với hạn mức yêu cầu, nhập lượng bạn cần. Nếu không chắc cần yêu cầu bao nhiêu, hãy kiểm tra mức sử dụng hiện tại trong [Google Cloud Console](https://console.cloud.google.com/) tại **IAM & Admin > Quotas** (lọc theo "Google Play Android Developer API"), sau đó gửi dữ liệu sử dụng và số lượng mục lịch sử bạn dự định import tới [support@adapty.io](mailto:support@adapty.io) để chúng tôi giúp bạn xác định lượng phù hợp. 6. Gửi form. Google thường xử lý các yêu cầu tăng hạn mức trong vài ngày làm việc. --- # File: prepare-your-app-for-store-review --- --- title: "Chuẩn bị ứng dụng cho quá trình xét duyệt của cửa hàng" description: "Lời khuyên để ứng dụng của bạn được chấp thuận trên App Store và Google Play Store" --- Bài viết này mô tả quy trình mà các cửa hàng thực hiện khi xem xét các bản gửi ứng dụng, đồng thời cung cấp các mẹo giúp ứng dụng được chấp thuận nhanh hơn. Nội dung được tham khảo từ các hướng dẫn gửi bài chính thức: * [Hướng dẫn gửi ứng dụng lên App Store](https://developer.apple.com/app-store/review/guidelines/) * [Hướng dẫn gửi ứng dụng lên Google Play Store](https://play.google/developer-content-policy/) :::important Cả hai cửa hàng đều có quy trình xét duyệt tương tự nhau. Khi một chính sách chỉ áp dụng cho một trong hai cửa hàng, bài viết sẽ đề cập đích danh cửa hàng đó. ::: Người dùng Adapty cần đặc biệt chú ý đến các vấn đề tuân thủ liên quan đến [paywall và in-app purchase](#iap-related-requirements). Đây là một trong những lý do phổ biến nhất khiến ứng dụng bị từ chối. ## Trước khi bắt đầu \{#before-you-begin\} Hãy xác nhận rằng ứng dụng của bạn đã sẵn sàng để nộp. Adapty cung cấp một [danh sách kiểm tra trước khi phát hành](release-checklist) để giúp bạn chuẩn bị ứng dụng trước khi đăng tải. Google Play Store yêu cầu các nhà phát hành lần đầu [kiểm thử ứng dụng](https://support.google.com/googleplay/android-developer/answer/14151465?hl=en) trước khi nộp. Quá trình kiểm thử phải có ít nhất 12 người tham gia và kéo dài tối thiểu 14 ngày liên tiếp. Yêu cầu này được áp dụng từ năm 2025 nhằm giảm số lượng ứng dụng lỗi đến tay đội ngũ kiểm duyệt của Google. ## Tổng quan về quy trình đánh giá \{#review-process-overview\} #### Bước 1: Kiểm tra tự động \{#step-1-the-automated-screening\} Cả App Store và Google Play Store đều theo quy trình đánh giá hai bước tương tự nhau. Ngay sau khi nộp, ứng dụng của bạn sẽ trải qua quá trình quét tự động, có thể mất đến vài giờ. Cả hai cửa hàng đều quét ứng dụng của bạn để tìm phần mềm độc hại, trong đó Google đặc biệt chú trọng vào quá trình này. Google tìm kiếm các dấu hiệu hành vi của hoạt động độc hại, như việc kết nối với các máy chủ đáng ngờ và truy cập dữ liệu người dùng không có lý do chính đáng. Nếu ứng dụng của bạn bị đánh giá là có khả năng gây hại, nó sẽ bị gắn cờ và chuyển đến một chuyên gia phân tích bảo mật để xem xét thủ công. [Tài liệu Google Play Protect](https://developers.google.com/android/play-protect/cloud-based-protections#machine-learning) có danh sách gần đúng các kiểm tra được thực hiện trong bước này. Các cửa hàng cũng xác nhận sự hiện diện của metadata cần thiết, không có các dependency độc hại hoặc quá lỗi thời, và tính toàn vẹn của bản build. #### Bước 2: Đánh giá thủ công \{#step-2-the-human-review\} Sau khi ứng dụng của bạn vượt qua bước sàng lọc tự động, nó sẽ được một người đánh giá xem xét trực tiếp. Bước này có thể mất đến vài ngày, tùy thuộc vào độ phức tạp của ứng dụng và hàng đợi đánh giá hiện tại. Các ứng dụng xử lý dữ liệu nhạy cảm sẽ mất nhiều thời gian hơn để được xem xét. ## Yêu cầu chung \{#general-requirements\} ### Tính ổn định \{#stability\} Các ứng dụng bị crash trong quá trình kiểm duyệt sẽ bị từ chối. Người kiểm duyệt có thể cố tình mô phỏng điều kiện mạng không ổn định, vì vậy ứng dụng phải xử lý tốt tình huống này. ### Tính đầy đủ \{#completeness\} Cả Apple lẫn Google đều yêu cầu *tính đầy đủ* ("chức năng tối thiểu") đối với nội dung trên cửa hàng. * Placeholder, màn hình "sắp ra mắt", và các tính năng bị lỗi đều có thể khiến ứng dụng iOS bị từ chối. * Google [linh hoạt hơn](https://support.google.com/googleplay/android-developer/answer/9898783?hl=en), đặc biệt nếu ứng dụng của bạn đang ở giai đoạn [Early Access](https://knowledge.workspace.google.com/admin/users/access/turn-early-access-apps-on-or-off-for-users). * Cả hai cửa hàng đều **từ chối** ứng dụng có ít hoặc không có chức năng. Điều này bao gồm các ứng dụng chỉ hiển thị một hình ảnh, file PDF, hoặc trang web. Nội dung bị thiếu cũng rơi vào cùng danh mục này. * Nếu ứng dụng không thực hiện được những gì bạn quảng cáo, nó sẽ bị từ chối. * Nếu bạn cấu hình một in-app purchase trong dashboard nhưng không đưa nó vào bản build, ứng dụng sẽ bị từ chối. ### Độ chính xác của metadata \{#metadata-accuracy\} Thông tin sai lệch, không chính xác, hoặc không nhất quán trong mô tả, ảnh chụp màn hình, và các metadata khác có thể dẫn đến việc bị từ chối. Không sử dụng trang danh sách trên store để quảng cáo các tính năng tương lai của ứng dụng. Nếu ứng dụng không được thiết kế cho công chúng nói chung, người kiểm duyệt sẽ yêu cầu tài liệu bổ sung giải thích quy trình hoạt động. Hãy bao gồm hướng dẫn rõ ràng trong metadata của ứng dụng. ### Xếp hạng nội dung \{#content-rating\} Nội dung bên trong ứng dụng phải phù hợp với xếp hạng đã khai báo. ### Khía cạnh pháp lý \{#legal-aspects\} * Chính sách quyền riêng tư của ứng dụng phải có thể truy cập từ bên trong ứng dụng. Bạn có thể sử dụng [nút liên kết](paywall-buttons#links) của Paywall Builder. * Yêu cầu người dùng đọc và chấp nhận bất kỳ thỏa thuận pháp lý nào **trước khi** chúng có hiệu lực. * Công khai sự hiện diện của quảng cáo trong ứng dụng. Không làm điều này có thể dẫn đến bị từ chối. * Nếu ứng dụng iOS của bạn bao gồm in-app purchase, bạn phải chấp nhận **Paid Apps Agreement** trong dashboard App Store Connect. ### Xác thực \{#authentication\} Nếu một phần nội dung của ứng dụng chỉ có thể truy cập sau khi xác thực, hãy cung cấp thông tin đăng nhập hợp lệ cho người kiểm duyệt. Không thể truy cập đầy đủ nội dung là lý do hợp lệ để bị từ chối. Nếu ứng dụng cho phép người dùng tạo tài khoản, nó cũng phải cho phép họ xóa tài khoản. Việc hướng người dùng đến hỗ trợ qua email hoặc website không đáp ứng yêu cầu này. ### Quyền truy cập và quyền riêng tư \{#access-and-privacy\} Metadata của ứng dụng phải nêu rõ lý do cho mỗi quyền được yêu cầu. Các quyền nhạy cảm nhất (ví dụ: quyền truy cập tin nhắn và nhật ký cuộc gọi) có thể yêu cầu video minh họa. Nguyên tắc tương tự áp dụng với dữ liệu người dùng nhạy cảm: nếu bạn yêu cầu dữ liệu đó, hãy giải thích tại sao. ## Yêu cầu liên quan đến IAP \{#iap-related-requirements\} Vi phạm chính sách kinh doanh là một trong những lý do phổ biến nhất khiến ứng dụng bị từ chối. Nếu phương thức kiếm tiền chính của ứng dụng là gói đăng ký và in-app purchase, ứng dụng sẽ bị kiểm tra kỹ lưỡng hơn. ### Yêu cầu đối với paywall \{#paywall-requirements\} Người kiểm duyệt ứng dụng kỳ vọng các paywall phải rõ ràng, dễ hiểu. Nếu bị nghi ngờ thao túng người dùng, ứng dụng sẽ bị từ chối. Nếu nhiều lần kiểm duyệt phát hiện bằng chứng về hành vi gian lận, tài khoản của bạn có thể bị vô hiệu hóa và ứng dụng bị [tạm ngưng](https://support.google.com/googleplay/android-developer/community-guide/287283557/app-suspended-for-repeated-rejections?hl=en). Google Play áp dụng [hệ thống cảnh báo](https://support.google.com/googleplay/android-developer/answer/9899234?hl=en) có thể dẫn đến việc xóa toàn bộ ứng dụng của bạn. Tuân thủ các nguyên tắc sau trong thiết kế paywall: - **Hãy minh bạch và rõ ràng ngay từ đầu.** Hiển thị đầy đủ giá, tần suất thanh toán, lợi ích và điều kiện hủy của từng sản phẩm trước khi yêu cầu người dùng mua hàng. Phân biệt rõ ràng giữa sản phẩm mua một lần và sản phẩm yêu cầu thanh toán định kỳ. Nếu sản phẩm có dùng thử miễn phí, hãy nêu rõ thời gian và điều kiện của nó. Không sử dụng ngôn ngữ cố ý gây nhầm lẫn để đánh lừa người dùng. - **Nhất quán.** Giá sản phẩm phải khớp nhau trên trang App Store, màn hình trong ứng dụng, màn hình quản lý gói đăng ký và nội dung marketing. Bất kỳ sự không khớp nào về giá, dù nhỏ, đều có thể bị từ chối. Paywall Builder của Adapty tự động đồng bộ giá giữa paywall và sản phẩm trong App Store Connect của bạn. Nếu paywall được code thủ công, bạn cần [lấy giá của từng sản phẩm](fetch-paywalls-and-products) từ mảng dữ liệu của nó. - **Hiển thị tất cả các gói đồng đều.** Không chọn sẵn tuỳ chọn đắt nhất, cũng không ẩn các tuỳ chọn rẻ hơn. - **Tránh "dark patterns".** Không tạo cảm giác cấp bách hoặc khan hiếm giả tạo. Không ép người dùng mua hàng bằng cách cố tình làm cho các tính năng miễn phí trở nên bất tiện hoặc khó tìm. ### Đảm bảo quyền truy cập \{#access-guarantee\} Ứng dụng phải đảm bảo quyền truy cập của người dùng vào các sản phẩm họ đã mua. * **Truy cập ngay lập tức** Một giao dịch mua thành công phải mở khóa quyền truy cập vào sản phẩm ngay lập tức, không có độ trễ hiển thị. Các trạng thái trung gian của quá trình xác thực thanh toán không được gây ra lỗi hay làm gián đoạn trải nghiệm người dùng. Một giao dịch mua thành công phải ẩn paywall ngay lập tức. Nếu bạn tiếp tục hiển thị paywall sau khi mua hàng, bạn đang ngăn người dùng truy cập vào nội dung họ đã trả tiền. * **Khôi phục quyền truy cập** Người dùng cần có khả năng khôi phục quyền truy cập vào sản phẩm từ thiết bị mới. Hãy đặt nút khôi phục ở vị trí dễ thấy. Nếu bạn xây dựng paywall bằng [Flow Builder](adapty-flow-builder), nút khôi phục sẽ tự động kích hoạt quá trình khôi phục. Nếu bạn [tự triển khai paywall thủ công](ios-implement-paywalls-manually), hãy thêm code để gọi phương thức [restorePurchases](restore-purchase). Adapty sẽ khôi phục mức độ truy cập của người dùng, **trừ khi** bạn sử dụng SDK ở chế độ [observer mode](observer-vs-full-mode). Ứng dụng cần có khả năng nhận diện các in-app purchase được thực hiện từ trang sản phẩm trên cửa hàng, hoặc từ các vị trí khác trong app store. ### Phương thức thanh toán phù hợp \{#appropriate-payment-methods\} Cả hai cửa hàng đều cấm bán hàng hóa vật lý thông qua in-app purchase và yêu cầu sử dụng hệ thống thanh toán của cửa hàng cho hầu hết các sản phẩm kỹ thuật số. Yêu cầu này không áp dụng ở một số khu vực địa lý, bao gồm Mỹ và EU. Tùy theo từng quốc gia, bạn có thể [bỏ qua hoàn toàn hệ thống thanh toán của cửa hàng](https://support.google.com/googleplay/android-developer/answer/16497028), hoặc [cho người dùng lựa chọn](https://support.google.com/googleplay/android-developer/answer/13821247) giữa thanh toán qua app store và phương thức thanh toán thay thế. Một số danh mục ứng dụng (chẳng hạn như ứng dụng đọc sách điện tử hoặc ứng dụng hẹn hò) có thể đủ điều kiện sử dụng phương thức thanh toán thay thế ngay cả ngoài các khu vực này. Hãy kiểm tra hướng dẫn chính thức của các cửa hàng để biết thêm chi tiết. :::tip Không giống như [Google](https://support.google.com/googleplay/android-developer/answer/13821247), Apple không cung cấp danh sách chính thức các quốc gia cho phép sử dụng phương thức thanh toán thay thế. Khi ngày càng có thêm nhiều khu vực pháp lý ban hành các luật tương tự, phạm vi áp dụng sẽ được mở rộng. Hãy đọc tài liệu liên quan đến quốc gia cụ thể của bạn trước khi tiến hành. ::: Lưu ý rằng cả hai cửa hàng đều có quy định về tích hợp nhà cung cấp thanh toán, và vẫn tiếp tục thu phí hoa hồng cho các giao dịch sử dụng những dịch vụ này. ## Xử lý khi bị từ chối \{#handling-rejection\} Nếu ứng dụng của bạn bị từ chối, người đánh giá sẽ nêu rõ hướng dẫn nào bị vi phạm. Hãy đọc kỹ hướng dẫn đó và khắc phục: * [Hướng dẫn gửi ứng dụng lên App Store](https://developer.apple.com/app-store/review/guidelines/) * [Hướng dẫn gửi ứng dụng lên Google Play Store](https://play.google/developer-content-policy/) Nếu bạn cho rằng việc từ chối là không công bằng, bạn có quyền khiếu nại. Hãy cung cấp bằng chứng tuân thủ và liên hệ với cửa hàng. * Không cập nhật ứng dụng trong khi đang được xem xét. * Mỗi lần bạn gửi ứng dụng để xem xét, bạn có thể gặp một người đánh giá khác. Điều này có thể có lợi hoặc bất lợi cho bạn. * Không sửa từng vấn đề một. Hãy gửi ứng dụng để xem xét lại khi tất cả các sửa đổi đã được hoàn tất. * Nếu Google Play từ chối ứng dụng của bạn vì vi phạm chính sách, hãy cập nhật dữ liệu liên quan trên tất cả các track, kể cả những track đang tạm dừng hoặc không hoạt động. * Các lần xem xét tiếp theo thường mất ít thời gian hơn lần đầu. * Xem xét nhanh có thể áp dụng cho các lỗi nghiêm trọng và các deadline khẩn cấp — hãy dùng tiết kiệm. ## Sau khi xét duyệt: giám sát liên tục \{#after-the-review-continuous-monitoring\} Cả hai cửa hàng đều tiếp tục theo dõi ứng dụng của bạn ngay cả sau khi ứng dụng vượt qua quá trình xét duyệt. Nếu chức năng của ứng dụng thay đổi sau khi được phê duyệt (ví dụ: do code được tải động), ứng dụng sẽ bị gắn cờ và gỡ khỏi danh sách. Một lượng lớn phản hồi tiêu cực từ người dùng cũng là lý do để ứng dụng bị xem xét thêm. Trong giai đoạn 2024–2025, Google đã [xóa 47% ứng dụng trên Play Store](https://techcrunch.com/2025/04/29/google-play-sees-47-decline-in-apps-since-start-of-last-year/) nhằm nâng cao chất lượng trung bình của nền tảng. Việc bỏ bê ứng dụng cũng tiềm ẩn rủi ro. Cả [Google](https://www.cnet.com/tech/mobile/google-play-store-will-hide-apps-that-havent-been-updated-in-years/) lẫn [Apple](https://developer.apple.com/support/app-store-improvements/#:~:text=Developers%20of%20apps%20that%20have,launch%20will%20be%20removed%20immediately.) đều gỡ các ứng dụng không được cập nhật hoặc không có lượt tải. ## Xem thêm \{#see-also\} * [Kiểm thử sandbox](test-purchases-in-sandbox) * [Danh sách kiểm tra trước khi phát hành](release-checklist) --- # File: firebase-apps --- --- title: "Firebase apps" description: "Tích hợp Firebase với Adapty để nâng cao phân tích người dùng và theo dõi gói đăng ký cho ứng dụng di động của bạn." --- Trang này hướng dẫn cách tích hợp Adapty vào ứng dụng của bạn nếu ứng dụng đang chạy trên Firebase. :::note Bắt đầu Đây không phải là tất cả các bước cần thiết để Adapty hoạt động, chỉ là một số mẹo hữu ích cho việc tích hợp với Firebase. Nếu bạn muốn tích hợp Adapty vào ứng dụng của mình, hãy đọc [Hướng dẫn Quickstart](quickstart) trước. ::: ## Xác định người dùng \{#user-identification\} Nếu bạn đang dùng Firebase auth, đoạn code này có thể giúp bạn đồng bộ người dùng giữa Firebase và Adapty. Lưu ý rằng đây chỉ là ví dụ, và bạn cần cân nhắc đặc thù xác thực của ứng dụng mình. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS with Firebase" default> ```swift showLineNumbers @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Adapty before Firebase Adapty.activate("YOUR_API_KEY") Adapty.delegate = self // Configure Firebase FirebaseApp.configure() // Add state change listener for Firebase Authentication Auth.auth().addStateDidChangeListener { (auth, user) in if let uid = user?.uid { // identify Adapty SDK with new Firebase user Adapty.identify(uid) { error in if let e = error { print("Sign in error: \(e.localizedDescription)") } else { print("User \(uid) signed in") } } } } return true } } extension AppDelegate: AdaptyDelegate { // MARK: - Adapty delegate func didReceiveUpdatedPurchaserInfo(_ purchaserInfo: PurchaserInfoModel) { // You can optionally post to the notification center whenever // purchaser info changes. // You can subscribe to this notification throughout your app // to refresh tableViews or change the UI based on the user's // subscription status NotificationCenter.default.post(name: NSNotification.Name(rawValue: "com.Adapty.PurchaserInfoUpdatedNotification"), object: purchaserInfo) } } ``` </TabItem> <TabItem value="kotlin" label="Android with Firebase" default> ```kotlin showLineNumbers class App : Application() { override fun onCreate() { super.onCreate() // Configure Adapty Adapty.activate(this, "YOUR_API_KEY") Adapty.setOnPurchaserInfoUpdatedListener(object : OnPurchaserInfoUpdatedListener { override fun onPurchaserInfoReceived(purchaserInfo: PurchaserInfoModel) { // handle any changes to subscription state } }) // Add state change listener for Firebase Authentication FirebaseAuth.getInstance().addAuthStateListener { auth -> val currentUserId = auth.currentUser?.uid if (currentUserId != null) { // identify Adapty SDK with new Firebase user Adapty.identify(currentUserId) { error -> if (error == null) { //success } } } else { Adapty.logout { } } } } } ``` </TabItem> </Tabs> --- # File: refund-saver --- --- title: "Refund Saver" description: "Sử dụng Adapty Refund Saver để giảm thiểu hoàn tiền và tối đa hóa doanh thu." --- Mỗi khi người dùng yêu cầu hoàn tiền, Apple sẽ tiến hành điều tra. Để quyết định **liệu yêu cầu hoàn tiền có hợp lý hay không**, Apple sẽ hỏi nhà phát triển về hoạt động của người dùng đó. Nếu không có bằng chứng này, ngay cả một gói đăng ký được sử dụng nhiều cũng có khả năng bị hoàn tiền. **Refund Saver** trả lời các yêu cầu tiêu thụ của Apple [một cách tự động](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information-v1), bảo vệ doanh thu của bạn và **tăng khả năng từ chối** các yêu cầu không hợp lý. Tính năng này hoạt động với mọi loại in-app purchase của Apple — gói đăng ký tự gia hạn, gói đăng ký một lần, consumable và non-consumable (bao gồm cả sản phẩm trọn đời). ## Refund Saver hoạt động như thế nào \{#how-refund-saver-works\} 1. Khi người dùng khởi tạo yêu cầu hoàn tiền, App Store sẽ gửi thông báo yêu cầu thông tin giao dịch và mức độ sử dụng. Nếu bạn **bỏ qua** hoặc **trả lời chậm**, Apple có khả năng sẽ **chấp thuận yêu cầu hoàn tiền**. 2. Adapty Refund Saver tự động xử lý các thông báo này, cung cấp cho Apple dữ liệu cần thiết. Việc tự động hóa này giúp giảm khả năng hoàn tiền không cần thiết, đồng thời tiết kiệm thời gian và bảo vệ doanh thu của bạn. 3. Adapty ghi lại từng kết quả — đã hoàn tiền hoặc bị từ chối. Dữ liệu đó được hiển thị trong phần phân tích Refund Saver trên Dashboard. :::info Với Refund Saver, bạn có thể tiết kiệm đến 40% doanh thu từ các yêu cầu hoàn tiền. ::: ## Yêu cầu để sử dụng Refund Saver \{#requirements-to-use-refund-saver\} Để sử dụng tính năng này, hãy đảm bảo bạn đã đáp ứng các điều kiện tiên quyết sau: 1. **Cập nhật Chính sách Bảo mật trong App Store Connect:** Chính sách Bảo mật của ứng dụng phải công bố việc thu thập và sử dụng dữ liệu tiêu thụ. Điều này đảm bảo người dùng hiểu rõ các hoạt động bảo mật của ứng dụng trước khi tải về. Tham khảo [App Privacy Details của Apple](https://developer.apple.com/app-store/app-privacy-details/) để được hướng dẫn. 2. **Lấy sự đồng ý của người dùng để chia sẻ dữ liệu trong ứng dụng**: Apple yêu cầu bạn phải có sự đồng ý hợp lệ từ người dùng trước khi chia sẻ dữ liệu cá nhân của họ với Apple. Với tư cách là nhà phát triển, bạn có trách nhiệm lấy sự đồng ý này vì bạn sẽ chia sẻ dữ liệu người dùng với Apple. Xem [hướng dẫn](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information) của Apple để biết thêm chi tiết. 3. **Bật Server Notifications V2:** Đảm bảo Server Notifications V2 đã được kích hoạt trong tài khoản Apple Developer và được cấu hình đúng trong Adapty, vì V1 không được hỗ trợ. Nếu chưa được kích hoạt, hãy làm theo các bước trong hướng dẫn [Bật thông báo máy chủ App Store](enable-app-store-server-notifications). ## Bật Refund Saver \{#turn-on-refund-saver\} 1. Mở phần [Refund Saver](https://app.adapty.io/refund-saver) trong Adapty Dashboard. 2. Nhấn **Turn on Refund Saver** để kích hoạt tính năng. ## Cài đặt hành vi hoàn tiền mặc định \{#set-a-default-refund-behavior\} Apple cho phép nhà phát triển chỉ định kết quả ưu tiên cho từng yêu cầu hoàn tiền khi phản hồi. Mục đích của cài đặt này là tìm sự cân bằng phù hợp giữa từ chối và chấp nhận yêu cầu hoàn tiền để chỉ những yêu cầu hợp lý mới được giải quyết. Lưu ý rằng cài đặt này chỉ dùng để tác động đến kết quả, nhưng quyết định cuối cùng vẫn thuộc về Apple. Adapty hỗ trợ thiết lập tùy chọn này, nhưng chúng tôi sẽ dùng cùng một giá trị cho mọi yêu cầu hoàn tiền. 1. Để thay đổi tùy chọn của bạn, nhấn **Edit refund preference**. 2. Trong cửa sổ **Edit refund preference**, chọn tùy chọn **Default refund request preference** của bạn: | Tùy chọn | Mô tả | | -------------------------------------------- | ------------------------------------------------------------ | | Always decline | (mặc định) Đây là tùy chọn mặc định và thường cho kết quả tốt nhất trong việc giảm thiểu hoàn tiền. | | Decline first refund request, grant all next | Với mỗi giao dịch mà Refund Saver gặp, ban đầu sẽ yêu cầu Apple từ chối hoàn tiền. Tuy nhiên, nếu cùng giao dịch đó xuất hiện lại, Refund Saver sẽ luôn đề xuất chấp thuận hoàn tiền. Cách tiếp cận này giúp giảm thiểu sự thất vọng của người dùng do bị từ chối hoàn tiền không công bằng — người dùng có thể đơn giản yêu cầu lại và có khả năng sẽ nhận được. | | Always refund | Đề xuất Apple chấp thuận mọi yêu cầu hoàn tiền. | | No preference | Không đưa ra bất kỳ đề xuất nào cho Apple. Trong trường hợp này, Apple sẽ xác định kết quả hoàn tiền dựa trên các chính sách nội bộ và lịch sử người dùng, mà không có bất kỳ ảnh hưởng nào từ cài đặt của bạn. Tùy chọn này cung cấp cách tiếp cận trung lập nhất. | ## Cài đặt hành vi hoàn tiền cho một người dùng cụ thể trên dashboard \{#set-refund-behavior-for-a-specific-user-in-the-dashboard\} Dù bạn đã cấu hình hành vi Refund Saver mặc định cho toàn bộ ứng dụng, bạn vẫn có thể muốn đặt tùy chọn riêng cho từng người dùng cụ thể. Trong Adapty Dashboard, bạn có thể thực hiện điều này từ hồ sơ người dùng. Sử dụng phần **Refund Saver Preferences** nằm ở phía dưới bên trái. :::note Tùy chọn theo từng người dùng sẽ ghi đè lên mặc định ở cấp ứng dụng — bao gồm cả hành vi "Decline first refund request, grant all next". ::: ## Cài đặt hành vi hoàn tiền cho một người dùng cụ thể trong SDK \{#set-refund-behavior-for-a-specific-user-in-the-sdk\} Bạn có thể đặt tùy chọn hoàn tiền trong code ứng dụng cho từng lượt cài đặt tùy theo hành động của người dùng. Sử dụng đoạn code bên dưới để đặt tùy chọn: <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (3.4.1+)" default> ```swift showLineNumbers code do { try await Adapty.updateRefundPreference(<PREFERENCE_VALUE>) // possible values: .noPreference, .grant, .decline } catch { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter (3.4.0+)" default> ```javascript showLineNumbers code try { // possible values: AdaptyRefundPreference.noPreference, AdaptyRefundPreference.grant, AdaptyRefundPreference.decline await Adapty().updateRefundPreference(<PREFERENCE_VALUE>); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="rn" label="React Native (3.4.0+)" default> ```typescript showLineNumbers try { await adapty.updateRefundPreference(<PREFERENCE_VALUE>); // possible values: RefundPreference.NoPreference, RefundPreference.Grant, RefundPreference.Decline } catch (error) { // handle the `AdaptyError` } ``` </TabItem> <TabItem value="unity" label="Unity (3.3.0+)" default> ```csharp showLineNumbers Adapty.UpdateAppStoreRefundPreference(<PREFERENCE_VALUE>, (error) => { if (error != null) { // handle the error return; } }); ``` </TabItem> </Tabs> :::note Bạn cũng có thể sử dụng Server-side API để [đặt tùy chọn hoàn tiền riêng lẻ](api-adapty/operations/setRefundSaverSettings): - Sử dụng SDK khi việc đặt tùy chọn gắn liền trực tiếp với tương tác phía client, chẳng hạn khi người dùng nhấn nút để cấu hình tùy chọn của họ. - Sử dụng API khi bạn cần xử lý phía server hoặc khi điều đó phù hợp hơn với kiến trúc ứng dụng của bạn. ::: ## Lấy sự đồng ý của người dùng \{#obtain-user-consent\} Cách bạn thu thập sự đồng ý của người dùng để chia sẻ dữ liệu là tùy bạn, nhưng Apple yêu cầu sự đồng ý hợp lệ từ người dùng trước khi chia sẻ bất kỳ dữ liệu cá nhân nào với họ. Apple khuyến nghị sử dụng **phương pháp opt-in**, bao gồm các thông báo trong ứng dụng giải thích cách dữ liệu sẽ được sử dụng và yêu cầu hành động rõ ràng từ người dùng để đồng ý. Nếu người dùng bỏ qua hoặc từ chối thông báo, họ không được coi là đã đồng ý. Để biết thêm chi tiết, hãy tham khảo [hướng dẫn](https://developer.apple.com/documentation/appstoreserverapi/send-consumption-information) của Apple. Nếu việc lấy sự đồng ý rõ ràng không thực tế với ứng dụng của bạn, bạn có thể cân nhắc **phương pháp opt-out**. Cách này bao gồm việc đưa điều khoản chia sẻ dữ liệu vào Điều khoản Dịch vụ, giải thích rằng người dùng đồng ý chia sẻ dữ liệu khi chấp nhận các điều khoản. Hãy đảm bảo nêu rõ cách người dùng có thể thu hồi sự đồng ý của họ. Dưới đây là ví dụ về điều khoản cho phương pháp opt-out, bao gồm các loại dữ liệu bạn có thể chia sẻ. Đây chỉ là mẫu để hướng dẫn bạn soạn thảo nội dung của riêng mình. Bạn có trách nhiệm đảm bảo phiên bản cuối cùng tuân thủ tất cả các luật áp dụng và yêu cầu của Apple. *"Nếu chúng tôi nhận được yêu cầu hoàn tiền cho một in-app purchase, chúng tôi có thể cung cấp cho Apple thông tin về hoạt động in-app purchase của người dùng. Điều này có thể bao gồm các chi tiết như thời gian kể từ khi cài đặt ứng dụng, tổng thời gian sử dụng ứng dụng, mã định danh tài khoản ẩn danh, liệu in-app purchase có được sử dụng hết hay không, liệu có bao gồm thời gian dùng thử hay không, tổng số tiền đã chi và tổng số tiền đã được hoàn."* Tùy thuộc vào phương pháp bạn chọn, hãy đặt tùy chọn **Default consent policy** trong menu **Edit refund preferences**: <p> </p> | Tùy chọn | Mô tả | | ------- | ------------------------------------------------------------ | | Opt-out | (mặc định) Nếu Adapty không biết trạng thái đồng ý của người dùng, hệ thống giả định rằng sự đồng ý **đã được cấp** và Refund Saver **sẽ chia sẻ** dữ liệu liên quan đến hoàn tiền với Apple. | | Opt-in | Nếu Adapty không biết trạng thái đồng ý của người dùng, hệ thống giả định rằng sự đồng ý **chưa được cấp** và Refund Saver **sẽ không chia sẻ** bất kỳ dữ liệu nào với Apple. Đây là cách tiếp cận được Apple khuyến nghị. | ## Cập nhật sự đồng ý của người dùng trong SDK \{#update-user-consent-in-the-sdk\} Để thông báo cho Adapty biết một người dùng cụ thể có đồng ý hay không, hãy sử dụng phương thức `updateCollectingRefundDataConsent`. Giá trị này được lưu trữ phía server theo từng hồ sơ người dùng, vì vậy bạn chỉ cần gọi khi sự đồng ý thay đổi. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (3.4.1+)" default> ```swift showLineNumbers do { try await Adapty.updateCollectingRefundDataConsent(<CONSENT_VALUE>) // true = consent is explicitly provided, false = consent is explicitly revoked } catch { // handle the error } ``` </TabItem> <TabItem value="flutter" label="Flutter (3.4.0+)" default> ```dart showLineNumbers try { // true = user gave consent, false = user revoked consent await Adapty().updateCollectingRefundDataConsent(<CONSENT_VALUE>); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` </TabItem> <TabItem value="rn" label="React Native (3.4.0+)" default> ```typescript showLineNumbers try { await adapty.updateCollectingRefundDataConsent(<CONSENT_VALUE>); // true = consent is explicitly provided, false = consent is explicitly revoked } catch (error) { // handle the `AdaptyError` } ``` </TabItem> <TabItem value="unity" label="Unity (3.3.0+)" default> ```csharp showLineNumbers Adapty.UpdateAppStoreCollectingRefundDataConsent(<CONSENT_VALUE>, (error) => { if (error != null) { // handle the error return; } }); ``` </TabItem> </Tabs> :::note Bạn cũng có thể sử dụng Server-side API để [đặt tùy chọn chia sẻ dữ liệu riêng lẻ](api-adapty/operations/setRefundSaverSettings): - Sử dụng SDK khi việc đặt tùy chọn gắn liền trực tiếp với tương tác phía client, chẳng hạn khi người dùng nhấn nút để cấu hình tùy chọn của họ. - Sử dụng API khi bạn cần xử lý phía server hoặc khi điều đó phù hợp hơn với kiến trúc ứng dụng của bạn. ::: ## Kiểm tra sự đồng ý của người dùng \{#check-user-consent\} Bạn có thể kiểm tra trạng thái đồng ý hiện tại của người dùng bất cứ lúc nào. Trong Adapty Dashboard, chỉ cần mở hồ sơ người dùng và tìm cài đặt **Allow data sharing** trong phần **Refund Saver Preferences** ở phía dưới bên trái. :::note Bạn cũng có thể sử dụng Server-side API để [lấy tùy chọn hoàn tiền và chia sẻ dữ liệu riêng lẻ](api-adapty/operations/getRefundSaverSettings). ::: ## Giới hạn \{#limitations\} - **Chỉ dành cho App Store của Apple:** Refund Saver chỉ khả dụng cho các yêu cầu hoàn tiền được thực hiện qua App Store của Apple. Google Play không cung cấp phân tích dữ liệu tiêu thụ cho hoàn tiền. Quyết định hoàn tiền trên Google Play chỉ dựa trên các chính sách của Google và thông tin do người dùng cung cấp. - **Yêu cầu Server Notifications V2:** Refund Saver không tương thích với App Store Server Notifications V1. Nếu bạn đang sử dụng V1 trong Adapty, bạn cần chuyển sang V2, xem hướng dẫn [Gửi thông báo máy chủ App Store đến Adapty](enable-app-store-server-notifications) để biết chi tiết. Chuyển sang V2 cũng sẽ cải thiện phân tích của bạn trong Adapty bằng cách cung cấp dữ liệu chính xác và toàn diện hơn. --- # File: meta-create-campaign --- --- title: "Quảng cáo ứng dụng của bạn trên Meta Ads" --- Trong hướng dẫn từng bước này, bạn sẽ học cách tạo và thiết lập quảng cáo cho ứng dụng trên Meta, giúp bạn dễ dàng tối ưu hóa và theo dõi hiệu suất của chúng. ## Cấu trúc quảng cáo trên Meta \{#how-ads-in-meta-are-structured\} Khi quảng cáo trên Meta Ads, bạn cần cấu hình ba cấp độ phân cấp: - **Campaign**: Chiến dịch xác định mục tiêu quảng cáo của bạn. - **Ad set**: Nhóm quảng cáo chỉ định đối tượng mục tiêu và các vị trí đặt quảng cáo — xác định quảng cáo sẽ hiển thị ở đâu và cho ai. Mỗi chiến dịch có thể chứa nhiều nhóm quảng cáo. - **Ads**: Quảng cáo là các mẫu sáng tạo thực tế mà người dùng nhìn thấy và tương tác. Mỗi nhóm quảng cáo có thể chứa nhiều quảng cáo; tuy nhiên, để đạt hiệu suất tối ưu, nên giới hạn không quá năm quảng cáo mỗi nhóm. ## Bước 1. Tạo tài khoản Meta Ads Manager \{#step-1-create-meta-ads-manager-account\} Để bắt đầu sử dụng Meta Ads, bạn cần có một trang Facebook doanh nghiệp vì bạn không thể chạy quảng cáo từ trang cá nhân. Vì vậy, bạn cần liên kết trang doanh nghiệp của mình với danh mục doanh nghiệp trên Meta Ads: 1. Truy cập [business.facebook.com](https://business.facebook.com/). Nếu bạn chưa có trang doanh nghiệp trong danh mục doanh nghiệp, bạn cần thêm vào. Nhấp vào **Go to settings**. 2. Vào **Account > Pages** từ thanh bên trái. Nhấp **Add** và chọn **Add an existing Facebook page** hoặc **Create a new Facebook page**. Xem [hướng dẫn tạo trang doanh nghiệp](https://www.facebook.com/business/help/473994396650734) nếu bạn chưa có trang nào. 3. Tùy chọn, kết nối tài khoản Instagram của bạn trên trang **Account > Instagram accounts** trong phần cài đặt. Sau khi đã kết nối trang doanh nghiệp, bạn đã sẵn sàng để tiếp tục. ## Bước 2. Thêm Meta pixel \{#step-2-add-meta-pixel\} Bạn cần có Meta pixel để kết nối dữ liệu chiến dịch với doanh thu và đạt kết quả tốt hơn. Trước khi kết nối dữ liệu và tạo pixel, bạn cần chuẩn bị: - Trang doanh nghiệp – thêm vào business portfolio của bạn tại [**Settings > Accounts > Pages**](https://business.facebook.com/latest/settings/pages) - Tài khoản business manager – bạn phải có toàn quyền kiểm soát business portfolio - Email doanh nghiệp – cài đặt tại [**Settings > Business info**](https://business.facebook.com/latest/settings/business_info) - Tài khoản quảng cáo – thêm vào business portfolio của bạn tại [**Settings > Accounts > Ad accounts**](https://business.facebook.com/latest/settings/ad_accounts) Khi bạn đã sẵn sàng, hãy tạo một pixel: 1. Vào [**Events Manager**](https://www.facebook.com/events_manager2). Nhấp **Connect data**. 2. Chọn **Web** làm loại nguồn dữ liệu. 3. Đặt tên cho dataset của bạn và nhấp **Create**. 4. Với [Adapty Attribution](adapty-user-acquisition), bạn không cần hoàn tất toàn bộ quá trình cài đặt pixel. Vì vậy, khi được hỏi về tích hợp, bạn chỉ cần nhấp **x** trong cửa sổ thiết lập, và pixel của bạn vẫn sẽ xuất hiện trong danh sách sau khi bạn làm mới trang. 5. Khi dataset của bạn xuất hiện trong danh sách, bạn có thể tiến hành tạo chiến dịch. ## Bước 3. Tạo chiến dịch \{#step-3-create-campaign\} Để tạo chiến dịch trong Meta Ads Manager: 1. Truy cập [Meta Ads Manager](https://adsmanager.facebook.com/adsmanager/manage). Trong tab **Campaign**, nhấp vào **Create**. 2. Chọn **Sales** làm mục tiêu chiến dịch và nhấp vào **Continue**. 3. Đặt tên cho chiến dịch của bạn trong phần **Campaign name**. 4. Trong phần **Budget**, ở mục **Budget strategy**, chọn cách bạn muốn kiểm soát ngân sách: - **Campaign budget**: Lựa chọn dễ nhất nếu bạn chưa chắc cơ hội nào sẽ hiệu quả nhất. Nếu chọn tùy chọn này, Meta Ads sẽ tự động phát hiện các nhóm quảng cáo hoạt động tốt nhất và phân bổ ngân sách nhiều hơn cho chúng. Sau đó, chọn ngân sách **Daily** (hàng ngày) hoặc **Lifetime** (trọn đời) và nhập giới hạn theo đơn vị tiền tệ của bạn. Ngân sách **Daily** linh hoạt hơn khi bạn đang tìm hiểu — bạn có thể bắt đầu với số tiền nhỏ và điều chỉnh dần theo thực tế. Hoặc chọn **Schedule budget increase** để đặt quy tắc tự động tăng ngân sách theo giá trị cố định hoặc phần trăm. - **Ad set budget**: Chọn tùy chọn này nếu bạn muốn tự xác định đối tượng nào sẽ nhận nhiều hay ít ngân sách chiến dịch hơn. Nếu chưa chắc chắn, bạn có thể chọn **Share some of your budget with other ad sets** để cho phép Meta tự động điều chỉnh ngân sách nhóm quảng cáo lên đến 20% khi điều đó có lợi cho hiệu suất quảng cáo. 5. Trong **Campaign bid strategy**, chọn tùy chọn phù hợp nhất với mục tiêu của bạn: - **Highest volume (default)**: Tùy chọn đơn giản nhất để bắt đầu. Khi chọn tùy chọn này, bạn để Meta tối ưu chi phí mỗi lần nhấp nhằm đạt kết quả tốt nhất trong phạm vi ngân sách. - **Cost per result goal**: Đặt mục tiêu chi phí trên mỗi kết quả nếu bạn đã biết các chỉ số chuẩn của mình. - **Bid cap**: Đặt mức giá thầu tối đa bạn sẵn sàng trả. 6. Adapty cho phép bạn thực hiện [A/B test](ab-tests) toàn diện. Tuy nhiên, bạn cũng có thể bật A/B test trong Meta Ads nếu cần. Đọc thêm về A/B test trong Meta Ads Manager [tại đây](https://www.facebook.com/business/help/1159714227408868). 7. Bây giờ là lúc thêm nhóm quảng cáo đầu tiên vào chiến dịch. Nhấn **Next** để tiếp tục. ## Bước 4. Tạo ad set \{#step-4-create-ad-set\} Để tạo một ad set: 1. Đặt tên cho ad set của bạn trong trường **Ad set name**. 2. Trong menu dropdown **Conversion location**, chọn **Website**. 3. Trong trường **Performance goal**, chọn **Maximize number of landing page views** nếu bạn có landing page hoặc **Maximize number of link clicks** nếu bạn dùng smart link để điều hướng người dùng trực tiếp đến cửa hàng. 4. Trong trường **Dataset**, chọn dataset bạn đã tạo ở [Bước 2](#step-2-add-meta-pixel). 5. Chọn một **Conversion event**. Trong trường hợp này, đó thường sẽ là **Purchase** hoặc **Start trial**. Đừng lo nếu bạn thấy cảnh báo rằng dataset của mình chưa có sự kiện nào – điều đó chỉ có nghĩa là dataset vừa được tạo mới. 6. Nếu khi thiết lập chiến dịch bạn đã chọn **Ad set budget**, hãy chọn loại ngân sách **Daily** hay **Lifetime** và nhập giới hạn theo đơn vị tiền tệ của bạn. Ngân sách **Daily** linh hoạt hơn khi bạn còn đang tìm hiểu, vì bạn có thể bắt đầu với số tiền nhỏ rồi điều chỉnh dần theo thời gian. Đặt ngày bắt đầu và ngày kết thúc (nếu có) cho ad set. Ví dụ: nếu bạn muốn quảng cáo một ưu đãi trong ứng dụng, việc căn chỉnh khung thời gian của ad set theo đúng thời gian của ưu đãi là rất quan trọng. 7. Trong phần **Audience controls**, thiết lập cài đặt đối tượng: - **Location**: Vị trí có thể rộng hoặc hẹp tùy theo nhu cầu. Bạn có thể giới hạn **Locations** trong ad set để phù hợp với đặc thù khu vực trong quảng cáo của mình. - **Minimum age**: Chọn độ tuổi tối thiểu của người dùng sẽ thấy quảng cáo. Một số quảng cáo có thể bị yêu cầu theo quy định pháp lý. Bạn không thể chọn độ tuổi tối thiểu dưới 18 trên toàn cầu hoặc dưới 20 tại Thái Lan. - **Language**: Chỉ đặt **Language** khi đó không phải ngôn ngữ phổ biến nhất ở các quốc gia đã chọn. Ví dụ: bạn không cần chọn **English** ở Hoa Kỳ, nhưng nếu bạn nhắm đến những người nói tiếng Tây Ban Nha đang sống ở đó, bạn có thể muốn chọn **Spanish**. 8. Theo mặc định, Meta tự động tìm các nhóm người nhỏ hơn có khả năng quan tâm đến quảng cáo của bạn. Tuy nhiên, nếu bạn thêm gợi ý đối tượng, bạn có thể hướng Meta đến những người mà bạn cho là có khả năng phản hồi. Trong phần **Advantage+ audience**, bạn có thể điều chỉnh: - **Age**: Đặt phạm vi độ tuổi cụ thể để nhắm mục tiêu, giúp bạn tiếp cận tốt hơn các nhóm độ tuổi khác nhau. - **Gender**: Hiển thị quảng cáo cho tất cả người dùng hoặc nhắm mục tiêu theo giới tính. - **Detailed targeting**: Cài đặt này cho phép bạn kiểm soát đối tượng cho quảng cáo và/hoặc ứng dụng của mình một cách chi tiết nhất. Tại đây, bạn có thể tạo nhóm dựa trên **Demographics**, **Interests** hoặc **Behaviors**. Tùy thuộc vào tính năng của ứng dụng, ví dụ, bạn có thể tập trung vào các nghề nghiệp khác nhau, người hâm mộ các ban nhạc cụ thể, cha mẹ có con sơ sinh, hoặc những người có xu hướng mua sắm trực tuyến nhiều. :::note Cài đặt **Detailed targeting** áp dụng với toán tử **Or**. Nếu bạn muốn áp dụng các điều kiện với toán tử **And**, hãy nhấp vào **Define further** và chọn các điều kiện mới. ::: 9. Trong phần **Placements**, bạn có thể chọn nơi quảng cáo của mình sẽ xuất hiện. Theo mặc định, cài đặt **Advantage+** được chọn, cho phép Meta phân bổ ngân sách của bộ quảng cáo trên nhiều vị trí dựa trên nơi có khả năng hoạt động tốt nhất. Chúng tôi khuyến nghị bạn sử dụng tùy chọn này nếu bạn chưa chắc chắn về vị trí đặt quảng cáo. Nếu bạn muốn chọn các vị trí cụ thể theo cách thủ công, hãy chọn **Manual placements** và tùy chỉnh chúng. Đọc thêm [tại đây](https://www.facebook.com/business/help/965529646866485). 10. **Khuyến nghị**: Nhắm mục tiêu theo thiết bị giúp bạn tối ưu chi phí. Trong phần **Placements**, nhấp vào **Show more settings**. Ở mục con **Devices and operating system**, chọn thiết bị, hệ điều hành và phiên bản HĐH nào sẽ được đưa vào đối tượng của bạn. Điều này đảm bảo quảng cáo chỉ hiển thị cho đúng người dùng mục tiêu. Ví dụ: người dùng máy tính để bàn sẽ không thấy quảng cáo, và người dùng có phiên bản HĐH cũ mà ứng dụng không hỗ trợ sẽ bị loại trừ. 11. Khi đã sẵn sàng, nhấp vào **Next** để tiếp tục. ## Bước 5. Tạo quảng cáo \{#step-5-create-ads\} Để tạo quảng cáo trong Meta Ads Manager: 1. Đặt tên cho quảng cáo của bạn trong trường **Ad name**. 2. Trong phần **Identity**, chọn trang Facebook sẽ được dùng để đăng quảng cáo. Nếu bạn có tài khoản Instagram riêng cho ứng dụng và đã kết nối nó trong Meta Business Suite ở [Bước 1](#step-1-create-meta-ads-manager-account), hãy chọn nó trong dropdown **Instagram account**. Nếu không – chọn **Use Facebook page** để quảng cáo Instagram được đăng qua trang Facebook. 3. Trong **Ad setup**, chọn cách bạn muốn đăng quảng cáo. Khi quảng cáo ứng dụng, chúng tôi khuyến nghị chọn **Create ad** để bài đăng của bạn chuyển hướng người dùng đến ứng dụng thay vì trang Facebook. Trong trường **Format**, chọn tùy chọn phù hợp tùy thuộc vào số lượng creative bạn có và cách bạn muốn hiển thị chúng. 4. Trong phần **Destination**, giữ **Website** được chọn làm **Main destination**. Trong trường **Website URL**, dán `https://api-ua.adapty.io/api/v1/attribution/click`. Trong [Adapty Attribution](adapty-user-acquisition), [tạo một web campaign](ua-facebook) và dán nội dung **Click link** sau `https://api-ua.adapty.io/api/v1/attribution/click` vào trường **URL parameters** trong phần **Tracking**. 5. Trong phần **Ad creative**, nhấp **Set up creative** và chọn **Image ad** hoặc **Video ad**. Thao tác này sẽ mở một cửa sổ mới yêu cầu bạn tải lên tệp media, cắt xén và thêm văn bản. 6. Nếu bạn muốn tự động dịch văn bản quảng cáo, trong phần **Languages**, nhấp **Add languages**. Sau đó, thêm ngôn ngữ chính – hệ thống sẽ tự động lấy văn bản từ creative của bạn. Tiếp theo, thêm các ngôn ngữ dịch để dịch tự động. 7. Khi đã sẵn sàng, nhấp **Publish** để phát hành quảng cáo của bạn. ## Bước tiếp theo \{#whats-next\} Để kích hoạt quảng cáo, bạn sẽ cần thêm phương thức thanh toán nếu chưa thực hiện trước đó. Sau đó, bạn có thể [khám phá cách chiến dịch ảnh hưởng đến doanh thu ứng dụng trong dashboard Adapty User Acquisition](adapty-user-acquisition). Chưa sử dụng Adapty User Acquisition? [Đặt lịch gọi với chúng tôi](https://calendly.com/tnurutdinov-adapty/30min) để tìm hiểu cách nó có thể giúp bạn theo dõi và tối ưu hóa các chiến dịch quảng cáo. --- # File: tiktok-create-campaign --- --- title: "Quảng cáo ứng dụng của bạn trên TikTok for Business" --- Trong hướng dẫn từng bước này, bạn sẽ học cách tạo và thiết lập quảng cáo cho ứng dụng trên TikTok for Business, giúp bạn dễ dàng tối ưu hóa và theo dõi hiệu suất quảng cáo. ## Bước 1. Thêm thông tin doanh nghiệp \{#step-1-add-business-info\} Nếu bạn mới bắt đầu sử dụng TikTok for Business, bạn cần thêm thông tin doanh nghiệp trước: 1. Truy cập [https://ads.tiktok.com](https://ads.tiktok.com/business/) và nhấn **Get started**. 2. Đăng ký bằng email hoặc tài khoản TikTok của bạn. 3. Nhập thông tin doanh nghiệp và làm theo hướng dẫn trên màn hình. Sau khi tài khoản doanh nghiệp được phê duyệt, bạn sẽ được chuyển đến trang tạo chiến dịch đầu tiên. ## Bước 2. Tạo pixel \{#step-2-create-a-pixel\} Bạn sẽ cần một TikTok pixel để kết nối dữ liệu chiến dịch với doanh thu và đạt được kết quả tốt hơn: 1. Truy cập [**Events Manager**](https://ads.tiktok.com/i18n/events_manager/home). Nhấn **Connect data source**. 2. Chọn **Web** làm loại nguồn dữ liệu. 3. Trong cửa sổ **Add your website**, nhấn **Skip**. 4. Chọn **Manual setup** và nhấn **Next**. 5. Chọn **TikTok pixel + Events API** và nhấn **Next**. 6. Đặt tên cho pixel của bạn và nhấn **Create**. 7. Với [Adapty Attribution](adapty-user-acquisition), bạn không cần hoàn tất toàn bộ quá trình cài đặt pixel. Chỉ cần đóng cửa sổ thiết lập lại, pixel của bạn sẽ xuất hiện trong danh sách. 8. Để pixel này có thể sử dụng trong các chiến dịch, bạn cần gửi một test event đến nó từ [Adapty Attribution](adapty-user-acquisition): 1. [Tạo một chiến dịch TikTok mới](ua-tiktok). 2. Mở rộng phần dành riêng cho từng nền tảng – ví dụ: iOS. 3. Chọn một pixel từ dropdown. 4. Nhấn **Send test event**. 5. Trong dropdown, chọn event bạn sẽ dùng để tối ưu hóa quảng cáo. 6. Trong TikTok for Business, mở pixel của bạn và chuyển sang tab **Test events**. Sao chép `test_event_code`. 7. Dán vào trường **Test event code** trong Adapty và nhấn **Send**. 9. Test event sẽ xuất hiện trên TikTok sau vài phút. Khi bạn thấy nó trong phần chi tiết pixel, bạn có thể tiến hành thiết lập chiến dịch trong TikTok Ads Manager. ## Bước 3. Chọn mục tiêu chiến dịch \{#step-3-select-the-campaign-objective\} :::important Hướng dẫn này sử dụng chế độ Quick setup trong TikTok Ads Manager. Một số cài đặt được đề xuất chỉ xuất hiện ở chế độ Full view, và chúng tôi sẽ chỉ ra điều đó ở các bước liên quan. ::: Truy cập [trang tạo quảng cáo](https://ads.tiktok.com/i18n/nb_creation/create/objectives) trong Ads Manager. Ở màn hình đầu tiên, chọn mục tiêu quảng cáo và nhấn **Continue**. Chọn **Sales > Website conversion**. ## Bước 4. Điền thông tin chiến dịch \{#step-4-fill-in-the-campaign-info\} Tiếp theo, điền thông tin chiến dịch: 1. Đặt tên chiến dịch trong trường **Campaign name**. 2. Trong trường **Optimization goal**, chọn **Conversion**. 3. Chọn pixel đang hoạt động từ danh sách thả xuống và chọn một **Optimization event**. Lưu ý rằng chỉ những sự kiện đang hoạt động mới có thể chọn được. Nếu sự kiện bạn cần không có sẵn, hãy gửi một sự kiện thử nghiệm theo hướng dẫn tại [Bước 2](#step-2-create-a-pixel). 4. Quảng cáo của bạn sẽ được hiển thị trong feed và tìm kiếm TikTok. Để thiết lập thêm, nhấp vào **Advanced settings**. Trong **Placements**, cấu hình các cài đặt placement: - **User comment**: Chọn nếu bạn muốn hiển thị quảng cáo trong phần bình luận. TikTok khuyến nghị bật bình luận của người dùng để giúp quảng cáo đạt được nhiều lượt hiển thị hơn. - **Allow video download**: Cho phép người xem tải xuống quảng cáo của bạn. - **Allow video sharing**: Cho phép người xem chia sẻ quảng cáo của bạn. 5. Nhấp vào **Continue**. ## Bước 5. Thêm nội dung quảng cáo \{#step-5-add-ad-content\} Bây giờ, hãy thiết lập nội dung sáng tạo và URL đích của bạn: 1. Trong trường **TikTok account**, chọn tài khoản sẽ được dùng để đăng bài. 2. Trong [Adapty Attribution](adapty-user-acquisition), [tạo một chiến dịch web](ua-tiktok) và dán **Click link** vào trường **Destination URL**. 3. Trong phần **Creatives**, nhấp vào **+ Videos and images**. 4. Nếu muốn dùng các bài đăng TikTok của bạn làm creative, hãy chọn chúng ở tab **TikTok post**. Nếu không, chuyển sang tab **Creative library** và nhấp **Upload**. Các tệp bạn tải lên đó sẽ có thể truy cập từ tab này sau này, giúp bạn tái sử dụng chúng trong các chiến dịch khác. 5. Cắt xén creative cho phù hợp với định dạng TikTok và chọn xem chúng sẽ được dùng làm quảng cáo đơn lẻ hay một carousel. 6. Mở rộng creative đã tải lên và nhấp **+** bên cạnh **No music selected**. Bạn có thể tải lên các tệp mp3 của riêng mình tại đây. Việc thêm nhạc là bắt buộc. 7. Trong trường **Add text**, nhập nội dung sẽ được dùng làm mô tả. 8. Chọn **Place the ads on this TikTok account as a post** nếu bạn muốn đăng quảng cáo này lên tài khoản TikTok của mình. 9. Trong trường **Call to action**, chọn hoặc xóa các lời kêu gọi hành động phù hợp với quảng cáo của bạn. Chúng sẽ được TikTok tự động thêm vào quảng cáo của bạn. 10. Nhấp **Continue**. ## Bước 6. Cấu hình nhắm mục tiêu và ngân sách \{#step-6-configure-targeting-and-budget\} Cuối cùng, thiết lập đối tượng sẽ thấy quảng cáo của bạn và mức ngân sách bạn dự định chi: 1. Trong phần **Targeting**, chọn **Automatic** hoặc **Custom**. Tùy chọn **Automatic** là dễ nhất nếu bạn chưa hiểu rõ đối tượng của mình. Tuy nhiên, nếu chọn **Custom**, bạn có thể tối ưu hóa chi tiêu bằng cách chọn những nhóm người dùng có nhiều khả năng phản hồi quảng cáo của bạn hơn. 2. Nếu bạn đã chọn **Custom**, hãy cấu hình: - **Location**: Vị trí mặc định là vị trí của tài khoản quảng cáo của bạn. Nếu bạn chọn nhiều hơn một quốc gia hoặc khu vực nhắm mục tiêu, kết quả xem xét quảng cáo sẽ được trả về riêng cho từng vị trí. Việc phân phối quảng cáo thực tế cũng có thể khác nhau tùy thuộc vào các vị trí được hỗ trợ của các placement khác nhau. - **Languages**: Theo mặc định, tất cả các ngôn ngữ đều được chọn. Hãy chọn ngôn ngữ nhắm mục tiêu dựa trên ngôn ngữ được sử dụng phổ biến nhất tại vị trí bạn đã chọn. - **Gender**: Theo mặc định, tất cả giới tính đều được chọn. :::tip Nếu bạn chuyển sang chế độ Full, bạn sẽ thấy thêm phần **Device** bên dưới **Targeting**. Tại đây bạn có thể giới hạn đối tượng theo loại thiết bị, hệ điều hành và phiên bản hệ điều hành — hữu ích nếu ứng dụng của bạn yêu cầu phiên bản tối thiểu. ::: 3. Trong phần **Budget**, chọn một trong các tùy chọn được đề xuất hoặc chọn **Custom**. 4. Nếu bạn đã chọn **Custom**, hãy chọn xem bạn cần ngân sách **Daily** hay **Lifetime** và nhập giới hạn theo đơn vị tiền tệ của bạn. Ngân sách **Daily** cho bạn nhiều linh hoạt hơn trong giai đoạn tìm hiểu, vì vậy bạn có thể bắt đầu với số tiền nhỏ hơn và dần dần điều chỉnh trong quá trình chạy. 5. Trong phần **Schedule**, chọn **Continue for at least 7 days** hoặc **Custom**. Chúng tôi khuyên bạn nên đặt lịch **Custom** nếu quảng cáo của bạn có tính thời hạn, để không bỏ lỡ thời điểm cần dừng quảng cáo. 6. Nếu bạn đã chọn **Custom**, hãy đặt thời gian bắt đầu hoặc thời gian bắt đầu và kết thúc cho quảng cáo. Lưu ý rằng múi giờ tài khoản của bạn sẽ được sử dụng. 7. Nhấp vào **Publish**. Khi hoàn tất, hệ thống sẽ tạo một chiến dịch mới với một nhóm quảng cáo. Nhóm quảng cáo sẽ chứa một quảng cáo nếu bạn đã cấu hình dạng carousel, hoặc nhiều quảng cáo nếu bạn đã thêm các creative dưới dạng quảng cáo riêng lẻ. ## Bước 7. Nhập thông tin thanh toán \{#step-7-enter-payment-details\} Để bắt đầu chạy quảng cáo, sau khi cấu hình đối tượng mục tiêu và ngân sách quảng cáo, hãy nhập thông tin thanh toán. Sau đó, bạn đã sẵn sàng! ## Bước tiếp theo \{#whats-next\} Bây giờ bạn có thể [khám phá cách chiến dịch ảnh hưởng đến doanh thu ứng dụng trong dashboard Adapty User Acquisition](adapty-user-acquisition). Chưa sử dụng Adapty User Acquisition? [Đặt lịch gọi với chúng tôi](https://calendly.com/tnurutdinov-adapty/30min) để tìm hiểu cách nó có thể giúp bạn theo dõi và tối ưu hóa các chiến dịch quảng cáo. --- # File: getting-started-with-server-side-api --- --- title: "Server-side API" description: "Bắt đầu với server-side API của Adapty để quản lý gói đăng ký." --- :::tip Đang dùng AI coding agent? Xem [Kiểm tra và cấp quyền truy cập gói đăng ký từ backend của bạn](server-side-api-with-ai) để có hướng dẫn đầy đủ trên một trang. ::: Với API, bạn có thể: 1. Kiểm tra trạng thái gói đăng ký của người dùng. 2. Kích hoạt gói đăng ký của người dùng với một mức độ truy cập. 3. Lấy thuộc tính người dùng. 4. Đặt thuộc tính người dùng. 5. Lấy và cập nhật cấu hình paywall. <img src="/assets/shared/img/server.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> <p> </p> :::note Để theo dõi các sự kiện gói đăng ký, hãy dùng tích hợp [Webhook](webhook) trong Adapty hoặc tích hợp trực tiếp với dịch vụ hiện có của bạn. ::: ## Trường hợp 1: Đồng bộ người dùng đăng ký giữa web và mobile \{#case-1-sync-subscribers-between-web-and-mobile\} Nếu bạn dùng các nhà cung cấp thanh toán web như Stripe, ChargeBee hoặc các dịch vụ khác, bạn có thể dễ dàng đồng bộ người dùng đăng ký. Cách thực hiện: 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. [Kiểm tra trạng thái gói đăng ký của họ](api-adapty/operations/getProfile) bằng API. 3. Nếu người dùng đang dùng gói freemium, hiển thị paywall trên trang web của bạn. 4. Sau khi thanh toán thành công, [cập nhật trạng thái gói đăng ký](api-adapty/operations/setTransaction) trong Adapty qua API. 5. Người dùng đăng ký của bạn sẽ tự động được đồng bộ với ứng dụng mobile. ## Trường hợp 2: Cấp gói đăng ký \{#case-2-grant-a-subscription\} :::note Vì lý do bảo mật, bạn không thể cấp gói đăng ký qua SDK. ::: Nếu bạn bán hàng qua cửa hàng trực tuyến của mình, Amazon Appstore, Microsoft Store, hoặc bất kỳ nền tảng nào khác ngoài Google Play và App Store, bạn cần đồng bộ các giao dịch đó với Adapty để cung cấp quyền truy cập và theo dõi giao dịch trong analytics. 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. [Thiết lập một cửa hàng tùy chỉnh cho sản phẩm của bạn trong Adapty Dashboard](custom-store). 3. Đồng bộ giao dịch với Adapty bằng yêu cầu API [Set transaction](api-adapty/operations/setTransaction). ## Trường hợp 3: Cấp mức độ truy cập \{#case-3-grant-an-access-level\} Giả sử bạn đang chạy chương trình khuyến mãi với bản dùng thử miễn phí 7 ngày và muốn trải nghiệm đồng nhất trên tất cả nền tảng. Để đồng bộ điều này với ứng dụng mobile: 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. Dùng API để [cấp quyền truy cập premium](api-adapty/operations/grantAccessLevel) trong 7 ngày. Sau 7 ngày, người dùng không đăng ký sẽ bị hạ xuống gói miễn phí. ## Trường hợp 4: Đồng bộ thuộc tính và custom attributes của người dùng \{#case-4-sync-users-properties-and-custom-attributes\} Nếu bạn có custom attributes cho người dùng — chẳng hạn như số từ đã học trong ứng dụng học ngoại ngữ — bạn cũng có thể đồng bộ chúng. 1. <InlineTooltip tooltip="Gán một ID duy nhất cho mỗi người dùng">[iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), và [Unity](unity-identifying-users)</InlineTooltip>. 2. [Cập nhật thuộc tính](api-adapty/operations/updateProfile) qua API hoặc SDK. Các custom attributes này có thể được dùng để tạo phân khúc và chạy A/B test. ## Trường hợp 5: Quản lý cấu hình paywall \{#case-5-manage-paywall-configurations\} Bạn có thể [cập nhật Remote Config trong paywall](api-adapty/operations/updatePaywall) để điều chỉnh giao diện và hành vi paywall một cách linh hoạt mà không cần triển khai lại ứng dụng. --- **Tiếp theo:** - Tiến hành [xác thực cho server-side API](ss-authorization) - Các yêu cầu: - [Lấy hồ sơ người dùng](api-adapty/operations/getProfile) - [Tạo hồ sơ người dùng](api-adapty/operations/createProfile) - [Cập nhật hồ sơ người dùng](api-adapty/operations/updateProfile) - [Xóa hồ sơ người dùng](api-adapty/operations/deleteProfile) - [Cấp mức độ truy cập](api-adapty/operations/grantAccessLevel) - [Thu hồi mức độ truy cập](api-adapty/operations/revokeAccessLevel) - [Đặt giao dịch](api-adapty/operations/setTransaction) - [Xác thực giao dịch, cấp mức độ truy cập cho khách hàng và nhập lịch sử giao dịch của họ](api-adapty/operations/validateStripePurchase) - [Thêm định danh tích hợp](api-adapty/operations/setIntegrationIdentifiers) - [Lấy paywall](api-adapty/operations/getPaywall) - [Liệt kê các paywall](api-adapty/operations/listPaywalls) - [Cập nhật paywall](api-adapty/operations/updatePaywall) --- # File: onboardings --- --- title: "Onboardings" --- :::warning Trình tạo onboarding không cần code đã hoàn chỉnh về mặt chức năng, nhưng Adapty không còn bổ sung tính năng hay phát hành cập nhật cho nó nữa. Với các dự án mới, hãy cân nhắc sử dụng [Adapty Flow Builder](adapty-flow-builder) — trình chỉnh sửa trực quan không cần code dành cho paywall một màn hình và flow onboarding nhiều màn hình hiển thị trực tiếp trên thiết bị: - **Mọi loại flow**: Xây dựng paywall một màn hình, onboarding nhiều bước có tích hợp paywall, và bất kỳ dạng nào ở giữa. - **Hiển thị native**: Flow hiển thị thông qua Adapty SDK, không dùng web view. - **Cập nhật mà không cần triển khai lại**: Thay đổi nội dung, thiết kế hoặc logic bất kỳ lúc nào, và bản cập nhật đến tay người dùng mà không cần phát hành ứng dụng. ::: Onboarding của Adapty cho phép các nhóm không chuyên kỹ thuật xây dựng flow onboarding mà không cần viết code. Trình tạo không cần code tạo ra một chuỗi màn hình giới thiệu người dùng với ứng dụng của bạn. Bạn có thể cá nhân hóa các màn hình với câu hỏi tương tác và biến số, sau đó chạy A/B test để tìm ra flow hoạt động tốt nhất. Onboarding khả dụng cho các ứng dụng sử dụng Adapty SDK v3.8.0+ (iOS, Android, React Native, Flutter), v3.14.0+ (Unity) hoặc v3.15.0+ (Kotlin Multiplatform, Capacitor). ## Cách hoạt động \{#how-it-works\} 1. [Thiết kế onboarding trong trình chỉnh sửa không cần code.](design-onboarding) 2. [Tạo một placement cho onboarding.](create-onboarding#step-2-create-a-placement-for-your-onboarding) 3. Tích hợp onboarding với dự án của bạn bằng Adapty SDK: - [iOS](ios-onboardings) - [Android](android-onboardings) - [React Native](react-native-onboardings) - [Flutter](flutter-onboardings) - [Unity](unity-onboardings) - [Kotlin Multiplatform](kmp-onboardings) - [Capacitor](capacitor-onboardings) 4. Kiểm thử onboarding và phát hành cho người dùng của bạn. --- # File: create-onboarding --- --- title: "Tạo onboarding" --- [Onboarding](onboardings) giúp người dùng mới làm quen với giá trị, tính năng và cách sử dụng ứng dụng di động của bạn. ## Bước 1. Tạo onboarding \{#step-1-create-an-onboarding\} Để tạo onboarding mới trong Adapty Dashboard: 1. Vào **Onboardings** từ menu chính của Adapty. Trang này hiển thị tổng quan về tất cả onboarding bạn đã thiết lập cùng với các chỉ số của chúng. Nhấp vào **Create onboarding**. <img src="/assets/shared/img/create-onboarding1.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Đặt tên mô tả cho onboarding của bạn và nhấp **Proceed to build onboarding**. <img src="/assets/shared/img/create-onboarding2.png" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Bạn sẽ được chuyển đến trình xây dựng onboarding. Trang này có sẵn một mẫu demo mặc định để bạn tham khảo, giúp hiểu cách onboarding thu thập dữ liệu và cách cá nhân hóa chúng bằng biến và câu hỏi. Hãy thoải mái xóa các màn hình không cần thiết và [thiết kế trải nghiệm onboarding của riêng bạn](design-onboarding) tại đây. <img src="/assets/shared/img/create-onboarding3.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Khi đã sẵn sàng, nhấp nút **Preview** ở góc trên bên phải. Tự trải nghiệm toàn bộ flow onboarding để đảm bảo mọi thứ hoạt động đúng như mong đợi. <img src="/assets/shared/img/create-onboarding4.png" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nếu mọi thứ hoạt động tốt, nhấp **Publish** ở góc trên bên phải. Vui lòng chờ cho đến khi quá trình xuất bản hoàn tất trước khi quay lại Adapty. Nếu không, tiến trình của bạn sẽ bị mất. :::danger Nếu bạn không nhấp **Publish**, SDK sẽ không thể lấy onboarding bạn đã tạo. ::: <img src="/assets/shared/img/create-onboarding5.png" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi onboarding được xuất bản, nhấp **Back to Adapty**. Onboarding của bạn đã được tạo và bạn có thể thêm nó vào một placement để bắt đầu sử dụng. ## Bước 2. Tạo placement cho onboarding của bạn \{#step-2-create-a-placement-for-your-onboarding\} 1. Vào **Placements** từ menu chính và chuyển sang tab **Onboardings**. Nhấp **Create placement**. <img src="/assets/shared/img/create-onboarding6.png" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhập tên và ID cho placement. Sau đó, nhấp **Run onboarding** và chọn onboarding hiển thị cho tất cả người dùng. 3. Nếu bạn đã chuẩn bị một onboarding riêng cho một nhóm người dùng cụ thể, [thêm nhiều đối tượng hơn](audience) và chọn onboarding khác cho họ. ## Bước 3. Tích hợp onboarding vào ứng dụng của bạn \{#step-3-integrate-the-onboarding-into-your-app\} :::important Onboarding khả dụng cho các ứng dụng sử dụng Adapty SDK v3.8.0 trở lên (iOS, Android, React Native, Flutter), v3.14.0 trở lên (Unity), hoặc v3.15.0 trở lên (Kotlin Multiplatform, Capacitor). ::: Để bắt đầu hiển thị onboarding trong ứng dụng, hãy tích hợp chúng qua Adapty SDK: - [iOS](ios-onboardings) - [Android](android-onboardings) - [React Native](react-native-onboardings) - [Flutter](flutter-onboardings) - [Unity](unity-onboardings) - [Kotlin Multiplatform](kmp-onboardings) - [Capacitor](capacitor-onboardings) Để biết onboarding nào hoạt động hiệu quả hơn, bạn cũng có thể chạy [A/B test](ab-tests). --- # File: design-onboarding --- --- title: "Thiết kế onboarding" description: "Tạo onboarding ý nghĩa." --- Trình tạo onboarding ứng dụng di động không cần code là một công cụ mạnh mẽ và linh hoạt, giúp bạn mang lại trải nghiệm onboarding tốt nhất cho người dùng. Bạn không cần phải là lập trình viên hay designer để có được kết quả tuyệt vời. ## Màn hình onboarding \{#onboarding-screens\} Flow onboarding bao gồm nhiều màn hình mà bạn thêm vào và thiết kế. Người dùng sẽ nhấn nút để điều hướng giữa các màn hình. :::tip Nếu một số người dùng cần một flow hơi khác (ví dụ: trong ứng dụng fitness, bạn có thể muốn hiển thị hình ảnh "mục tiêu" khác nhau tùy theo giới tính của người dùng), bạn không cần tạo các onboarding riêng biệt. Thay vào đó, bạn có thể ẩn một số màn hình theo mặc định và chỉ hiển thị chúng trong một số trường hợp nhất định. ::: ## Các thành phần onboarding \{#onboarding-elements\} Các thành phần onboarding được hiển thị ở bên trái theo thứ tự chúng xuất hiện. Nhấn **Add** ở góc trên bên phải để thêm thành phần mới. Có các nhóm thành phần sau mà bạn có thể thêm: - **Containers**: Container cho phép bạn thiết lập bố cục linh hoạt. Ví dụ: nếu bạn muốn thêm văn bản hai cột, bạn cần thêm **Columns** rồi kéo hai khối văn bản vào **Columns** trên bảng bên trái. Hoặc nếu bạn đang thêm carousel, bạn cần thêm hình ảnh vào các thành phần **Media** bên trong. - **Typography**: Thêm các khối văn bản được định dạng sẵn và tùy chỉnh giao diện theo nhu cầu. - **Media & Display**: Ngoài hình ảnh và video, bạn có thể thêm các biểu đồ động thể hiện giá trị ứng dụng của mình và khuyến khích người dùng. **Các định dạng video được hỗ trợ** là MP4 và WebM. **Kích thước file media tối đa** là 15 MB. Nếu bạn muốn thêm một thành phần hoạt hình không được hỗ trợ (như Lottie), bạn có thể chuyển đổi nó thành video (ví dụ: với [công cụ này](https://www.lottielab.com/lottie/lottie-to-video)) và nhúng vào dưới dạng video. - **Quiz**: Tạo các bảng câu hỏi ngắn với tùy chọn văn bản và hình ảnh để tùy chỉnh trải nghiệm onboarding và hiểu người dùng của bạn hơn. - **Inputs**: Thu thập dữ liệu từ người dùng. - **Buttons**: Nút cho phép người dùng điều hướng giữa các màn hình, đóng onboarding hoặc chuyển đến paywall. Bạn cũng có thể thêm nút bóng bẩy hoặc chuyển động để thu hút sự chú ý của người dùng và chuyển đổi lượt cài đặt thành giao dịch mua. - **Loaders**: Các loader có hoạt ảnh giữ cho người dùng tập trung trong quá trình chờ. - **User engagement**: Thêm lời chứng thực, danh sách email người dùng và đếm ngược. :::note Trong nhóm **Media & Display**, bạn cũng có thể thêm mã HTML tùy chỉnh nếu các tùy chọn tùy chỉnh hiện có chưa đủ. Tuy nhiên, các thành phần HTML tùy chỉnh không được tải trước hay lưu vào bộ nhớ cache, vì vậy nên sử dụng **Raw HTML** chỉ cho các thành phần nhỏ, nhẹ. ::: <img src="/assets/shared/img/design-onboarding4.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### ID thành phần và ID hành động \{#element-id-and-action-id\} Nếu bạn muốn sử dụng một nút cho các hành động tùy chỉnh, hãy gán cho nó một **action ID** rồi sử dụng nó trong mã nguồn của bạn. Action ID cho phép bạn xử lý các nút khác nhau có cùng action ID theo cùng một cách. <img src="/assets/shared/img/ios-events-1.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Nếu bạn muốn xử lý dữ liệu nhập của người dùng trong một trường cụ thể (ví dụ: lưu tuổi hoặc email của họ), hãy gán cho nó một **element ID** rồi sử dụng nó trong mã nguồn để liên kết câu hỏi với câu trả lời. Element ID chỉ được sử dụng một lần trong onboarding của bạn. <img src="/assets/shared/img/design-onboarding5.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Tùy chọn tùy chỉnh \{#customization-options\} Bạn có các tùy chọn tùy chỉnh sau trong trình tạo: - Tab **Styles**: Điều chỉnh giao diện của thành phần. <img src="/assets/shared/img/design-onboarding1.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> - Tab **Element**: Đặt các thuộc tính của thành phần, như khả năng hiển thị, hành động khi nhấn nút hoặc các thuộc tính khác không liên quan đến giao diện của thành phần. <img src="/assets/shared/img/design-onboarding2.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> - Tab **Screen**: Thiết lập cấu hình màn hình chung, như tiêu đề hoặc hiển thị bộ đếm màn hình. <img src="/assets/shared/img/design-onboarding3.png" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Sao chép màn hình và thành phần \{#copy-screens-and-elements\} Nếu bạn đã tạo một onboarding và muốn tái sử dụng một phần của nó, hoặc nếu bạn muốn thực hiện các thay đổi nhỏ và chạy A/B test, bạn có thể sao chép một hoặc nhiều màn hình từ onboarding này sang onboarding khác. Để sao chép màn hình, mở onboarding builder và thực hiện một trong các cách sau: - Nhấp chuột phải vào một màn hình và chọn **Copy** - Chọn màn hình mong muốn và nhấn `Ctrl+C` (Windows) hoặc `⌘+C` (Mac) Bạn cũng có thể sao chép các thành phần hoặc khối văn bản riêng lẻ, trong cùng một onboarding hoặc giữa các onboarding khác nhau. ## Sao chép màn hình từ web-to-app funnel \{#copy-screens-from-web-to-app-funnels\} Nếu bạn sử dụng các web-to-app funnel được tạo trong [FunnelFox](https://funnelfox.com/) và muốn sử dụng các màn hình từ funnel trong onboarding, bạn có thể thực hiện nhanh chóng bằng cách sao chép màn hình trong funnel builder và dán vào onboarding builder: 1. Trong FunnelFox funnel builder, nhấp chuột phải vào một màn hình và chọn **Copy**, hoặc chọn màn hình và nhấn `Ctrl+C`/`⌘+C`. 2. Mở onboarding builder. 3. Nhấp chuột phải vào màn hình mà bạn muốn chèn màn hình đã sao chép và chọn **Paste**, hoặc chọn nó và nhấn `Ctrl+V`/`⌘+V`. Màn hình đã sao chép sẽ được chèn bên dưới màn hình đang chọn. <img src="/assets/shared/img/funnel-to-onboarding.gif" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> --- # File: adapty-paywall-builder --- --- title: "Adapty Paywall Builder (Legacy)" description: "Tạo paywall và onboarding flow bằng trình chỉnh sửa trực quan không cần code." --- :::warning Paywall Builder vẫn hoạt động đầy đủ, nhưng Adapty không còn bổ sung tính năng hay cập nhật cho nó nữa. Với các dự án mới, hãy cân nhắc sử dụng [Adapty Flow Builder](adapty-flow-builder) — trình chỉnh sửa trực quan không cần code dành cho paywall một màn hình và onboarding flow nhiều màn hình, hiển thị trực tiếp trên thiết bị: - **Mọi loại flow**: Xây dựng paywall một màn hình, onboarding nhiều bước có kèm paywall, và bất cứ thứ gì ở giữa. - **Hiển thị gốc (native)**: Flow hiển thị qua Adapty SDK, không dùng web view. - **Cập nhật không cần phát hành lại**: Thay đổi nội dung, thiết kế hoặc logic bất cứ lúc nào — người dùng nhận cập nhật mà không cần app release mới. ::: **Paywall Builder** của Adapty là công cụ trực quan không cần code để thiết kế paywall tùy chỉnh. Bạn có thể bắt đầu từ template, tùy chỉnh bố cục và thêm các thành phần như carousel, card, danh sách sản phẩm và footer. Builder cũng hỗ trợ font tùy chỉnh, tag sản phẩm và bản địa hóa. Paywall Builder yêu cầu Adapty SDK v3.0 trở lên. Sau khi thiết kế xong paywall, hãy [thêm vào placement](add-audience-paywall-ab-test) và hiển thị trong ứng dụng của bạn: - [iOS](ios-quickstart-paywalls) - [Android](android-quickstart-paywalls) - [React Native](react-native-quickstart-paywalls) - [Flutter](flutter-quickstart-paywalls) - [Unity](unity-quickstart-paywalls) - [Capacitor](capacitor-quickstart-paywalls) - [Kotlin Multiplatform](kmp-quickstart-paywalls) --- # File: flutterflow --- --- title: "Adapty Plugin for FlutterFlow" description: "Tích hợp FlutterFlow với Adapty để quản lý gói đăng ký hiệu quả hơn." --- Adapty là một nền tảng đa năng được thiết kế để giúp các ứng dụng di động phát triển. Dù bạn mới bắt đầu hay đã có hàng nghìn người dùng, Adapty giúp bạn tiết kiệm hàng tháng khi tích hợp in-app purchase và tăng gấp đôi doanh thu gói đăng ký nhờ quản lý paywall. Plugin Adapty cho FlutterFlow cho phép bạn tận dụng toàn bộ tính năng của Adapty mà không cần viết code. Bạn có thể thiết kế trang paywall trong FlutterFlow, bật tính năng mua hàng cho chúng, rồi kiểm soát từ xa các sản phẩm hiển thị — bao gồm nhắm mục tiêu theo nhóm người dùng cụ thể hoặc A/B test. Sau khi phát hành ứng dụng, bạn có thể truy cập ngay số liệu phân tích chi tiết về giao dịch mua của khách hàng trực tiếp trên dashboard của chúng tôi. Muốn cập nhật sản phẩm hiển thị trên paywall? Rất đơn giản! Chỉ cần thay đổi vài thao tác trong Adapty Dashboard, và khách hàng của bạn sẽ thấy sản phẩm mới ngay lập tức — không cần phát hành phiên bản ứng dụng mới! Những gì Adapty còn mang lại cho bạn: - **Gói đăng ký và In-App Purchase**: Adapty xử lý xác thực biên lai phía server cho bạn và đồng bộ khách hàng trên mọi nền tảng, kể cả web. - **A/B test cho paywall**: Kiểm tra các mức giá, thời hạn, thời gian dùng thử và các yếu tố hiển thị khác nhau để tối ưu hóa gói đăng ký và sản phẩm mua một lần. - **Phân tích mạnh mẽ**: Truy cập các chỉ số chi tiết để hiểu rõ hơn và cải thiện khả năng kiếm tiền của ứng dụng. - **Tích hợp**: Adapty kết nối liền mạch với các công cụ phân tích của bên thứ ba như Amplitude, AppsFlyer, Adjust, Branch, Mixpanel, Facebook Ads, AppMetrica, Webhook tùy chỉnh và nhiều hơn nữa. --- # File: ff-getting-started --- --- title: "Bắt đầu" description: "Bắt đầu với Adapty Feature Flags để cá nhân hóa các luồng đăng ký." --- Với Adapty, bạn có thể tạo và chạy các paywall cũng như A/B test tại các điểm khác nhau trong hành trình người dùng ứng dụng di động của mình, chẳng hạn như Onboarding, Settings, v.v. Những điểm này được gọi là [Placements](placements). Một placement trong ứng dụng của bạn có thể quản lý nhiều paywall hoặc [A/B test](ab-tests) cùng một lúc, mỗi cái được tạo cho một nhóm người dùng nhất định mà chúng tôi gọi là [Audiences](audience). Hơn nữa, bạn có thể thử nghiệm với các paywall, thay thế cái này bằng cái khác theo thời gian mà không cần phát hành phiên bản ứng dụng mới. Thứ duy nhất bạn hardcode trong ứng dụng di động là placement ID. <img src="/assets/shared/img/audience.jpg" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Thư viện Adapty giữ cho paywall của bạn luôn được cập nhật với các sản phẩm mới nhất từ Adapty Dashboard. Nó [lấy dữ liệu sản phẩm](ff-action-flow) và [hiển thị trên paywall của bạn](ff-add-variables-to-paywalls), [xử lý các giao dịch mua](ff-make-purchase), và [kiểm tra mức độ truy cập của người dùng](ff-check-subscription-status) để xem họ có nên nhận được nội dung trả phí hay không. Để bắt đầu, chỉ cần [thêm thư viện Adapty](ff-getting-started#add-the-adapty-library-as-a-dependency) vào dự án FlutterFlow của bạn và [khởi tạo nó](ff-getting-started#initiate-adapty-plugin) như hướng dẫn bên dưới. :::warning Trước khi bắt đầu, hãy lưu ý các hạn chế sau: - Thư viện Adapty cho FlutterFlow không hỗ trợ ứng dụng web. Tránh biên dịch ứng dụng web với thư viện này. - Thư viện Adapty cho FlutterFlow không hỗ trợ các paywall được tạo bằng Adapty Paywall Builder. Bạn cần tự thiết kế paywall của mình trong FlutterFlow trước khi kích hoạt mua hàng với Adapty. ::: ## Thêm thư viện Adapty làm dependency \{#add-the-adapty-library-as-a-dependency\} 1. Trong [FlutterFlow Dashboard](https://app.flutterflow.io/dashboard), mở dự án của bạn, sau đó nhấp vào **Settings and Integrations** từ menu bên trái. Trong phần **Project setup** ở bên trái, chọn **Project dependencies**. <img src="/assets/shared/img/main_settings.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong phần **FlutterFlow Libraries**, nhấp vào **Add Library** và nhập `adapty-xtuel0`. Nhấp vào **Add**. 3. Bây giờ, bạn cần liên kết SDK key của mình với thư viện. Nhấp vào **View details** bên cạnh thư viện. <img src="/assets/shared/img/ff_view_details.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Sao chép **Public SDK key** từ tab [**App Settings** -> **General**](https://app.adapty.io/settings/general) trong Adapty Dashboard. <img src="/assets/shared/FF_img/adaptyapikey.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Dán key vào **AdaptyApiKey** trong FlutterFlow. <img src="/assets/shared/img/ff_apikey.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Thư viện Adapty FF giờ đây sẽ được thêm vào dự án của bạn như một dependency. Trong cửa sổ thư viện **Adapty** FF, bạn sẽ tìm thấy tất cả các tài nguyên Adapty đã được import vào dự án của bạn. ## Gọi action kích hoạt mới khi khởi động ứng dụng \{#call-the-new-activation-action-at-application-launch\} 1. Đi đến phần **Custom Code** từ menu bên trái và mở `main.dart`. <img src="/assets/shared/img/ff_dartmain.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào **+** và chọn `activate (Adapty)`. <img src="/assets/shared/img/ff_activate.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp vào **Save**. ## Khởi tạo plugin Adapty \{#initiate-adapty-plugin\} Để Adapty Dashboard nhận ra ứng dụng của bạn, bạn cần cung cấp một key đặc biệt trong FlutterFlow. 1. Trong dự án FlutterFlow của bạn, đi đến **Settings and Integrations > Permissions** từ menu bên trái. 2. Trong cửa sổ **Permissions** vừa mở, nhấp vào nút **Add Permission**. 3. Trong cả hai trường **iOS Permission Key** và **Android Permission Key**, dán `AdaptyPublicSdkKey`. 4. Đối với **Permission Message**, sao chép **Public SDK key** từ tab [**App Settings** -> **General**](https://app.adapty.io/settings/general) trong Adapty Dashboard. Mỗi ứng dụng có SDK key riêng, vì vậy nếu bạn có nhiều ứng dụng, hãy đảm bảo lấy đúng key. <img src="/assets/shared/img/ff_permissions.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau khi hoàn thành các bước này, bạn sẽ có thể gọi paywall trong ứng dụng FlutterFlow của mình và kích hoạt mua hàng thông qua đó. ## Tiếp theo là gì? \{#whats-next\} 1. [Tạo một action flow](ff-action-flow) để xử lý các sản phẩm paywall Adapty và dữ liệu của chúng trong FlutterFlow. 2. [Ánh xạ dữ liệu nhận được vào paywall](ff-add-variables-to-paywalls) mà bạn đã thiết kế trong FlutterFlow. 3. [Thiết lập nút mua hàng](ff-make-purchase) trên paywall của bạn để xử lý các giao dịch thông qua Adapty khi được nhấp. 4. Cuối cùng, [thêm kiểm tra trạng thái gói đăng ký](ff-check-subscription-status) để xác định có nên hiển thị nội dung trả phí cho người dùng hay không. --- # File: ff-action-flow --- --- title: "Bước 1. Tạo flow để hiển thị dữ liệu paywall" description: "Thiết lập các action flow cho feature flag trong Adapty để cá nhân hóa hành trình đăng ký của người dùng." --- :::important Khi sử dụng plugin FlutterFlow, bạn không thể dùng các paywall được tạo trong Adapty Paywall Builder. Bạn phải tự xây dựng trang paywall của mình trong FlutterFlow và kết nối nó với Adapty. ::: Sau khi thêm thư viện Adapty làm dependency cho dự án FlutterFlow, đã đến lúc xây dựng flow **lấy dữ liệu paywall và sản phẩm từ Adapty rồi hiển thị trên paywall bạn đã thiết kế trong FlutterFlow**. Trước tiên, chúng ta cần nhận dữ liệu paywall từ Adapty. Chúng ta sẽ bắt đầu bằng cách yêu cầu paywall của Adapty, sau đó lấy các sản phẩm liên quan, và cuối cùng kiểm tra xem dữ liệu đã được nhận thành công chưa. Nếu thành công, chúng ta sẽ hiển thị tiêu đề sản phẩm và giá trên trang paywall. Ngược lại, chúng ta sẽ hiển thị thông báo lỗi. Trước khi tiếp tục, hãy đảm bảo bạn đã hoàn thành các bước sau: 1. [Tạo ít nhất một paywall và thêm ít nhất một sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 2. [Tạo ít nhất một placement](create-placement) và [thêm paywall của bạn vào đó](add-audience-paywall-ab-test) trong Adapty Dashboard. Hãy bắt đầu! ## Bước 1.1. Yêu cầu paywall từ Adapty \{#step-11-request-adapty-paywall\} Như đã đề cập, để hiển thị dữ liệu trên paywall FlutterFlow, trước tiên chúng ta cần lấy dữ liệu đó từ Adapty. Bước đầu tiên là lấy chính paywall của Adapty. Đây là cách thực hiện: 1. Mở màn hình paywall của bạn và chuyển sang phần **Actions** ở thanh bên phải. Tại đó, mở **Action Flow Editor**. <img src="/assets/shared/img/ff_action_flow.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong cửa sổ **Select Action Trigger**, chọn **On Page Load**. <img src="/assets/shared/img/ff_action_trigger.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấn **Add Action**. Sau đó, tìm kiếm custom action `getPaywall` và chọn nó. <img src="/assets/shared/img/ff_getpaywall.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Trong phần **Set Actions Arguments**, nhập ID thực của [placement bạn đã tạo](create-placement) trong Adapty Dashboard có chứa paywall. Trong ví dụ này là `monthly`. Hãy chắc chắn sử dụng ID placement thực của bạn! <img src="/assets/shared/img/ff_placementid.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nếu bạn đã [bản địa hóa](localizations-and-locale-codes) paywall của mình trong Adapty dashboard, bạn cũng có thể thiết lập tham số **locale**. 6. Trong **Action Output Variable Name**, tạo một biến mới và đặt tên là `getPaywallResult`. Chúng ta sẽ dùng biến này ở bước tiếp theo để tham chiếu đến paywall Adapty và yêu cầu các sản phẩm của nó. <img src="/assets/shared/img/ff_getpaywallresult.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 1.2. Yêu cầu sản phẩm từ paywall Adapty \{#step-12-request-adapty-paywall-products\} Tuyệt! Chúng ta đã lấy được paywall Adapty. Bây giờ, hãy lấy các sản phẩm liên quan đến paywall này: 1. Nhấn **+** bên dưới action vừa tạo và chọn **Add Action**. Action này sẽ nhận các sản phẩm từ paywall Adapty. Để làm điều này, tìm kiếm và chọn `getPaywallProducts`. 2. Trong phần **Set Actions Arguments**, chọn biến `getPaywallResult` đã tạo trước đó. <img src="/assets/shared/img/ff_getpaywallproduct.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Điền vào các trường khác như sau: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: No further changes <img src="/assets/shared/img/ff_getpaywallresult2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấn **Confirm**. 5. Trong **Action Output Variable Name**, tạo một biến mới và đặt tên là `getPaywallProductsResult`. Chúng ta sẽ dùng biến này để liên kết paywall bạn đã thiết kế trong FlutterFlow với dữ liệu paywall Adapty. <img src="/assets/shared/img/ff_getpaywallproductsresult.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 1.3. Thêm kiểm tra xem paywall đã tải thành công chưa \{#step-13-add-check-if-the-paywall-uploaded-successfully\} Trước khi tiếp tục, hãy xác minh rằng paywall Adapty đã được nhận thành công. Nếu có, chúng ta có thể cập nhật paywall với dữ liệu sản phẩm. Nếu không, chúng ta sẽ xử lý lỗi. Đây là cách thêm kiểm tra: 1. Nhấn **+** và nhấn **Add Conditional**. <img src="/assets/shared/img/ff-add-conditional.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Trong phần **Action Output**, chọn biến output của action đã tạo trước đó (`getPaywallResult` trong ví dụ của chúng ta). <img src="/assets/shared/img/ff-getpaywallresult.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Để xác minh rằng paywall Adapty đã được nhận, kiểm tra sự hiện diện của một trường có giá trị. Điền vào các trường như sau: - **Available Options**: Has Field - **Field (AdaptyGetPaywallResult)**: value 4. Nhấn **Confirm** để hoàn tất điều kiện. ## Bước 1.4. Ghi log lượt xem paywall \{#step-14-log-the-paywall-review\} Để đảm bảo analytics của Adapty theo dõi lượt xem paywall, chúng ta cần ghi log sự kiện này. Nếu không có bước này, lượt xem sẽ không được tính trong analytics. Đây là cách thực hiện: 1. Nhấn **+** bên dưới nhãn **TRUE** và nhấn **Add Action**. 2. Trong trường **Select Action**, tìm kiếm và chọn **logShowPaywall**. <img src="/assets/shared/img/ff-logshowpaywall.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấn **Value** trong khu vực **Set Action Arguments** và chọn biến `getPaywallResult` chúng ta đã tạo. Biến này chứa dữ liệu paywall. 4. Điền vào các trường như sau: - **Available Options**: Data Structured Field - **Select Field**: value 5. Nhấn **Confirm**. <img src="/assets/shared/img/ff-lohsgowpaywallresult.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 1.5. Hiển thị lỗi nếu không nhận được paywall \{#step-15-show-error-if-paywall-not-received\} Nếu paywall Adapty không được nhận, bạn cần [xử lý lỗi](error-handling-on-flutter-react-native-unity#system-storekit-codes). Trong ví dụ này, chúng ta sẽ chỉ đơn giản hiển thị một thông báo cảnh báo. 1. Thêm action **Informational Dialog** vào nhãn **FALSE**. 2. Trong trường **Title**, thêm văn bản bạn muốn hiển thị làm tiêu đề dialog. Trong ví dụ này là **Error**. 3. Nhấn **Value** trong ô **Message**. 4. Điền vào các trường như sau: - **Set Variable**: biến `getPaywallProductResult` chúng ta đã tạo - **Available Options**: Data Structure Field - **Select Field**: error - **Available Options**: Data Structure Field - **Select Field**: errorMessage <img src="/assets/shared/img/ff-error.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấn **Confirm**. 6. Thêm action **Terminate** vào flow **FALSE**. <img src="/assets/shared/img/ff-terminate.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Nhấn **Close** ở góc trên bên phải. Chúc mừng! Bạn đã nhận thành công dữ liệu sản phẩm. Bây giờ, hãy [liên kết nó với paywall bạn đã thiết kế trong FlutterFlow](ff-add-variables-to-paywalls). --- # File: ff-add-variables-to-paywalls --- --- title: "Bước 2. Thêm dữ liệu vào trang paywall" description: "Thêm biến Feature Flag vào paywall trong Adapty." --- Sau khi đã [nhận được toàn bộ dữ liệu sản phẩm cần thiết](ff-action-flow), đã đến lúc ánh xạ chúng vào paywall đẹp mắt mà bạn đã thiết kế trong FlutterFlow. Trong ví dụ này, chúng ta sẽ ánh xạ tiêu đề sản phẩm và giá của nó. ## Bước 2.1. Thêm tiêu đề sản phẩm vào trang paywall \{#step-21-add-product-title-to-paywall-page\} 1. Nhấp đúp vào văn bản sản phẩm trên trang paywall của bạn. Trong cửa sổ **Set from Variable**, tìm kiếm biến `getPaywallProductResult` và chọn nó. <img src="/assets/shared/img/ff-paywall-text.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Điền vào các trường như sau: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structured Field - **Select Field**: localizedTitle - **Default Variable Value**: null - **UI Builder Display Value**: Tùy ý, trong ví dụ là `product.title` <img src="/assets/shared/img/ff-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp **Confirm** để lưu các thay đổi. ## Bước 2.2. Thêm văn bản giá vào trang paywall \{#step-22-add-price-text-to-paywall-page\} Lặp lại các bước từ Bước 2.1 cho văn bản giá như hướng dẫn bên dưới: 1. Nhấp đúp vào văn bản giá trên trang paywall của bạn. Trong cửa sổ **Set from Variable**, tìm kiếm biến `getPaywallProductResult` và chọn nó. <img src="/assets/shared/img/ff-price.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Điền vào các trường như sau: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structured Field - **Select Field**: price - **Default Variable Value**: null - **UI Builder Display Value**: Tùy ý, trong ví dụ là `product.price` 3. Nhấp nút **Confirm** để lưu các thay đổi. ### Thêm giá theo đơn vị tiền tệ địa phương vào trang paywall \{#add-price-in-local-currency-to-paywall-page\} 1. Nhấp đúp vào giá trên trang paywall của bạn. Trong cửa sổ **Set from Variable**, tìm kiếm biến `getPaywallProductResult` và chọn nó. 2. Điền vào các trường như sau: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structured Field - **Select Field**: price - **Available Options**: Data Structured Field - **Select Field**: amount - **Available Options**: Decimal - **Decimal Type**: Automatic - **Default Variable Value**: null - **UI Builder Display Value**: Tùy ý, trong ví dụ là `price.amount` 3. Nhấp **Confirm** để lưu các thay đổi. Và thế là xong! Khi bạn khởi chạy ứng dụng, nó sẽ hiển thị dữ liệu sản phẩm từ paywall Adapty trực tiếp trên trang paywall của bạn! Bây giờ hãy [cho phép người dùng mua sản phẩm này](ff-make-purchase). --- # File: ff-make-purchase --- --- title: "Bước 3. Kích hoạt tính năng mua hàng" description: "Tìm hiểu cách thực hiện mua hàng bằng hệ thống Feature Flags của Adapty." --- Chúc mừng! Bạn đã [thiết lập thành công paywall để hiển thị dữ liệu sản phẩm từ Adapty](ff-add-variables-to-paywalls), bao gồm tên và giá sản phẩm. Bây giờ, hãy chuyển sang bước cuối cùng – cho phép người dùng thực hiện mua hàng qua paywall. ## Bước 3.1. Cho phép người dùng mua hàng \{#step-31-enable-users-to-make-purchases\} 1. Nhấp đúp vào nút mua trên trang paywall của bạn. Trong bảng bên phải, mở phần **Actions** nếu chưa mở. 2. Mở **Action Flow Editor**. <img src="/assets/shared/img/ff-action-flow-editor.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Select Action Trigger**, chọn **On Tap**. 4. Trong cửa sổ **No Actions Created**, nhấp **Add Action**. Tìm kiếm action `makePurchase` và chọn nó. <img src="/assets/shared/img/ff-makepurchase.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Trong phần **Set Actions Arguments**, chọn biến `getPaywallProductsResult` đã tạo trước đó. 6. Điền vào các trường như sau: - **Available Options**: Data Structure Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First <img src="/assets/shared/img/ff-makepurchase-value.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 7. Nhấp vào `subscriptionUpdateParameters`, tìm kiếm `AdaptySubscriptionUpdateParameters` và chọn nó. Nhấp **Confirm**. :::info Theo mặc định, bạn có thể để trống tất cả các trường của object. Bạn chỉ cần điền vào khi muốn thay thế một gói đăng ký bằng gói khác trong ứng dụng Android. Đọc thêm [tại đây](https://android.adapty.io/adapty/com.adapty.models/-adapty-subscription-update-parameters/). ::: <img src="/assets/shared/img/ff-subupdate.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 8. Nhấp **Confirm**. 9. Trong **Action Output Variable Name**, tạo một biến mới và đặt tên là `makePurchaseResult` – biến này sẽ được dùng sau để xác nhận mua hàng thành công. <img src="/assets/shared/img/ff-makepurchaseresult.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 3.2. Kiểm tra xem mua hàng có thành công không \{#step-32-check-if-the-purchase-was-successful\} Bây giờ, hãy thiết lập kiểm tra xem giao dịch mua có được thực hiện thành công không. 1. Nhấp **+** và nhấp **Add Conditional**. 2. Trong **Set Condition for Action**, chọn biến `makePurchaseResult`. 3. Trong cửa sổ **Set Variable**, điền vào các trường như sau: - **Available Options**: Has Field - **Select Field**: profile <img src="/assets/shared/img/ff-makepurchaseresult-conditional.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấp **Confirm**. ## Bước 3.3. Mở nội dung trả phí \{#step-33-open-paid-content\} Nếu mua hàng thành công, bạn có thể mở khóa nội dung trả phí. Cách thiết lập như sau: 1. Nhấp **+** dưới nhãn **TRUE** và nhấp **Add Action**. 2. Trong trường **Define Action**, tìm kiếm và chọn trang bạn muốn mở từ danh sách **Navigate To**. Trong ví dụ này, trang đó là **Questions**. <img src="/assets/shared/img/ff-questions.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 3.4. Hiển thị thông báo lỗi nếu mua hàng thất bại \{#step-34-show-error-message-if-purchase-failed\} Nếu mua hàng thất bại, hãy hiển thị thông báo cho người dùng. 1. Thêm action **Informational Dialog** vào nhãn **FALSE**. 2. Trong trường **Title**, nhập văn bản bạn muốn dùng làm tiêu đề hộp thoại, chẳng hạn **Purchase Failed**. <img src="/assets/shared/img/ff-purchase-fail.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Nhấp **Value** trong hộp **Message**. Trong cửa sổ **Set from Variable**, tìm kiếm `makePurchaseResult` và chọn nó. Điền vào các trường như sau: - **Available Options**: Data Structure Field - **Select Field**: error - **Available Options**: Data Structure Field - **Select Field**: errorMessage <img src="/assets/shared/img/ff-fail-message.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Nhấp **Confirm**. 5. Thêm action **Terminate** vào flow **FALSE**. <img src="/assets/shared/img/ff-terminate-purchase.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Cuối cùng, nhấp **Close** ở góc trên bên phải. Chúc mừng! Người dùng của bạn giờ đây có thể mua sản phẩm. Để hoàn thiện hơn, hãy [thiết lập kiểm tra quyền truy cập nội dung trả phí](ff-check-subscription-status) ở các nơi khác để quyết định hiển thị nội dung trả phí hay paywall cho họ. --- # File: ff-check-subscription-status --- --- title: "Bước 4. Kiểm tra quyền truy cập nội dung trả phí" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký bằng feature flags của Adapty để phân khúc người dùng tốt hơn." --- Khi xác định xem người dùng có quyền truy cập nội dung trả phí cụ thể hay không, bạn cần kiểm tra mức độ truy cập của họ. Điều này có nghĩa là kiểm tra xem người dùng có ít nhất một mức độ truy cập và mức đó có phải là mức cần thiết hay không. Bạn có thể làm điều này bằng cách kiểm tra hồ sơ người dùng, trong đó chứa tất cả các mức độ truy cập hiện có. Bây giờ, hãy cho phép người dùng mua sản phẩm của bạn: 1. Double-click vào nút sẽ hiển thị nội dung trả phí và mở phần **Actions** ở khung bên phải nếu chưa mở. 2. Mở **Action Flow Editor**. <img src="/assets/shared/img/ff-open-paid-content.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Trong cửa sổ **Select Action Trigger**, chọn **On Tap**. 4. Trong cửa sổ **No Actions Created**, nhấp vào nút **Add Conditional Action**. 5. Nhấp vào **UNSET** để đặt các đối số hành động và chọn biến `currentProfile`. Đây là biến Adapty chứa dữ liệu về hồ sơ người dùng hiện tại. <img src="/assets/shared/img/ff-currentprofile.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '300px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 6. Điền vào các trường như sau: - **Available Options**: Data Structure Field - **Select Field**: accessLevels - **Available Options**: Filter List Items - **Filter Conditions**: 1. Chọn **Conditions -> Single Condition** và nhấp vào **UNSET**. 2. Trong trường **First value**, chọn **Item in list** làm **Source** và điền vào các trường như sau: - **Available Options**: Data Structure Field - **Select Field**: accessLevelIdentifier 3. Đặt toán tử lọc thành **Equal to**. 4. Nhấp vào **UNSET** bên cạnh **Second value** và trong trường **Value**, nhập ID mức độ truy cập của bạn; trong ví dụ của chúng tôi, chúng tôi dùng `premium`. <img src="/assets/shared/img/ff-filter.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '500px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Nhấp **Confirm** và tiếp tục điền vào các trường khác bên dưới. - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structure Field - **Select Field**: accessLevel - **Available Options**: Data Structure Field - **Select Field**: isActive 7. Nhấp **Confirm**. Bây giờ, hãy thêm các hành động cho những gì xảy ra tiếp theo — nếu người dùng có gói đăng ký phù hợp hay không. Đưa họ đến trang dành cho người đăng ký premium hoặc mở trang paywall để họ có thể mua quyền truy cập. --- # File: ff-resources --- --- title: "Các action và kiểu dữ liệu của plugin Adapty FlutterFlow" description: "Truy cập tài nguyên cờ tính năng của Adapty để tối ưu hóa các tính năng dựa trên gói đăng ký." --- ## Hành động tùy chỉnh \{#custom-actions\} Dưới đây là các phương thức Adapty được tích hợp vào FlutterFlow thông qua plugin Adapty. Chúng có thể được sử dụng như các hành động tùy chỉnh trong FlutterFlow. | Custom Action | Mô tả | Tham số hành động | Kiểu dữ liệu Adapty - Biến đầu ra | |---|----|--------|----| | activate | Khởi tạo Adapty SDK | Không có || | <p id="getPaywall">getPaywall</p> | Truy xuất một paywall. Hành động này không trả về sản phẩm của paywall. Sử dụng hành động `getPaywallProducts` để lấy các sản phẩm thực tế | <ul><li>[Placement_ID](placements)</li><li>[Locale](localizations-and-locale-codes)</li></ul> | [AdaptyGetPaywallResult](ff-resources#adaptygetpaywallresult)| | <p id="getPaywallProducts">getPaywallProducts</p> | Trả về danh sách các sản phẩm thực tế của paywall | [AdaptyPaywall](ff-resources#adaptypaywall) | [AdaptyGetProductsResult](ff-resources#adaptygetproductsresult) | | <p id="getproductsintroductoryoffereligibility">getProductsIntroductoryOfferEligibility</p> | Kiểm tra xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu cho gói đăng ký iOS hay không | [AdaptyPaywallProduct](product) | [AdaptyGetIntroEligibilitiesResult](ff-resources#adaptygetintroeligibilitiesresult) | | <p id="makePurchase">makePurchase</p> | Hoàn tất giao dịch mua và mở khóa nội dung. Nếu paywall có ưu đãi, Adapty tự động áp dụng khi thanh toán | <ul><li> **product**: đối tượng AdaptyPaywallProduct được lấy từ paywall.</li><li> **subscriptionUpdateParams**: đối tượng [`AdaptySubscriptionUpdateParameters`](ff-resources#adaptysubscriptionupdateparameters) dùng để nâng cấp hoặc hạ cấp gói đăng ký (dùng cho Android).</li><li>**isOfferPersonalized**: Chỉ định liệu ưu đãi có được cá nhân hóa cho người mua hay không (dùng cho Android).</li></ul> | [AdaptyMakePurchaseResult](ff-resources#adaptymakepurchaseresult) | | <p id="getprofile">getProfile</p> | <p>Truy xuất hồ sơ người dùng hiện tại của ứng dụng. Cho phép bạn thiết lập mức độ truy cập và các thông số khác</p><p>Nếu thất bại (ví dụ: do không có kết nối internet), dữ liệu đã lưu trong bộ nhớ đệm sẽ được trả về. Adapty thường xuyên cập nhật bộ nhớ đệm hồ sơ để đảm bảo thông tin luôn được cập nhật nhất có thể</p> | Không có | [AdaptyGetProfileResult](ff-resources#adaptygetprofileresult) | | updateProfile | Thay đổi các thuộc tính tùy chọn của hồ sơ người dùng hiện tại như email, số điện thoại, v.v. Bạn có thể dùng các thuộc tính này sau để tạo [phân khúc](segments) người dùng hoặc xem trong CRM | ID và các thông số cần cập nhật cho [AdaptyProfile](ff-resources#adaptyprofile) | [AdaptyError](ff-resources#adaptyerror) (Tùy chọn) | | restorePurchases | Khôi phục tất cả các giao dịch mua mà người dùng đã thực hiện | Không có | [AdaptyGetProfileResult](ff-resources#adaptygetprofileresult) | | logShowPaywall | Ghi lại khi một paywall cụ thể được hiển thị cho người dùng | [AdaptyPaywall](ff-resources#adaptypaywall) | [AdaptyError](ff-resources#adaptyerror) (Tùy chọn) | | identify | Xác định người dùng bằng `customerUserId` của hệ thống của bạn | customerUserId | [AdaptyError](ff-resources#adaptyerror) (Tùy chọn) | | logout | Đăng xuất người dùng hiện tại khỏi ứng dụng của bạn | Không có | [AdaptyError](ff-resources#adaptyerror) (Tùy chọn)| | presentCodeRedemptionSheet | Hiển thị một sheet cho phép người dùng đổi mã (chỉ dành cho iOS) | Không có | Không có | ## Các kiểu dữ liệu \{#data-types\} Các kiểu dữ liệu của Adapty (tập hợp các giá trị dữ liệu) được cung cấp đến FlutterFlow thông qua plugin Adapty. ### AdaptyAccessLevel Thông tin về [mức độ truy cập](access-level) của người dùng. | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | activatedAt | DateTime | Thời điểm mức độ truy cập này được kích hoạt | | activeIntroductoryOfferType | String | Loại ưu đãi giới thiệu đang áp dụng. Nếu được đặt, nghĩa là một ưu đãi đã được áp dụng trong chu kỳ đăng ký này | | activePromotionalOfferId | String | ID của ưu đãi đang áp dụng (mua từ iOS) | | activePromotionalOfferType | String | Loại ưu đãi đang áp dụng (mua từ iOS). Nếu được đặt, nghĩa là một ưu đãi đã được áp dụng trong chu kỳ đăng ký này | | billingIssueDetectedAt | DateTime | Thời điểm phát hiện sự cố thanh toán. Gói đăng ký vẫn có thể đang hoạt động. Đặt về null nếu thanh toán được xử lý thành công | | cancellationReason | String | Lý do hủy gói đăng ký | | expiresAt | DateTime | Thời điểm hết hạn của mức độ truy cập (có thể đã qua hoặc không được đặt đối với quyền truy cập trọn đời) | | id | String | Định danh của mức độ truy cập | | isActive | Boolean | True nếu mức độ truy cập này đang hoạt động. Nhìn chung, bạn có thể kiểm tra thuộc tính này để xác định xem người dùng có quyền truy cập vào các tính năng premium hay không | | isInGracePeriod | Boolean | True nếu gói đăng ký tự động gia hạn này đang trong [thời gian ân hạn](https://developer.apple.com/help/app-store-connect/manage-subscriptions/enable-billing-grace-period-for-auto-renewable-subscriptions) | | isLifetime | Boolean | True nếu mức độ truy cập này có hiệu lực trọn đời (không có ngày hết hạn) | | isRefund | Boolean | True nếu giao dịch mua này đã được hoàn tiền | | offerId | String | ID của ưu đãi đang áp dụng (mua từ Android) | | renewedAt | DateTime | Thời điểm mức độ truy cập được gia hạn lần cuối | | startsAt | DateTime | Thời điểm bắt đầu của mức độ truy cập này (có thể là trong tương lai) | | store | String | Cửa hàng nơi thực hiện giao dịch mua | | unsubscribedAt | DateTime | Thời điểm tắt tự động gia hạn cho gói đăng ký. Gói đăng ký vẫn có thể đang hoạt động. Nếu không được đặt, người dùng đã kích hoạt lại gói đăng ký | | vendorProductId | String | ID sản phẩm từ cửa hàng đã mở khóa mức độ truy cập này | | willRenew | Boolean | True nếu gói đăng ký tự động gia hạn này được đặt để gia hạn | ### AdaptyAccessLevelIdentifiers Struct này được dùng để thay thế cặp key-value cho `Map<String, AdaptyAccessLevel` [AdaptyAccessLevel](ff-resources#adaptyaccesslevel). | Tên trường | Kiểu dữ liệu | Mô tả | |------------|------|-------------| | accessLevelIdentifier | String | ID của mức độ truy cập | | accessLevel | Data ([AdaptyAccessLevel](ff-resources#adaptyaccesslevel)) | [AdaptyAccessLevel](ff-resources#adaptyaccesslevel) tương ứng | ### AdaptyCustomDoubleAttribute Thông tin về các thuộc tính double tùy chỉnh được định nghĩa cho [người dùng](ff-resources#adaptyprofile). | Tên trường | Kiểu | Mô tả | |------------|------|-------| | key | String | ID của thuộc tính double tùy chỉnh | | value | Double | Giá trị của thuộc tính double tùy chỉnh | ### AdaptyCustomStringAttribute Thông tin về các thuộc tính chuỗi tùy chỉnh được định nghĩa cho [người dùng](ff-resources#adaptyprofile). | Tên trường | Kiểu | Mô tả | |------------|------|-------| | key | String | ID của thuộc tính chuỗi tùy chỉnh | | value | String | Giá trị của thuộc tính chuỗi tùy chỉnh | ### AdaptyError Chứa thông tin chi tiết về lỗi. Để xem danh sách đầy đủ các mã lỗi, hãy tham khảo [React Native, Flutter, Unity - Xử lý lỗi](error-handling-on-flutter-react-native-unity). | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | errorMessage | String | Mô tả lỗi dễ đọc cho người dùng | | errorCode | Integer | Mã số xác định lỗi | ### AdaptyGetIntroEligibilitiesResult Chứa kết quả của custom action `getProductsIntroductoryOfferEligibility`. | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | value | List < Data ([AdaptyProductIntroEligibility](ff-resources#adaptyproductintroeligibility)) > | Danh sách tình trạng đủ điều kiện nhận ưu đãi giới thiệu của người dùng | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Chứa thông tin chi tiết về lỗi thông qua [`AdaptyError`](ff-resources#adaptyerror) | ### AdaptyGetPaywallResult Chứa kết quả của custom action `getPaywall`. | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | value | Data ([AdaptyPaywall](ff-resources#adaptypaywall)) | Chứa danh sách các đối tượng [AdaptyPaywall](ff-resources#adaptypaywall) | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Chứa thông tin lỗi thông qua [AdaptyError](ff-resources#adaptyerror) | ### AdaptyGetProductsResult Chứa kết quả của custom action `getPaywallProducts`. | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | value | List < Data ([AdaptyPaywallProduct](product)) > | Chứa danh sách các [AdaptyPaywallProduct](product) | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Chứa thông tin lỗi thông qua [AdaptyError](ff-resources#adaptyerror) | ### AdaptyGetProfileResult Chứa kết quả của custom action `getProfile`. | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | value | Data ([AdaptyProfile](ff-resources#adaptyprofile)) | Chứa hồ sơ người dùng dưới dạng [AdaptyProfile](ff-resources#adaptyprofile) | | error | Data (AdaptyError) | Chứa thông tin lỗi qua [AdaptyError](ff-resources#adaptyerror) | ### AdaptyMakePurchaseResult Chứa kết quả của custom action `makePurchase`. | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | value | Data ([AdaptyProfile](ff-resources#adaptyprofile)) | Chứa hồ sơ người dùng dưới dạng [AdaptyProfile](ff-resources#adaptyprofile) | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Chứa thông tin lỗi qua [AdaptyError](ff-resources#adaptyerror) | ### AdaptyNonSubscription Thông tin về các sản phẩm mua không phải gói đăng ký. Đây có thể là các sản phẩm mua một lần (consumable), các vật phẩm mở khóa (như mở khóa bản đồ mới trong game), v.v. | Tên trường | Kiểu | Mô tả | |--------------------------|----------|-------------| | isConsumable | Boolean | Cho biết liệu sản phẩm có phải là consumable hay không | | isOneTime | Boolean | Cho biết liệu sản phẩm có phải là sản phẩm mua một lần hay không (ví dụ: nếu là true, giao dịch mua chỉ được xử lý một lần) | | isRefund | Boolean | Cho biết liệu sản phẩm đã được hoàn tiền hay chưa | | isSandbox | Boolean | Cho biết liệu sản phẩm có được mua trong môi trường sandbox hay không | | purchasedAt | DateTime | Thời điểm sản phẩm được mua | | purchaseId | String | ID của giao dịch mua trong Adapty. Có thể dùng để theo dõi các sản phẩm mua một lần | | store | String | Cửa hàng nơi sản phẩm được mua (ví dụ: App Store, Google Play) | | vendorProductId | String | ID của sản phẩm trong hệ thống của nhà cung cấp | | vendorTransactionId | String | ID giao dịch trong hệ thống của nhà cung cấp | ### AdaptyPaywall Thông tin về một [paywall](paywalls). | Tên trường | Kiểu | Mô tả | |----------------------|----------|-------------| | abTestName | String | Tên của A/B test cha | | hasViewConfiguration | Boolean | Cho biết paywall có cấu hình giao diện hay không | | locale | String | ID ngôn ngữ của paywall | | name | String | Tên paywall | | placement.id | String | ID của placement cha | | remoteConfigString | String | Một dictionary tùy chỉnh từ Adapty Dashboard được liên kết với paywall này | | placement.revision | Integer | Phiên bản/revision hiện tại của paywall. Mỗi thay đổi sẽ tạo ra một revision mới | | variationId | String | ID biến thể dùng để gán các giao dịch mua cho paywall này | | vendorProductIds | String | Mảng các ID sản phẩm liên quan đến paywall | ### AdaptyPaywallProduct Thông tin về [sản phẩm](product). | Tên trường | Kiểu | Mô tả | | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | vendorProductId | String | ID của sản phẩm từ cửa hàng ứng dụng | | localizedDescription | String | Mô tả sản phẩm bằng ngôn ngữ của người dùng | | localizedTitle | String | Tên sản phẩm bằng ngôn ngữ của người dùng | | regionCode | String | Mã vùng của locale dùng để định dạng giá sản phẩm (dùng cho iOS) | | isFamilyShareable | Boolean | Giá trị Boolean cho biết sản phẩm có được chia sẻ gia đình trong App Store Connect hay không. Luôn là FALSE với iOS dưới phiên bản 14.0 và macOS dưới phiên bản 11.0 (dùng cho iOS) | | paywallVariationId | String | ID của biến thể, dùng để gán các giao dịch mua cho paywall này | | paywallABTestName | String | Tên A/B test cha | | paywallName | String | Tên paywall cha | | price | Data ([AdaptyPriceData](#adaptyprice)) | Giá của sản phẩm | | subscriptionDetails | Data ([AdaptySubscriptionDetails](#adaptysubscriptiondetails)) | Thông tin về gói đăng ký | ### AdaptyPrice Thông tin về giá sản phẩm. | Tên trường | Kiểu | Mô tả | | --------------- | ------ | ------------------------------------------ | | amount | Double | Giá trị số của mức giá | | currencyCode | String | Mã tiền tệ của mức giá | | currencySymbol | String | Ký hiệu tiền tệ được sử dụng | | localizedString | String | Mức giá hiển thị theo ngôn ngữ của người dùng | ### AdaptyProductIntroEligibility Xác định xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu cho một gói đăng ký iOS hay không. | Tên trường | Kiểu dữ liệu | Mô tả | | --------------- | ----------------------------------------------------------- | ------------------------------------------------------------ | | vendorProductId | String | ID của sản phẩm từ cửa hàng ứng dụng | | eligibility | [AdaptyEligibilityEnum](ff-resources#adaptyeligibilityenum) | Xác định xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu cho gói đăng ký iOS hay không | ### AdaptyProductNonsubscriptions Chi tiết về sản phẩm mua một lần đang hoạt động được liên kết với sản phẩm này. | Tên trường | Kiểu | Mô tả | | ---------------- | ----------------------------------------------------------- | ------------------------------------------------------------ | | productId | String | ID của sản phẩm từ cửa hàng ứng dụng | | nonsubscriptions | [AdaptyNonSubscription](ff-resources#adaptynonsubscription) | Thông tin về các sản phẩm mua một lần. Đây có thể là các sản phẩm consumable (mua một lần), hoặc các mục mở khóa (ví dụ: mở khóa bản đồ mới trong game), v.v. | ### AdaptyProductSubscriptions Chi tiết gói đăng ký đang hoạt động gắn với sản phẩm này. | Field Name | Type | Description | | ------------ | ----------------------------------------------------- | ---------------------------------------- | | productId | String | ID của sản phẩm từ cửa hàng ứng dụng | | subscription | [AdaptySubscription](ff-resources#adaptysubscription) | Thông tin về các giao dịch mua gói đăng ký | ### AdaptyProfile Thông tin về hồ sơ người dùng | Field Name | Type | Description | | ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | accessLevels | List < Data ([AdaptyAccessLevelIdentifiers](ff-resources#adaptyaccesslevelidentifiers)) > | Danh sách tất cả các mức độ truy cập thuộc về người dùng | | profileId | String | ID của hồ sơ người dùng | | customerUserId | String | ID của người dùng trong hệ thống của nhà cung cấp | | subscriptions | List < Data ([MapKeySubscriptions](#mapkeysubscriptions)) > | Danh sách tất cả các gói đăng ký mà người dùng đã mua | | nonSubscriptions | List < Data ([MapKeyNonSubscriptions](#mapkeynonsubscriptions)) > | Danh sách tất cả các sản phẩm mua một lần mà người dùng đã mua | ### AdaptyProfileParameters Thông tin về người dùng. | Tên trường | Kiểu | Mô tả | | ----------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | firstName | String | Tên của người dùng | | lastName | String | Họ của người dùng | | gender | [AdaptyGenderEnum](#adaptygenderenum) | Giới tính của người dùng | | birthday | String | Ngày sinh của người dùng | | email | String | Email của người dùng | | phoneNumber | String | Số điện thoại của người dùng | | facebookAnonymousId | String | ID của người dùng trong [tích hợp Facebook Ads](facebook-ads) | | amplitudeUserId | String | ID của người dùng trong [tích hợp Amplitude](amplitude) | | amplitudeDeviceId | String | ID thiết bị của người dùng trong [tích hợp Amplitude](amplitude) | | mixpanelUserId | String | ID của người dùng trong [tích hợp Mixpanel](mixpanel) | | appmetricaProfileId | String | ID của người dùng trong [tích hợp AppMetrica](appmetrica) | | appmetricaDeviceId | String | ID thiết bị của người dùng trong [tích hợp AppMetrica](appmetrica) | | oneSignalPlayerId | String | ID của người dùng trong [tích hợp OneSignal](onesignal) | | pushwooshHWID | String | ID thiết bị của người dùng trong [tích hợp Pushwoosh](pushwoosh) | | firebaseAppInstanceId | String | ID của người dùng trong [tích hợp Firebase](firebase-and-google-analytics) | | airbridgeDeviceId | String | ID thiết bị của người dùng trong [tích hợp Airbridge](airbridge) | | appTrackingTransparencyStatus | AdaptyATTStatus | Trạng thái quyền truy cập IDFA (dùng cho iOS) | | analyticsDisabled | Boolean | Xác định xem [analytics bên ngoài có bị tắt cho người dùng này không](analytics-integration#disabling-external-analytics-for-a-specific-customer) | | customStringAttributes | List < Data ([AdaptyCustomStringAttribute](ff-resources#adaptycustomstringattribute)) > | Danh sách các thuộc tính string tùy chỉnh của người dùng | | customDoubleAttributes | List < Data ([AdaptyCustomDoubleAttribute](ff-resources#adaptycustomdoubleattribute)) > | Danh sách các thuộc tính double tùy chỉnh của người dùng | ### AdaptySubscription Thông tin về gói đăng ký hiện tại của người dùng. | Tên trường | Kiểu | Mô tả | | --------------------------- | -------- | ------------------------------------------------------------ | | activatedAt | DateTime | Thời điểm gói đăng ký này được kích hoạt | | activeIntroductoryOfferType | String | Loại ưu đãi giới thiệu đang áp dụng. Nếu được thiết lập, có nghĩa là một ưu đãi đã được áp dụng trong kỳ đăng ký này | | activePromotionalOfferId | String | ID của ưu đãi đang áp dụng (dùng cho iOS) | | activePromotionalOfferType | String | Loại ưu đãi đang áp dụng (dùng cho iOS). Nếu được thiết lập, có nghĩa là một ưu đãi đã được áp dụng trong kỳ đăng ký này | | cancellationReason | String | Lý do gói đăng ký bị hủy | | expiresAt | DateTime | Thời điểm gói đăng ký hết hạn | | renewedAt | DateTime | Thời điểm gói đăng ký được gia hạn gần nhất | | unsubscribedAt | DateTime | Thời điểm tự động gia hạn bị tắt đối với gói đăng ký. Gói đăng ký vẫn có thể còn hiệu lực. Nếu không được thiết lập, người dùng đã kích hoạt lại gói đăng ký | | billingIssueDetectedAt | DateTime | Thời điểm phát hiện sự cố thanh toán. Gói đăng ký vẫn có thể còn hiệu lực. Được đặt thành null nếu thanh toán được xử lý thành công | | isActive | Boolean | True nếu gói đăng ký này đang hoạt động. Nhìn chung, bạn có thể kiểm tra thuộc tính này để xác định xem người dùng có quyền truy cập vào các tính năng cao cấp hay không | | isInGracePeriod | Boolean | True nếu gói đăng ký tự động gia hạn này đang trong [thời gian ân hạn](https://developer.apple.com/help/app-store-connect/manage-subscriptions/enable-billing-grace-period-for-auto-renewable-subscriptions) | | isLifetime | Boolean | True nếu gói đăng ký này có hiệu lực trọn đời (không có ngày hết hạn) | | isRefund | Boolean | True nếu lần mua này đã được hoàn tiền | | isSandbox | Boolean | Cho biết sản phẩm có được mua trong môi trường sandbox hay không | | offerId | String | ID của ưu đãi đang áp dụng (dùng cho Android) | | startsAt | DateTime | Thời điểm bắt đầu của mức độ truy cập này (có thể là trong tương lai) | | store | String | Cửa hàng nơi sản phẩm được mua (ví dụ: App Store, Google Play) | | vendorOriginalTransactionId | String | ID của gói đăng ký ban đầu trong hệ thống của nhà cung cấp | | vendorProductId | String | ID của sản phẩm trong hệ thống của nhà cung cấp | | vendorTransactionId | String | ID giao dịch trong hệ thống của nhà cung cấp | | willRenew | Boolean | True nếu gói đăng ký tự động gia hạn này được thiết lập để gia hạn | ### AdaptySubscriptionDetails Sơ đồ của đối tượng Subscription là một phần của [AdaptyPaywallProduct](product). | Tên trường | Loại | Mô tả | | ----------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | androidBasePlanId | String | [ID gói cơ bản](https://support.google.com/googleplay/android-developer/answer/12154973) trong Google Play Store hoặc [ID giá](https://docs.stripe.com/products-prices/how-products-and-prices-work#use-products-and-prices) trong Stripe. | | androidIntroductoryOfferEligibility | [AdaptyEligibilityEnum](ff-resources#adaptyeligibilityenum) | Xác định người dùng có đủ điều kiện nhận ưu đãi giới thiệu cho gói đăng ký iOS hay không | | androidOfferId | String | ID của ưu đãi đang hoạt động (dùng cho Android) | | androidOfferTags | List < String > | Danh sách [thẻ tùy chỉnh](https://developers.google.com/android-publisher/api-ref/rest/v3/OfferTag) được chỉ định cho các gói cơ bản và ưu đãi gói đăng ký. | | introductoryOffer | List < Data ([AdaptySubscriptionPhase](ff-resources#adaptysubscriptionphase)) > | ID của ưu đãi giới thiệu (dùng cho iOS) | | localizedSubscriptionPeriod | String | Chu kỳ gói đăng ký theo ngôn ngữ của người dùng | | promotionalOffer | Data ([AdaptySubscriptionPhase](ff-resources#adaptysubscriptionphase)) | Chi tiết ưu đãi (dùng cho iOS) | | promotionalOfferEligibility | Boolean | Xác định người dùng có đủ điều kiện nhận ưu đãi cho gói đăng ký iOS hay không | | promotionalOfferId | String | ID của ưu đãi (dùng cho iOS) | | renewalType | [AdaptyRenewalTypeEnum](#adaptyrenewaltypeenum) | Xác định gói đăng ký có tự động gia hạn hay không thông qua [AdaptyRenewalTypeEnum](ff-resources#adaptyrenewaltypeenum) | | subscriptionGroupIdentifier | String | ID của nhóm sản phẩm mà sản phẩm thuộc về (dùng cho iOS) | | subscriptionPeriod | Data ([AdaptySubscriptionPeriod](#adaptysubscriptionperiod)) | Thời hạn của gói đăng ký | ### AdaptySubscriptionPeriod Thời hạn của gói đăng ký. | Tên trường | Loại | Mô tả | | ------------- | --------------------------------------------- | ----------------------------------------------------------------------- | | numberOfUnits | Integer | Số ngày/tuần/tháng/năm mà gói đăng ký có hiệu lực. | | unit | [AdaptyPeriodUnitEnum](#adaptyperiodunitenum) | Đơn vị đo lường của chu kỳ: ngày, tuần, tháng, năm. | ### AdaptySubscriptionPhase Đại diện cho một giai đoạn của gói đăng ký, chẳng hạn như thời gian dùng thử miễn phí hoặc thời kỳ ưu đãi giới thiệu. | Tên trường | Kiểu | Mô tả | | --------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | identifier | String | ID của giai đoạn | | localizedNumberOfPeriods | String | Độ dài của giai đoạn. Ví dụ, một ưu đãi 6 tháng sẽ hiển thị là `6 months` theo ngôn ngữ của người dùng. | | localizedSubscriptionPeriod | String | Thời hạn gói đăng ký theo ngôn ngữ của người dùng, ví dụ `3 months`. | | numberOfPeriods | Integer | Số chu kỳ gói đăng ký trong giai đoạn này. Ví dụ, một ưu đãi 6 tháng sẽ có hai chu kỳ 3 tháng. | | paymentMode | [AdaptyPaymentModeEnum](#adaptypaymentmodeenum) | Mô hình thanh toán được sử dụng cho giai đoạn này. | | price | Data ([AdaptyPrice](#adaptyprice)) | Giá của giai đoạn này. | | subscriptionPeriod | Data ([AdaptySubscriptionPeriod](#adaptysubscriptionperiod)) | Chu kỳ gói đăng ký mà giai đoạn này dựa trên. | ### AdaptySubscriptionUpdateParameters (*Chỉ dành cho Android*) Tham số để thay thế một gói đăng ký bằng gói khác. | Tên trường | Kiểu dữ liệu | Mô tả | | ---------- | ------------------------------------------------------------ | ---------- | | oldSubVendorProductId | String | ID của gói đăng ký hiện tại trên Play Store mà bạn muốn thay thế. | | replacementMode | [AdaptySubscriptionUpdateReplacementMode](ff-resources#adaptysubscriptionupdatereplacementmode) | Enum tương ứng với các giá trị của [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode). | ### MapKeyNonSubscriptions Thay thế cho một dictionary dành cho [AdaptyNonSubscription](ff-resources#adaptynonsubscription). | Tên trường | Kiểu | | ---------- | ------------------------------------------------------------ | | key | String | | value | List < Data ([AdaptyNonSubscription](ff-resources#adaptynonsubscription)) > | ### MapKeySubscriptions Thay thế cho dictionary của [AdaptySubscription](ff-resources#adaptysubscription). | Tên trường | Kiểu dữ liệu | | ---------- | ------------------------------------------------------------ | | key | String | | value | List < Data ([AdaptySubscription](ff-resources#adaptysubscription)) > | ## Enums \{#enums\} Adapty enums (các biến là tập hợp các hằng số được định nghĩa trước) được cung cấp cho FlutterFlow thông qua plugin Adapty. ### AdaptyEligibilityEnum Xác định xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu cho gói đăng ký iOS hay không. | Tên trường | Mô tả | |--------------------------|-------------| | eligible | Người dùng đủ điều kiện nhận ưu đãi giới thiệu, bạn có thể hiển thị thông tin này trong UI | | ineligible | Người dùng không đủ điều kiện nhận bất kỳ ưu đãi nào, bạn không nên hiển thị nó trong UI | | notApplicable | Sản phẩm này chưa được cấu hình để có ưu đãi | ### AdaptyGenderEnum Xác định giới tính của người dùng. | Tên trường | Mô tả | | ---------- | ---------------------------------------------- | | none | Giới tính chưa được thiết lập | | female | Giới tính của người dùng là nữ | | male | Giới tính của người dùng là nam | | Other | Người dùng đã xác định giới tính của họ là "khác" | ### AdaptyPaymentModeEnum Xác định mô hình thanh toán. | Tên trường | Mô tả | | ---------- | ------------------------------------------------------------ | | payAsYouGo | Mô hình thanh toán theo mức sử dụng thực tế, thay vì trả một khoản phí cố định trước | | payUpFront | Mô hình thanh toán trước khi nhận sản phẩm/dịch vụ | | freeTrial | Người dùng đang trong thời gian dùng thử miễn phí | | unknown | Mô hình thanh toán chưa được xác định | ### AdaptyPeriodUnitEnum \{#adaptyperidunitenum\} Xác định đơn vị dùng để đo thời gian các chu kỳ. | Tên trường | Mô tả | | ---------- | -------------- | | day | Theo ngày | | week | Theo tuần | | month | Theo tháng | | year | Theo năm | | unknown | Không xác định | ### AdaptyRenewalTypeEnum Định nghĩa xem gói đăng ký có tự động gia hạn hay không. | Field Name | Description | | ------------- | -------------------------------------------------------- | | prepaid | Gói đăng ký trả trước và không tự động gia hạn. | | autorenewable | Gói đăng ký tự động gia hạn. | ### AdaptySubscriptionUpdateReplacementMode Xác định chế độ cập nhật gói đăng ký cho Android. | Field Name | Description | | ------------- | --------------------------------------------------- | | withTimeProration | (mặc định) Gói mới có hiệu lực ngay lập tức, thời gian còn lại sẽ được tính theo tỷ lệ và cộng vào tài khoản người dùng. | | chargeProratedPrice | Gói mới có hiệu lực ngay lập tức và chu kỳ thanh toán vẫn giữ nguyên. Số tiền cho khoảng thời gian còn lại sẽ được tính phí. Tùy chọn này chỉ áp dụng cho việc nâng cấp gói đăng ký. | | withoutProration | Gói mới có hiệu lực ngay lập tức, và giá mới sẽ được tính vào lần gia hạn tiếp theo. Chu kỳ thanh toán vẫn giữ nguyên. | | deferred | Giao dịch mới có hiệu lực ngay lập tức, nhưng gói mới sẽ chỉ áp dụng khi gói cũ hết hạn. | | chargeFullPrice | Gói mới có hiệu lực ngay lập tức và chu kỳ thanh toán vẫn giữ nguyên. Giá đầy đủ cho khoảng thời gian còn lại sẽ được tính phí. Tùy chọn này chỉ áp dụng cho việc nâng cấp gói đăng ký. | ### Trạng thái ứng dụng \{#app-states\} Biến trạng thái ứng dụng là các biến đặc biệt lưu trữ trạng thái hiện tại của ứng dụng. Chúng có thể được truy cập và chỉnh sửa ở bất kỳ đâu trong toàn bộ ứng dụng, trên mọi trang và component. Loại biến này hữu ích khi bạn cần chia sẻ dữ liệu giữa các phần khác nhau của ứng dụng, chẳng hạn như tùy chọn người dùng hay token xác thực. | Tên trường | Kiểu dữ liệu | Lưu trữ | Mô tả | | -------------- | -------------------------------------------------- | --------- | ------------------------------------------------------------ | | currentProfile | Data ([AdaptyProfile](ff-resources#adaptyprofile)) | False | Biến chứa thông tin về hồ sơ người dùng hiện tại. Hãy giữ nó luôn được cập nhật. | --- # End of Documentation _Generated on: 2026-07-24T13:01:43.668Z_ _Successfully processed: 277/277 files_ # UNITY - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.680Z Total files: 41 --- # File: sdk-installation-unity --- --- title: "Cài đặt & cấu hình Unity SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên Unity cho ứng dụng dựa trên gói đăng ký." --- SDK Adapty bao gồm hai module chính để tích hợp liền mạch vào ứng dụng Unity của bạn: - **Core Adapty**: SDK cốt lõi này bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder) — công cụ no-code thân thiện giúp tạo paywall đa nền tảng dễ dàng. :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Assets) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Yêu cầu \{#requirements\} Adapty SDK hỗ trợ iOS 13.0+, nhưng yêu cầu iOS 15.0+ để hoạt động với các paywall được tạo trong Paywall Builder. :::info Adapty tương thích với Google Play Billing Library lên đến phiên bản 8.x. Theo mặc định, Adapty sử dụng Google Play Billing Library v7.0.0. Để dùng phiên bản mới hơn, hãy [ghi đè dependency Billing](https://developer.android.com/google/play/billing/integrate#dependency) trong cấu hình build Android của bạn. ::: :::info Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ::: ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Unity.svg?style=flat&logo=unity)](https://github.com/adaptyteam/AdaptySDK-Unity/releases) Chọn phương thức cài đặt bạn muốn: <Tabs groupId="unity-install-method"> <TabItem value="git-url" label="Git URL"> Cài đặt Adapty SDK qua Unity Package Manager bằng Git URL: 1. Trong Unity, mở **Window → Package Manager**. 2. Nhấn **+** ở góc trên bên trái, rồi chọn **Add package from git URL...**. 3. Nhập URL sau và nhấn **Add**: ``` https://github.com/adaptyteam/AdaptySDK-Unity.git?path=Packages/com.adapty.unity-sdk#upm ``` Để biết thêm chi tiết, xem hướng dẫn của Unity về [cài đặt gói UPM từ Git URL](https://docs.unity3d.com/Manual/upm-ui-giturl.html). </TabItem> <TabItem value="unity-package" label="Unity package" default> Tải [`adapty-unity-plugin-*.unitypackage`](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Releases) từ GitHub và import vào dự án của bạn. <img src="/assets/shared/img/456bd98-adapty-unity-plugin.webp" style={{ border: 'none', /* border width and color */ width: '400px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </TabItem> </Tabs> Sau khi cài đặt SDK, hãy thực hiện các bước sau: 1. Cài đặt [plugin External Dependency Manager (EDM)](https://github.com/googlesamples/unity-jar-resolver#getting-started). Adapty SDK sử dụng plugin này để quản lý các phụ thuộc iOS Cocoapods và Android gradle. 2. Sau khi cài đặt EDM, bạn có thể cần gọi dependency manager: `Assets -> External Dependency Manager -> Android Resolver -> Force Resolve` và `Assets -> External Dependency Manager -> iOS Resolver -> Install Cocoapods` 3. Khi build dự án Unity cho iOS, bạn sẽ nhận được file `Unity-iPhone.xcworkspace`, file này phải được mở thay vì `Unity-iPhone.xcodeproj`, nếu không các dependency của Cocoapods sẽ không được sử dụng. ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Kích hoạt Adapty SDK trong code của ứng dụng. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: Để lấy **Public SDK Key**: 1. Truy cập Adapty Dashboard và điều hướng đến [**App settings → General**](https://app.adapty.io/settings/general). 2. Trong phần **Api keys**, sao chép **Public SDK Key** (KHÔNG phải Secret Key). 3. Thay thế `"YOUR_PUBLIC_SDK_KEY"` trong code. Hoặc lấy theo cách lập trình, sử dụng [Adapty CLI](developer-cli): ``` npm install -g adapty adapty auth login adapty apps list ``` Hoặc, trực tiếp: ``` npx adapty auth login adapty apps list ``` - Đảm bảo bạn sử dụng **Public SDK key** để khởi tạo Adapty, **Secret key** chỉ nên dùng cho [server-side API](getting-started-with-server-side-api). - **SDK keys** là duy nhất cho mỗi ứng dụng, vì vậy nếu bạn có nhiều ứng dụng, hãy đảm bảo chọn đúng key. ```csharp showLineNumbers title="C#" using UnityEngine; using AdaptySDK; public class AdaptyListener : MonoBehaviour, AdaptyEventListener { void Start() { DontDestroyOnLoad(this.gameObject); Adapty.SetEventListener(this); var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY"); Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); } public void OnLoadLatestProfile(AdaptyProfile profile) { } public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } public void OnInstallationDetailsFail(AdaptyError error) { } } ``` :::important Hãy chờ callback hoàn thành của `Activate` 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 Unity SDK](unity-sdk-call-order) để biết đầy đủ trình tự. ::: ## Thiết lập lắng nghe sự kiện \{#set-up-event-listening\} Tạo một script để lắng nghe các sự kiện Adapty. Đặt tên là `AdaptyListener` trong scene của bạn. Chúng tôi khuyến nghị sử dụng phương thức `DontDestroyOnLoad` cho object này để đảm bảo nó tồn tại suốt vòng đời của ứng dụng. <img src="/assets/shared/img/2ccd564-create_adapty_listener.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Adapty sử dụng namespace `AdaptySDK`. Ở đầu các file script sử dụng Adapty SDK, bạn có thể thêm: ```csharp showLineNumbers title="C#" using AdaptySDK; ``` Đăng ký nhận các sự kiện của Adapty: ```csharp showLineNumbers title="C#" using UnityEngine; using AdaptySDK; public class AdaptyListener : MonoBehaviour, AdaptyEventListener { public void OnLoadLatestProfile(AdaptyProfile profile) { // handle updated profile data } public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } public void OnInstallationDetailsFail(AdaptyError error) { } } ``` Chúng tôi khuyến nghị điều chỉnh Script Execution Order để đặt AdaptyListener trước Default Time. Điều này đảm bảo Adapty được khởi tạo sớm nhất có thể. <img src="/assets/shared/img/activate_unity.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bây giờ hãy thiết lập paywall trong ứng dụng của bạn: - Nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder), trước tiên hãy [kích hoạt module AdaptyUI](#activate-adaptyui-module-of-adapty-sdk) bên dưới, sau đó làm theo [hướng dẫn nhanh về Paywall Builder](unity-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, xem [hướng dẫn nhanh cho paywall tùy chỉnh](unity-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder) và đã cài đặt module AdaptyUI, bạn cần kích hoạt AdaptyUI. Bạn có thể kích hoạt nó trong quá trình cấu hình: ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetActivateUI(true); ``` ## 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 chuyện gì đang xảy ra. Có các mức độ log sau: | Level | Description | | ---------- | ------------------------------------------------------------ | | `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à nhiều thông báo thông tin khác | | `verbose` | Ghi lại mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như các lời gọi hàm, truy vấn API, v.v. | Bạn có thể thiết lập mức độ log trong ứng dụng khi cấu hình Adapty: ```csharp showLineNumbers title="C#" // 'verbose' is recommended for development and the first production release var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY"); builder.LogLevel = AdaptyLogLevel.Verbose; ``` Bạn cũng có thể thay đổi mức độ log trong khi ứng dụng đang chạy: ```csharp showLineNumbers title="C#" Adapty.SetLogLevel(AdaptyLogLevel.Verbose, (error) => { // handle result }); ``` ### 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 các chính sách bảo mật dữ liệu bổ sung để tuân thủ các quy định của cửa hàng hoặc quốc gia. #### Tắt tính năng thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `SetIPAddressCollectionDisabled` thành `true` để tắt tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư của người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm thiểu việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không được yêu cầu trong ứng dụng của bạn. ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetIPAddressCollectionDisabled(true); ``` #### Tắt tính năng thu thập và chia sẻ advertising ID \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `SetAppleIDFACollectionDisabled` và/hoặc `SetGoogleAdvertisingIdCollectionDisabled` thành `true` để tắt việc thu thập mã định danh quảng cáo. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách của App Store/Google Play, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc nếu ứng dụng của bạn không cần attribution quảng cáo hay phân tích dựa trên mã định danh quảng cáo. ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetAppleIDFACollectionDisabled(true) .SetGoogleAdvertisingIdCollectionDisabled(true); ``` #### Thiết lập cấu hình media cache cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Mặc định, AdaptyUI lưu cache media (như ảnh và video) để cải thiện hiệu suất và giảm lưu lượng mạng. Bạn có thể tùy chỉnh các cài đặt cache bằng cách cung cấp một cấu hình tùy chỉnh. Dùng `SetAdaptyUIMediaCache` để ghi đè các cài đặt cache mặc định: ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetAdaptyUIMediaCache( 100 * 1024 * 1024, // MemoryStorageTotalCostLimit 100MB null, // MemoryStorageCountLimit 100 * 1024 * 1024 // DiskStorageSizeLimit 100MB ); ``` Tham số: | Parameter | Required | Description | |-----------------------------|----------|----------------------------------------------------------------------------------| | memoryStorageTotalCostLimit | optional | Tổng kích thước cache trong bộ nhớ tính bằng byte. Mặc định theo giá trị riêng của từng nền tảng. | | memoryStorageCountLimit | optional | Giới hạn số lượng item trong bộ nhớ. Mặc định theo giá trị riêng của từng nền tảng. | | diskStorageSizeLimit | optional | Giới hạn kích thước file trên đĩa tính bằng byte. Mặc định theo giá trị riêng của từng nền tảng. | ### 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 tính năng này trên Android, đặt `SetGoogleLocalAccessLevelAllowed` thành `true`: ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetGoogleLocalAccessLevelAllowed(true); ``` ### Xóa dữ liệu khi khôi phục backup \{#clear-data-on-backup-restore\} Khi `SetAppleClearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ bản sao lưu iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng đã cache, chi tiết sản phẩm và paywall. Sau đó SDK sẽ khởi tạo 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. ::: ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetAppleClearDataOnBackup(true); ``` ## Xử lý sự cố \{#troubleshooting\} #### Quy tắc sao lưu Android (cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `<manifest>` có chứa tools: ```xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app"> ... </manifest> ``` #### 2. Ghi đè các thuộc tính backup trong `<application>` \{#2-override-backup-attributes-in-application\} Trong cùng file `AndroidManifest.xml`, cập nhật thẻ `<application>` để ứ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 <application android:name=".App" android:allowBackup="true" android:fullBackupContent="@xml/sample_backup_rules" android:dataExtractionRules="@xml/sample_data_extraction_rules" tools:replace="android:fullBackupContent,android:dataExtractionRules"> ... </application> ``` 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" <?xml version="1.0" encoding="utf-8"?> <data-extraction-rules> <cloud-backup> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </cloud-backup> <device-transfer> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="appsflyer-purchase-data"/> <exclude domain="database" path="afpurchases.db"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> </device-transfer> </data-extraction-rules> ``` **Dành cho Android 11 trở xuống** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" <?xml version="1.0" encoding="utf-8"?> <full-backup-content> <exclude domain="sharedpref" path="appsflyer-data"/> <exclude domain="sharedpref" path="AdaptySDKPrefs.xml"/> :::important Trong Unity, áp dụng các thay đổi này vào `Assets/Plugins/Android/AndroidManifest.xml` và tạo các file quy tắc sao lưu trong `Assets/Plugins/Android/res/xml/`. ::: #### Thanh toán thất bại sau khi quay lại từ ứng dụng khác trên Android \{#purchases-fail-after-returning-from-another-app-in-android\} Nếu Activity khởi động luồng mua hàng sử dụng `launchMode` không 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ị xử lý như đã hủy. Để đảm bảo giao dịch mua hoạt động đúng, chỉ sử dụng chế độ khởi chạy `standard` hoặc `singleTop` cho Activity khởi động flow mua hàng, và tránh các chế độ khác. Trong `AndroidManifest.xml`, đảm bảo Activity khởi động flow mua hàng được đặt thành `standard` hoặc `singleTop`: ```xml <activity android:name=".MainActivity" android:launchMode="standard" /> ``` #### Ứng dụng bị crash khi hiển thị paywall trên Android \{#app-crashes-when-a-paywall-is-displayed-on-android\} Nếu ứng dụng của bạn bị crash trên Android khi hiển thị paywall, có thể plugin Kotlin đang bị thiếu trong cấu hình Gradle. Để thêm nó: 1. Trong **Player Settings**, đảm bảo rằng các tùy chọn **Custom Launcher Gradle Template** và **Custom Base Gradle Template** đã được chọn. <img src="/assets/shared/img/kotlin-plugin1.webp" style={{ border: 'none', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Thêm dòng sau vào `/Assets/Plugins/Android/launcherTemplate.gradle`: ```groovy showLineNumbers apply plugin: 'com.android.application' // highlight-next-line apply plugin: 'kotlin-android' apply from: 'setupSymbols.gradle' apply from: '../shared/keepUnitySymbols.gradle' ``` 3. Thêm dòng sau vào `/Assets/Plugins/Android/baseProjectTemplate.gradle`: ```groovy showLineNumbers plugins { // If you are changing the Android Gradle Plugin version, make sure it is compatible with the Gradle version preinstalled with Unity // See which Gradle version is preinstalled with Unity here https://docs.unity3d.com/Manual/android-gradle-overview.html // See official Gradle and Android Gradle Plugin compatibility table here https://developer.android.com/studio/releases/gradle-plugin#updating-gradle // To specify a custom Gradle version in Unity, go do "Preferences > External Tools", uncheck "Gradle Installed with Unity (recommended)" and specify a path to a custom Gradle version id 'com.android.application' version '8.3.0' apply false id 'com.android.library' version '8.3.0' apply false // highlight-next-line id 'org.jetbrains.kotlin.android' version '1.8.0' apply false **BUILD_SCRIPT_DEPS** } ``` --- # File: unity-quickstart-paywalls --- --- title: "Bật tính năng mua hàng bằng cách sử dụng paywall trong Unity SDK" description: "Tìm hiểu cách hiển thị paywall trong ứng dụng Unity của bạn với Adapty SDK." --- Để 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 kỳ 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 ứng dụng. - [**Placement**](placements) – vị trí và thời điểm hiển thị paywall trong ứng dụng (như `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trong dashboard, sau đó yêu cầu chúng bằng placement ID trong code. Điều này giúp dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho từng nhóm người dùng. Adapty cung cấp cho bạn ba cách để bật tính năng mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Cách triển khai | Độ phức tạp | Khi nào nên dùng | |---------------------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Dễ | Bạn [tạo một paywall hoàn chỉnh, sẵn sàng để mua trong no-code builder](quickstart-paywalls). Adapty tự động render và xử lý toàn bộ flow mua hàng phức tạp, xác thực receipt 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ì tính linh hoạt trong ưu đãi sản phẩm. Xem [hướng dẫn](unity-quickstart-manual). | | Observer mode | 🔴 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. Lưu ý rằng observer mode có những giới hạn nhất định trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai paywall được tạo trong Adapty paywall builder.** Nếu bạn không muốn dùng paywall builder, hãy xem [hướng dẫn xử lý mua hàng trong paywall tạo thủ công](unity-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ị paywall container bạn đã lấy được trong ứng dụng. 3. **Xử lý các hành động nút bấm**: Liên kết tương tác của người dùng với paywall với phản hồi tương ứng trong ứ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 Adapty SDK](sdk-installation-unity) trong code ứng dụng. :::tip Cách nhanh nhất để hoàn thành các bước này là làm theo [hướng dẫn quickstart](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy paywall \{#1-get-the-paywall\} Các paywall của bạn được liên kết với các placement đã cấu hình trong 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à styling 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ị. ::: ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", (paywall, error) => { if(error != null) { // handle the error return; } // Create paywall view parameters var parameters = new AdaptyUICreatePaywallViewParameters(); // Create the paywall view AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { if(error != null) { // handle the error return; } // view - the paywall view ready to be presented }); }); ``` :::info Hướng dẫn quickstart này cung cấp cấu hình tối thiểu cần thiết để hiển thị paywall. Để biết chi tiết cấu hình nâng cao, xem [hướng dẫn lấy paywall](unity-get-pb-paywalls). ::: ## 2. Hiển thị paywall \{#2-display-the-paywall\} Khi đã có cấu hình paywall, bạn chỉ cần thêm vài dòng code để 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ể 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 instance `view` mới. ```csharp showLineNumbers title="Unity" view.Present((error) => { // handle the error }); ``` :::info Để biết thêm chi tiết về cách hiển thị paywall, xem [hướng dẫn](unity-present-paywalls). ::: ## 3. Xử lý các hành động nút bấm \{#3-handle-button-actions\} Khi người dùng nhấn các nút trong paywall, Unity SDK sẽ 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 bạn xử lý hành động trong code. Ví dụ: paywall của bạn thường có nút đóng và các URL để mở (như điều khoản sử dụng và chính sách bảo mật). Để xử lý các hành động này, class của bạn cần implement interface `AdaptyPaywallsEventsListener` và đăng ký làm listener. :::tip Đọc hướng dẫn về cách xử lý [hành động](unity-handle-paywall-actions) và [sự kiện](unity-handling-events) của nút bấm. ::: ```csharp showLineNumbers title="Unity" public class YourClass : MonoBehaviour, AdaptyPaywallsEventsListener { void Start() { // Register this class as the paywall events listener Adapty.SetPaywallsEventsListener(this); } // AdaptyPaywallsEventsListener method - handles button actions public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Close: view.Dismiss(null); break; case AdaptyUIUserActionType.OpenUrl: Application.OpenURL(action.Value); break; default: break; } } } ``` ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra giao dịch mua hàng của bạn trong [App Store sandbox](test-purchases-in-sandbox) hoặc trong [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. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](unity-check-subscription-status) để đảm bảo hiển thị paywall hoặc cấp quyền truy cập tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Dưới đây là cách tích hợp tất cả các bước đó vào ứng dụng của bạn. ```csharp showLineNumbers using System; using UnityEngine; using AdaptySDK; public class PaywallManager : MonoBehaviour, AdaptyPaywallsEventsListener { [SerializeField] private string placementId = "YOUR_PLACEMENT_ID"; private AdaptyUIPaywallView currentPaywallView; void Start() { // Register for paywall events Adapty.SetPaywallsEventsListener(this); GetAndDisplayPaywall(); } private void GetAndDisplayPaywall() { Adapty.GetPaywall(placementId, (paywall, error) => { if (error != null) { Debug.LogError("Error getting paywall: " + error.Message); return; } if (paywall.HasViewConfiguration) { CreateAndPresentPaywallView(paywall); } else { Debug.LogWarning("Paywall was not created using the builder"); } }); } private void CreateAndPresentPaywallView(AdaptyPaywall paywall) { var parameters = new AdaptyUICreatePaywallViewParameters(); AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { if (error != null) { Debug.LogError("Error creating paywall view: " + error.Message); return; } currentPaywallView = view; view.Present((presentError) => { if (presentError != null) { Debug.LogError("Error presenting paywall: " + presentError.Message); return; } Debug.Log("Paywall presented successfully"); }); }); } // AdaptyPaywallsEventsListener implementation public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Close: Debug.Log("Close button pressed"); view.Dismiss(null); break; case AdaptyUIUserActionType.OpenUrl: Application.OpenURL(action.Value); break; default: break; } } // Required interface methods (implement as needed) public void PaywallViewDidAppear(AdaptyUIPaywallView view) { } public void PaywallViewDidDisappear(AdaptyUIPaywallView view) { } public void PaywallViewDidSelectProduct(AdaptyUIPaywallView view, string productId) { } public void PaywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { } public void PaywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchasedResult) { } public void PaywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } public void PaywallViewDidStartRestore(AdaptyUIPaywallView view) { } public void PaywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { } public void PaywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { } public void PaywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { } public void PaywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { } public void PaywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } public void ShowPaywall() { GetAndDisplayPaywall(); } void OnDestroy() { if (currentPaywallView != null) { currentPaywallView.Dismiss(null); } } } ``` --- # File: unity-check-subscription-status --- --- title: "Kiểm tra trạng thái đăng ký trong Unity SDK" description: "Tìm hiểu cách kiểm tra trạng thái đăng ký trong ứng dụng Unity 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ội dung hiển thị — cho xem paywall hay mở quyền truy cập các tính năng trả phí. ## Lấy trạng thái đăng ký \{#get-subscription-status\} Khi quyết định có nên hiển thị paywall hay nội dung trả phí cho người dùng, bạn kiểm tra [mức độ truy cập](access-level) trong hồ sơ người dùng của họ. Bạn có hai lựa chọn: - Gọi `GetProfile` nếu bạn cần dữ liệu hồ sơ mới nhất ngay lập tức (ví dụ: khi khởi động ứng dụng) hoặc muốn buộc cập nhật. - Thiết lập **cập nhật hồ sơ tự động** để giữ một bản sao cục bộ được tự động làm mới mỗi khi trạng thá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 đăng ký là dùng phương thức `GetProfile` để truy cập hồ sơ: ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access }); ``` ### Lắng nghe cập nhật đăng ký \{#listen-to-subscription-updates\} Để tự động nhận cập nhật hồ sơ trong ứng dụng: 1. Kế thừa `AdaptyEventListener` và implement phương thức `OnLoadLatestProfile` — Adapty sẽ tự động gọi phương thức này mỗi khi trạng thá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ể dùng nó trong toàn bộ ứng dụng mà không cần thực hiện thêm các yêu cầu mạng. ```csharp public class SubscriptionManager : MonoBehaviour, AdaptyEventListener { private AdaptyProfile currentProfile; void Start() { // Register this object as an Adapty event listener Adapty.SetEventListener(this); } // Store the profile when it updates public void OnLoadLatestProfile(AdaptyProfile profile) { currentProfile = profile; // Update UI, unlock content, etc. } public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } public void OnInstallationDetailsFail(AdaptyError error) { } // Use stored profile instead of calling getProfile() public bool HasAccess() { if (currentProfile?.AccessLevels != null && currentProfile.AccessLevels.ContainsKey("premium")) { return currentProfile.AccessLevels["premium"].IsActive; } return false; } } ``` :::note Adapty tự động gọi `OnLoadLatestProfile` khi ứng dụng khởi động, cung cấp dữ liệu đăng ký đã được cache ngay cả khi thiết bị ngoại tuyến. ::: ## Kết nối hồ sơ với logic paywall \{#connect-profile-with-paywall-logic\} Khi bạn cần đưa ra quyết định ngay lập tức về việc hiển thị paywall hay cấp quyền truy cập tính năng trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách tiếp cận này hữu ích cho các tình huống như 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ể. ```csharp private void CheckAccessLevel() { Adapty.GetProfile((profile, error) => { if (error != null) { Debug.LogError("Error checking access level: " + error.Message); // Show paywall if access check fails return; } var accessLevel = profile.AccessLevels["YOUR_ACCESS_LEVEL"]; if (accessLevel == null || !accessLevel.IsActive) { // Show paywall if no access } }); } private void InitializePaywall() { LoadPaywall(); CheckAccessLevel(); } ``` ## Bước tiếp theo \{#next-steps\} Sau khi đã biết cách theo dõi trạng thái đăng ký, hãy tìm hiểu cách [làm việc với hồ sơ người dùng](unity-quickstart-identify) để đảm bảo người dùng có thể truy cập những gì họ đã trả phí. --- # File: unity-quickstart-identify --- --- title: "Xác định người dùng trong Unity SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký in-app trong Unity." --- :::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ó phù hợp với hệ thống xác thực hiện có của bạn. ::: Cách bạn quản lý 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, hãy 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, hãy xem [phần về người dùng đã xác định](#identified-users). **Các khái niệm chính**: - **Hồ sơ người dùng** là các thực thể cần thiết để SDK hoạt động. Adapty tự động tạo chúng. - Chúng có thể là ẩn danh **(không có customer user ID)** hoặc đã xác định **(có customer user ID)**. - Bạn cung cấp **customer user ID** để đối chiếu hồ sơ người dùng trong Adapty với hệ thống xác thực nội bộ của bạn. Đây là những điểm 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 của họ | | **Quản lý hồ sơ người dùng** | Hồ sơ người dùng mới mỗi lần cài đặt lại | Cùng một hồ sơ người dùng xuyên suốt các 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 được lưu trữ 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 khi ứng dụng khởi chạy, Adapty **tạo một hồ sơ người dùng mới cho người dùng**. 2. Khi người dùng mua bất cứ thứ gì trong ứng dụng, giao dịch mua này được **liên kết với hồ sơ người dùng Adapty của họ 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ơ người dùng ẩn danh mới khi kích hoạt**. 4. Nếu người dùng đã thực hiện giao dịch mua trước đó trong ứng dụng của bạn, theo mặc định, các giao dịch mua của họ sẽ được tự động đồng bộ từ App Store khi SDK được kích hoạt. Vậy, với người dùng ẩn danh, hồ sơ người dùng mới sẽ được tạo mỗi lần cài đặt, nhưng điều đó không phải là vấn đề vì trong phân tích Adapty, bạn có thể [cấu hình những gì sẽ được coi là lần cài đặt mới](general#4-installs-definition-for-analytics). Đối với người dùng ẩn danh, bạn cần đếm lượt cài đặt theo **ID thiết bị**. Trong trường hợp này, mỗi lần cài đặt ứng dụng trên một thiết bị được tính là một lượt cài đặt, bao gồm cả cài đặt lại. ## Người dùng đã xác định \{#identified-users\} Bạn có hai lựa 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 liên kết với Customer User ID khác, mức độ truy cập được chia sẻ, vì vậy cả hai hồ sơ người dùng đề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ơ người dùng này sang hồ sơ người dùng 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. ::: <img src="/assets/shared/img/identify-diagram.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ### 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 vào ứng dụng hoặc đăng ký), hãy sử dụng phương thức `identify` để đặt customer user ID của họ. - Nếu bạn **chưa từng sử dụng customer user ID này trước đây**, Adapty sẽ tự động liên kết nó với hồ sơ người dùng 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ơ người dùng đượ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. ::: Hãy chờ callback hoàn thành của `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 rơi vào hồ sơ người dùng ẩn danh. Xem [Thứ tự gọi trong Unity SDK](unity-sdk-call-order). ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user if(error == null) { // successful identify } }); ``` ### 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 biệt. Nếu bạn biết customer user ID nhưng chỉ đặt nó sau khi kích hoạt, điều đó có nghĩa là khi kích hoạt, Adapty sẽ tạo một hồ sơ người dùng ẩn danh mới và chỉ chuyển sang hồ sơ người dùng hiện có sau khi bạn gọi `identify`. Bạn có thể truyền vào customer user ID hiện có (đã từng sử dụng trước đây) hoặc một ID mới. Nếu bạn truyền một ID mới, hồ sơ người dùng mới được tạo khi kích hoạt sẽ được tự động liên kết với customer user ID đó. :::note Theo mặc định, việc tạo hồ sơ người dùng ẩn danh không ảnh hưởng đến các dashboard phân tích, vì lượt cài đặt được tính dựa trên ID thiết bị. ID thiết bị đại diện cho một lần cài đặt ứng dụng từ cửa hàng trên một thiết bị và chỉ được tạo lại sau khi ứng dụng được cài đặt lại. Nó không phụ thuộc vào việc đây là lần cài đặt đầu tiên hay lần cài đặt lại, hoặc liệu có sử dụng customer user ID hiện có hay không. Việc tạo hồ sơ người dùng (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 thêm sự kiện cài đặt. Nếu bạn muốn đếm lượt 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). ::: ```csharp showLineNumbers using UnityEngine; using AdaptySDK; var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") .SetCustomerUserId("YOUR_USER_ID"); // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); ``` ### Đăng xuất người dùng \{#log-users-out\} Nếu bạn có nút cho phép người dùng đăng xuất, hãy sử dụng phương thức `logout`. :::important Việc đăng xuất người dùng sẽ tạo một hồ sơ người dùng ẩn danh mới cho người dùng đó. ::: ```csharp showLineNumbers Adapty.Logout((error) => { if(error == null) { // successful logout } }); ``` :::info Để đăng nhập lại người dùng vào ứng dụng, hãy sử dụng phương thức `identify`. ::: ### Cho phép mua hàng mà không cần đăng nhập \{#allow-purchases-without-login\} Nếu người dùng của bạn có thể thực hiện giao dịch mua cả trước và sau khi đăng nhập vào ứng dụng, bạn cần đảm bảo rằng họ sẽ 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 gắn nó với ID hồ sơ người dùng ẩn danh của họ. 2. Khi người dùng đăng nhập vào tài khoản, Adapty chuyển sang làm việc với hồ sơ người dùng đã 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 gán customer user ID cho hồ sơ người dùng 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ơ người dùng), bạn cần lấy mức độ truy cập thực tế sau khi chuyển đổi hồ sơ người dùng. Bạn có thể gọi [`getProfile`](unity-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe các cập nhật hồ sơ người dùng](unity-check-subscription-status) để dữ liệu tự động đồng bộ. ## Các bước tiếp theo \{#next-steps\} Xin chúc mừng! Bạn đã triển khai logic thanh toán in-app trong ứng dụng của mình! Chúc bạn thành công với việc kiếm tiền từ ứng dụng! Để khai thác Adapty tối đa 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 - [**Onboardings**](onboardings): Thu hút người dùng với onboarding và thúc đẩy sự gắn kết - [**Tích hợp**](configuration): Tích hợp với các dịch vụ attribution marketing và phân tích chỉ trong một dòng code - [**Đặt thuộc tính hồ sơ người dùng tùy chỉnh**](unity-setting-user-attributes): Thêm thuộc tính tùy chỉnh vào hồ sơ người dùng và tạo phân khúc, để bạn có thể chạy A/B test hoặc hiển thị các paywall khác nhau cho các người dùng khác nhau --- # File: adapty-sdk-integration-skill-unity --- --- title: "Tích hợp Adapty vào ứng dụng Unity của bạn với kỹ năng tích hợp SDK" description: "Sử dụng skill adapty-sdk-integration để tích hợp Adapty SDK vào ứng dụng Unity của bạn từ đầu đến cuối với công cụ lập trình AI của bạn." --- :::important Skill 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-unity) — hướng dẫn này sẽ dẫn dắt công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn form phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt xong, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-unity --- --- title: "Tích hợp Adapty vào ứng dụng Unity 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 Unity của bạn sử dụng Cursor, Context7, ChatGPT, Claude, hoặc các công cụ AI khác." --- Hướng dẫn này sẽ đưa bạn qua từng bước tích hợp Adapty vào ứng dụng Unity với một công cụ AI — bạn chỉ cần cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ đoạn code SDK nào. Bạn có thể thực hiện điều này với 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 ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng của mình](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm 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ả khi nào cần mở Dashboard để kết nối cửa hàng. ### Cách thủ công qua Dashboard \{#dashboard-approach\} Nếu bạn muốn tự cấu hình mọi thứ, đây là những gì bạn cần trước khi viết code. LLM của bạn không thể tra cứu các giá trị dashboard — bạn cần cung cấp chúng. 1. **Kết nối 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 Unity của bạn nhắm đến cả hai nền tảng. Đây là yêu cầu bắt buộc để mua hàng 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 Adapty configuration builder. 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 paywall và placement**: Trong Adapty Dashboard, tạo paywall trên trang **Paywalls**, sau đó gán nó cho một placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty.GetPaywall("YOUR_PLACEMENT_ID")`. [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 truy cập 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 bạn có đủ năm mục 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 nhiều lệnh 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 cập nhật 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 sẽ 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 [Context7 GitHub repository](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 Unity SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn quan trọng. Hãy làm theo [hướng dẫn triển khai](#implementation-walkthrough) bên dưới từng bước để đảm bảo mọi thứ hoạt động. ::: ### 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. Thêm `.md` vào cuối URL, hoặc nhấn **Copy for LLM** dưới tiêu đề bài viết. Ví dụ: [adapty-cursor-unity.md](https://adapty.io/docs/vi/adapty-cursor-unity.md). Mỗi giai đoạn trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới đều có block "Gửi cho LLM của bạn" với các link `.md` để dán vào. Để có nhiều tài liệu hơn cùng một lúc, xem [file index và các tập con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này đi qua việc tích hợp Adapty theo thứ tự triển khai. Mỗi giai đoạn bao gồm tài liệu cần gửi cho LLM, những gì bạn sẽ thấy khi hoàn thành, và các vấn đề thường gặp. ### Lên kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết code, hãy yêu cầu LLM phân tích dự án của bạn và tạo kế hoạch triển khai. Nếu công cụ AI của bạn hỗ trợ chế độ lập kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy sử dụng nó để LLM có thể đọc cả cấu trúc dự án lẫn tài liệu Adapty trước khi viết code. Hãy cho LLM biết bạn dùng cách tiếp cận nào để xử lý mua hàng — điều này ảnh hưởng đến hướng dẫn mà nó nên theo: - [**Adapty Paywall Builder**](adapty-paywall-builder): Bạn tạo paywall trong trình tạo no-code của Adapty, và SDK hiển thị chúng tự động. - [**Paywall tự tạo**](unity-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ý mua hàng. - [**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](unity-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm package Adapty SDK qua Unity Package Manager và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — mọi thứ khác đều không hoạt động nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-unity) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-unity.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Dự án build và chạy được. Unity Console 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ật từ App settings chưa. ::: ### Hiển thị paywall và xử lý mua hàng \{#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. Hướng dẫn bạn cần phụ thuộc vào cách bạn xử lý mua hàng. Kiểm tra từng lần mua trong sandbox khi bạn thực hiện — đừng chờ đến cuối. Xem [Kiểm tra mua hàng trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. <Tabs groupId="paywall-approach"> <TabItem value="builder" label="Paywall Builder" default> **Hướng dẫn:** - [Bật mua hàng sử dụng paywall (quickstart)](unity-quickstart-paywalls) - [Lấy paywall từ Paywall Builder và cấu hình của chúng](unity-get-pb-paywalls) - [Hiển thị paywall](unity-present-paywalls) - [Xử lý sự kiện paywall](unity-handling-events) - [Phản hồi các hành động button](unity-handle-paywall-actions) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/unity-quickstart-paywalls.md - https://adapty.io/docs/vi/unity-get-pb-paywalls.md - https://adapty.io/docs/vi/unity-present-paywalls.md - https://adapty.io/docs/vi/unity-handling-events.md - https://adapty.io/docs/vi/unity-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Paywall xuất hiện 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 minh placement ID khớp chính xác với dashboard và placement có đối tượng được gán. ::: </TabItem> <TabItem value="manual" label="Paywall tự tạo"> **Hướng dẫn:** - [Bật mua hàng trong paywall tùy chỉnh của bạn (quickstart)](unity-quickstart-manual) - [Lấy paywall và sản phẩm](fetch-paywalls-and-products-unity) - [Hiển thị paywall được thiết kế bởi remote config](present-remote-config-paywalls-unity) - [Thực hiện mua hàng](unity-making-purchases) - [Khôi phục mua hàng](unity-restore-purchase) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/unity-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products-unity.md - https://adapty.io/docs/vi/present-remote-config-paywalls-unity.md - https://adapty.io/docs/vi/unity-making-purchases.md - https://adapty.io/docs/vi/unity-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 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 minh paywall có sản phẩm được gán trong dashboard và placement có đối tượng. ::: </TabItem> <TabItem value="observer" label="Observer mode"> **Hướng dẫn:** - [Tổng quan Observer mode](observer-vs-full-mode) - [Triển khai Observer mode](implement-observer-mode-unity) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode-unity) Gửi 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-unity.md - https://adapty.io/docs/vi/report-transactions-observer-mode-unity.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi mua hàng sandbox bằng flow mua hàng hiện tại của bạn, giao dịch xuất hiện trong **Event Feed** trên Adapty dashboard. - **Lưu ý:** Không có sự kiện → xác minh bạn đang báo cáo giao dịch cho Adapty và server notifications được cấu hình cho cả hai cửa hàng. ::: </TabItem> </Tabs> ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua hàng, kiểm tra hồ sơ người dùng để xem mức độ truy cập có đang hoạt động không nhằm giới hạn nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](unity-check-subscription-status) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/unity-check-subscription-status.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi mua hàng sandbox, `profile.AccessLevels["premium"]?.IsActive` trả về `true`. - **Lưu ý:** `AccessLevels` trống sau khi mua hàng → kiểm tra sản phẩm có mức độ truy cập được gán trong dashboard chưa. ::: ### Xác định người dùng \{#identify-users\} Liên kết tài khoản người dùng trong ứng dụng của bạn với hồ sơ Adapty để mua hàng được duy trì trên nhiều thiết bị. :::important Bỏ qua bước này nếu ứng dụng của bạn không có xác thực. ::: **Hướng dẫn:** [Xác định người dùng](unity-quickstart-identify) Gửi cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/unity-quickstart-identify.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi gọi `Adapty.Identify("your-user-id")`, phần **Profiles** trên dashboard hiển thị ID người dùng tùy chỉnh của bạn. - **Lưu ý:** Gọi `Identify` sau khi kích hoạt nhưng trước khi lấy paywall để tránh attribution hồ sơ ẩn danh. ::: ### Chuẩn bị phát hành \{#prepare-for-release\} Khi tích hợp của bạn hoạt động trong sandbox, hãy đi qua checklist phát hành để đảm bảo mọi thứ sẵn sàng cho môi trường production. **Hướng dẫn:** [Checklist phát hành](release-checklist) Gửi 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 checklist được xác nhận: kết nối cửa hàng, server notifications, flow mua hàng, kiểm tra mức độ truy cập và các yêu cầu về quyền riêng tư. - **Lưu ý:** Thiếu server notifications → 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**. ::: ## 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 ngữ cảnh rộng hơn ngoài các trang riêng lẻ, chúng tôi lưu trữ các file index liệt kê hoặc kết hợp toàn bộ tài liệu Adapty: - [`llms.txt`](https://adapty.io/docs/vi/llms.txt): Liệt kê tất cả các trang với các link `.md`. Đây là [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho website có thể truy cập bởi LLM. Lưu ý rằng với một số AI agent (ví dụ: ChatGPT), bạn sẽ cần tải xuống `llms.txt` 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ộ tài liệu Adapty được kết hợp thành một file duy nhất. Rất lớn — chỉ dùng khi bạn cần toàn bộ nội dung. - [`unity-llms.txt`](https://adapty.io/docs/vi/unity-llms.txt) và [`unity-llms-full.txt`](https://adapty.io/docs/vi/unity-llms-full.txt) dành riêng cho Unity: Các tập con theo nền tảng giúp tiết kiệm token so với toàn bộ site. --- # File: unity-get-pb-paywalls --- --- title: "Lấy paywall từ Paywall Builder và cấu hình của chúng trong Unity 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 ứng dụng Unity của bạn." --- Sau khi [bạn thiết kế phần giao diện cho paywall của mình](adapty-paywall-builder) bằng Paywall Builder mới trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên trong quy trình này là lấy paywall được liên kết với placement và cấu hình view của nó như mô tả bên dưới. :::warning Paywall Builder mới hoạt động với Unity 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 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 của bạn](fetch-paywalls-and-products-unity). :::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. ::: <details> <summary>Trước khi bắt đầu hiển thị paywall trong ứng dụng di động của bạn (nhấn để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-unity) trong ứng dụng di động của bạn. </details> ## 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 mã ứng dụng di động để hiển thị cho người dùng. Paywall như vậy chứa cả nội dung cần hiển thị và cách hiển thị nó. 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 sau đó hiển thị nó trong ứng dụng di động của bạn. Để đảm bảo hiệu suất tối ưu, điều quan trọng là phải lấy paywall và [cấu hình view](unity-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, cho phép đủ thời gian để tải hình ảnh trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `GetPaywall`: ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", "en", (paywall, error) => { if(error != null) { // handle the error return; } // paywall - the resulting object }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này được kỳ vọng là mã ngôn ngữ bao gồm một hoặc hai subtag được phân tách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là cho ngôn ngữ, subtag thứ hai là cho khu vực.</p><p></p><p>Ví dụ: `en` có nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha Brazil.</p><p>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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố gắng tải dữ liệu từ máy chủ và sẽ trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình gặp vấn đề với kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ kém đến đâu. Cache được cập nhật thường xuyên, vì vậy việc sử dụng nó trong phiên là an toàn để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để lấy paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập đượ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.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm các yêu cầu khác nhau bên dưới.</p> | Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html) với danh sách ID sản phẩm, mã định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình 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 toggle **Show on device** trong paywall builder. Nếu tùy chọn này không đượ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ó `ViewConfiguration` không, điều này cho biết nó được tạo bằng Paywall Builder. Điều này sẽ hướng dẫn bạn cách hiển thị paywall. Nếu `ViewConfiguration` có mặt, hãy xử lý nó như paywall Paywall Builder; nếu không, [xử lý nó như paywall remote config](present-remote-config-paywalls-unity). Trong Unity SDK, hãy gọi trực tiếp phương thức `CreatePaywallView` mà không cần lấy cấu hình view thủ công trước. :::warning Kết quả của phương thức `CreatePaywallView` chỉ có thể được sử dụng một lần. Nếu bạn cần sử dụng lại, hãy gọi phương thức `CreatePaywallView` từ đầu. Gọi nó hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```csharp showLineNumbers var parameters = new AdaptyUICreatePaywallViewParameters() .SetPreloadProducts(preloadProducts) .SetLoadTimeout(new TimeSpan(0, 0, 3)); AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { // handle the result }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :------------------ | :------------- | :----------------------------------------------------------- | | **paywall** | bắt buộc | Một đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm các yêu cầu khác nhau bên dưới. | | **PreloadProducts** | tùy chọn | Cung cấp một mảng `AdaptyPaywallProducts` để tối ưu hóa thời gian hiển thị sản phẩm trên màn hình. Nếu `nil` được truyền vào, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **CustomTags** | tùy chọn | Định nghĩa một dictionary các custom tag và giá trị đã được xử lý của chúng. Custom tag đóng vai trò là placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể cho nội dung được cá nhân hóa trong paywall. Tham khảo chủ đề Custom tags in paywall builder để biết thêm chi tiết. | | **CustomTimers** | tùy chọn | Định nghĩa một dictionary các custom timer và ngày kết thúc của chúng. Custom timer cho phép bạn hiển thị đồng hồ đếm ngược trong paywall của mình. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](localizations-and-locale-codes). ::: Sau khi có view, [hiển thị paywall](unity-present-paywalls). ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets. Hình ảnh hero và video có ID được định nghĩa sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị ảnh xem trước trước khi phát video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty Unity SDK lên phiên bản 3.8.0 trở lên. ::: Dưới đây là ví dụ về cách bạn có thể cung cấp custom assets thông qua một dictionary đơn giản: ```csharp showLineNumbers var customAssets = new Dictionary<string, AdaptyCustomAsset> { { "custom_image", AdaptyCustomAsset.LocalImageFile("custom_assets/images/custom_image.png") }, { "hero_video", AdaptyCustomAsset.LocalVideoFile("custom_assets/videos/custom_video.mp4") } }; var parameters = new AdaptyUICreatePaywallViewParameters() .SetCustomAssets(customAssets) .SetLoadTimeout(new TimeSpan(0, 0, 3)); AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { // handle the result }); ``` :::note Nếu một asset không được tìm thấy, paywall sẽ hiển thị với giao diện mặc định của nó. ::: ## 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 Unity của bạn, bạn có thể truyền một dictionary gồm các ID timer và ngày kết thúc của chúng trực tiếp vào phương thức `SetCustomTimers`. Dưới đây là ví dụ: ```csharp showLineNumbers var customTimers = new Dictionary<string, DateTime> { { "CUSTOM_TIMER_6H", DateTime.Now.AddHours(6) }, { "CUSTOM_TIMER_NY", new DateTime(2025, 1, 1) } }; var parameters = new AdaptyUICreatePaywallViewParameters() .SetCustomTimers(customTimers) .SetLoadTimeout(new TimeSpan(0, 0, 3)); AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { // handle the result }); ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là các **Timer ID** của các timer do nhà phát triển định nghĩa mà bạn đặt trong Adapty Dashboard. Bộ xử lý timer đả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. ## 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, paywall được lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong các trường hợp bạn có nhiều đối tượng và paywall, và người dùng của bạn có kết nối internet yếu, việc 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 cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `GetPaywallForDefaultAudience`, phương thức này 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 trình bày chi tiết trong phần [Lấy Paywall](#fetch-paywall) ở trên. :::warning Hãy cân nhắc sử dụng `GetPaywall` thay vì `GetPaywallForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể tạo ra vấn đề khi hỗ trợ nhiều phiên bản ứng dụng, đòi hỏi thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không chính xác. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ việc nhắm mục tiêu dựa trên quốc gia, attribution hoặc custom attributes. Nếu việc lấy nhanh hơn vượt trội so với những hạn chế này cho trường hợp sử dụng của bạn, hãy sử dụng `GetPaywallForDefaultAudience` như được hiển thị bên dưới. Nếu không, hãy sử dụng `GetPaywall` như được mô tả [ở trên](#fetch-paywall). ::: ```csharp showLineNumbers Adapty.GetPaywallForDefaultAudience("YOUR_PLACEMENT_ID", "en", (paywall, error) => { if(error != null) { // handle the error return; } // paywall - the resulting object }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Mã định danh của bản địa hóa paywall. Tham số này được kỳ vọng là mã ngôn ngữ bao gồm một hoặc hai subtag được phân tách bằng ký tự dấu trừ (**-**). Subtag đầu tiên là cho ngôn ngữ, subtag thứ hai là cho khu vực.</p><p></p><p>Ví dụ: `en` có nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha Brazil.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố gắng tải dữ liệu từ máy chủ và sẽ trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>Tuy nhiên, nếu bạn cho rằng người dùng của mình gặp vấn đề với kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ kém đến đâu. Cache được cập nhật thường xuyên, vì vậy việc sử dụng nó trong phiên là an toàn để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>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. Chúng tôi cũng sử dụng CDN để lấy paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập đượ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.</p> | --- # File: unity-present-paywalls --- --- title: "Hiển thị paywall" description: "Tìm hiểu cách hiển thị paywall trong ứng dụng Unity của bạn với Adapty SDK." --- Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này đề cập đến **Paywall Builder mới**, yêu cầu Adapty SDK 3.3.0 trở lên. Để hiển thị paywall dùng Remote Config, xem [Render paywalls designed with remote config](present-remote-config-paywalls). ::: Để hiển thị một paywall, sử dụng phương thức `view.Present()` trên `view` được tạo bởi phương thức [`CreatePaywallView`](unity-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `CreatePaywallView` thêm một lần nữa để tạo một `view` instance mới. :::warning Việc tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```csharp showLineNumbers title="Unity" view.Present((error) => { // 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. ::: ## Hiển thị hộp thoại \{#show-dialog\} Sử dụng phương thức này thay vì các hộp thoại cảnh báo gốc khi một paywall view đang được hiển thị trên Android. Trên Android, các cảnh báo thông thường xuất hiện phía sau paywall view, khiến người dùng không nhìn thấy chúng. Phương thức này đảm bảo hộp thoại được hiển thị đúng cách phía trên paywall trên tất cả các nền tảng. ```csharp showLineNumbers title="Unity" var dialog = new AdaptyUIDialogConfiguration() .SetTitle("Close paywall?") .SetContent("You will lose access to exclusive offers.") .SetDefaultActionTitle("Stay") .SetSecondaryActionTitle("Close"); AdaptyUI.ShowDialog(view, dialog, (action, error) => { if (error == null) { if (action == AdaptyUIDialogActionType.Secondary) { // User confirmed - close the paywall view.Dismiss(); } // If primary - do nothing, user stays } }); ``` ## Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `Present()`. Tham số này nhận giá trị `AdaptyUIIOSPresentationStyle.FullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.PageSheet`. ```csharp showLineNumbers title="Unity" view.Present(AdaptyUIIOSPresentationStyle.PageSheet, (error) => { // handle the error }); ``` --- # File: unity-handle-paywall-actions --- --- title: "Xử lý hành động nút trong Unity SDK" description: "Xử lý hành động nút trên paywall trong Unity bằng Adapty để tối ưu hóa doanh thu ứng dụng." --- Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập 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 ID hành động tùy chỉnh. 2. Viết code trong ứng dụng để xử lý từng hành động bạn đã gán. Hướng dẫn này 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 cần được xử lý trong code của ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code ứng dụng, triển khai handler cho hành động `close` để đóng paywall. ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Close: view.Dismiss(null); break; default: // handle other events 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ư các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết từ paywall của bạn (ví dụ: **Điều khoản sử dụng** hoặc **Chính sách quyền riêng 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 handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.OpenUrl: var urlString = action.Value; if(!string.IsNullOrWhiteSpace(urlString)) { Application.OpenURL(urlString); } break; default: // handle other events break; } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Custom** với ID `login`. 2. Trong code ứng dụng, triển khai handler cho hành động tùy chỉnh `login` để xác định người dùng của bạn. ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Custom: if (action.Value == "login") { // Navigate to login scene SceneManager.LoadScene("LoginScene"); } break; default: // handle other events 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 ID cho nó. 2. Trong code ứng dụng, triển khai handler cho ID hành động bạn đã tạo. Ví dụ, nếu bạn có thêm một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần, bạn có thể thêm nút để hiển thị một paywall khác: ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIPaywallView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Custom: if (action.Value == "openNewPaywall") { // Display another paywall ShowAlternativePaywall(); } break; default: // handle other events break; } } private void ShowAlternativePaywall() { // Implement your logic to show alternative paywall } ``` --- # File: unity-handling-events --- --- title: "Xử lý sự kiện paywall" description: "Tìm hiểu cách xử lý các sự kiện paywall trong ứng dụng Unity của bạn với Adapty SDK." --- :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, lựa chọn sản phẩm và hiển thị 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](unity-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. Những sự kiện đó bao gồm các lần nhấn nút (nút đóng, URL, lựa chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện 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.3.0 trở lên. ::: :::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. ::: ## Xử lý sự kiện \{#handling-events\} Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai interface `AdaptyPaywallsEventsListener`: ```csharp showLineNumbers title="Unity" using UnityEngine; using AdaptySDK; public class PaywallEventsHandler : MonoBehaviour, AdaptyPaywallsEventsListener { void Start() { Adapty.SetPaywallsEventsListener(this); } // Implement all required interface methods below } ``` ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### Paywall xuất hiện \{#paywall-appeared\} Được gọi khi màn hình paywall hiển thị trên màn hình. :::note Trên iOS, cũng được gọi khi người dùng nhấn vào [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall và web paywall mở trong trình duyệt trong ứng dụng. ::: ```csharp showLineNumbers title="Unity" public void PaywallViewDidAppear(AdaptyUIPaywallView view) { } ``` #### Paywall biến mất \{#paywall-disappeared\} Đượ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 [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. ::: ```csharp showLineNumbers title="Unity" public void PaywallViewDidDisappear(AdaptyUIPaywallView view) { } ``` #### Chọn sản phẩm \{#product-selection\} Được gọi khi một sản phẩm được chọn để mua (bởi người dùng hoặc hệ thống). ```csharp showLineNumbers title="Unity" public void PaywallViewDidSelectProduct( AdaptyUIPaywallView view, string productId ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "productId": "premium_monthly" } ``` </Details> #### Bắt đầu mua \{#started-purchase\} Được gọi khi người dùng khởi tạo quá trình mua hàng. ```csharp showLineNumbers title="Unity" public void PaywallViewDidStartPurchase( AdaptyUIPaywallView view, AdaptyPaywallProduct product ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ``` </Details> #### Mua thành công, đã hủy hoặc đang chờ xử lý \{#successful-canceled-or-pending-purchase\} Nếu giao dịch mua thành công, người dùng hủy giao dịch, hoặc giao dịch mua đang ở trạng thái chờ xử lý, phương thức này sẽ được gọi. Các trường hợp người dùng hủy và thanh toán đang chờ xử lý (ví dụ: cần phê duyệt của phụ huynh) sẽ kích hoạt phương thức này, không phải `PaywallViewDidFailPurchase`. ```csharp showLineNumbers title="Unity" public void PaywallViewDidFinishPurchase( AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchasedResult ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "Success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Cancelled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "UserCancelled" } } // 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": "Pending" } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình trong trường hợp này. #### Mua thất bại \{#failed-purchase\} Nếu giao dịch mua thất bại do lỗi, phương thức này sẽ được gọi. Điều này bao gồm các lỗi StoreKit/Google Play Billing (hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng), lỗi xác minh giao dịch và lỗi hệ thống. Lưu ý rằng các trường hợp người dùng hủy sẽ kích hoạt `PaywallViewDidFinishPurchase` với kết quả đã hủy, và các thanh toán đang chờ xử lý không kích hoạt phương thức này. ```csharp showLineNumbers title="Unity" public void PaywallViewDidFailPurchase( AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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" } } } ``` </Details> #### Bắt đầu khôi phục \{#started-restore\} Được gọi khi người dùng khởi tạo quá trình khôi phục: ```csharp showLineNumbers title="Unity" public void PaywallViewDidStartRestore(AdaptyUIPaywallView view) { } ``` #### Khôi phục thành công \{#successful-restore\} Được gọi khi khôi phục giao dịch mua thành công: ```csharp showLineNumbers title="Unity" public void PaywallViewDidFinishRestore( AdaptyUIPaywallView view, AdaptyProfile profile ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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" } ] } } ``` </Details> Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` cần thiết. Tham khảo chủ đề [Trạng thái gói đăng ký](unity-listen-subscription-changes) để tìm hiểu cách kiểm tra. #### Khôi phục thất bại \{#failed-restore\} Được gọi khi khôi phục giao dịch mua thất bại: ```csharp showLineNumbers title="Unity" public void PaywallViewDidFailRestore( AdaptyUIPaywallView view, AdaptyError error ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ``` </Details> #### Hoàn tất điều hướng thanh toán web \{#finished-web-payment-navigation\} Sau khi cố gắng mở [web paywall](web-paywall) để mua hàng (dù thành công hay thất bại), phương thức này sẽ được gọi: ```csharp showLineNumbers title="Unity" public void PaywallViewDidFinishWebPaymentNavigation( AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error ) { } ``` **Tham số:** - `product`: Sản phẩm mà web paywall được mở (hoặc thử mở) - `error`: `null` nếu web paywall mở thành công, hoặc `AdaptyError` nếu thất bại <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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": "wrong_param", "message": "Current method is not available for this product", "details": { "underlyingError": "Product not configured for web purchases" } } } ``` </Details> ### Tải dữ liệu và hiển thị \{#data-fetching-and-rendering\} #### Lỗi tải sản phẩm \{#product-loading-errors\} Được gọi khi tải sản phẩm thất bại và cung cấp `AdaptyError`. 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ự lấy các đối tượng cần thiết từ máy chủ. Thao tác này có thể thất bại và AdaptyUI sẽ báo lỗi bằng cách gọi phương thức này: ```csharp showLineNumbers title="Unity" public void PaywallViewDidFailLoadingProducts( AdaptyUIPaywallView view, AdaptyError error ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ``` </Details> #### Lỗi hiển thị \{#rendering-errors\} Được gọi khi xảy ra lỗi trong quá trình hiển thị giao diện và cung cấp `AdaptyError`: ```csharp showLineNumbers title="Unity" public void PaywallViewDidFailRendering( AdaptyUIPaywallView view, AdaptyError error ) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ``` </Details> 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, vui lòng thông báo cho chúng tôi. --- # File: unity-web-paywalls --- --- title: "Triển khai web paywall trong Unity SDK" description: "Thiết lập web paywall để nhận thanh toán mà không mất phí và không cần kiểm duyệt của App Store." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trong dashboard](web-paywall) và đã cài đặt Adapty SDK phiên bản 3.14 trở lên. ::: ## Mở web paywall \{#open-web-paywalls\} Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywall bằng phương thức SDK. Phương thức `Adapty.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 một người dùng nhất định với trang web họ được chuyển hướng đến. 2. Theo dõi khi người dùng quay lại ứng dụng và sau đó gọi `Adapty.GetProfile` theo chu kỳ ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không. Nhờ vậy, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ kích hoạt trong ứng dụng gần như ngay lập tức. ```csharp showLineNumbers title="Unity" Adapty.OpenWebPaywall( product, (error) => { if (error != null) { Debug.LogError($"Failed to open web paywall: {error.Message}"); } else { Debug.Log("Web paywall opened successfully"); } } ); ``` :::note Có hai phiên bản của phương thức `OpenWebPaywall`: 1. `OpenWebPaywall(product)` tạo URL theo paywall và đồng thời thêm dữ liệu sản phẩm vào URL. 2. `OpenWebPaywall(paywall)` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng phiên bản này khi các sản phẩm trong Adapty paywall của bạn khác với những sản phẩm trên web paywall. ::: #### Xử lý lỗi \{#handle-errors\} | Mã lỗi | Mô tả | Hành động khuyến nghị | |-----------|--------------------------------------------------------|---------------------------------------------------------------------------| | `AdaptyErrorCode.WrongParam` | Paywall hoặc sản phẩm chưa được cấu hình URL mua hàng qua web, hoặc không thể mở URL trong trình duyệt | Kiểm tra thông báo lỗi để biết chi tiết. Xác minh cấu hình paywall/sản phẩm trong Adapty Dashboard, hoặc kiểm tra cài đặt thiết bị. | | `AdaptyErrorCode.DecodingFailed` | 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 | :::note Kiểm tra thuộc tính `Message` của lỗi để biết thông tin cụ thể về vấn đề xảy ra, vì `WrongParam` có thể chỉ ra nhiều sự cố khác nhau (thiếu URL mua hàng, không mở được trình duyệt, v.v.). ::: ## 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ợ bắt đầu từ Adapty SDK v3.15. ::: Theo mặc định, web paywall mở trong trình duyệt ngoài, khiến người dùng rời khỏi ứng dụng của bạn. Để 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 bên trong ứng dụng, 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, truyền `AdaptyWebPresentation.InAppBrowser` vào phương thức `OpenWebPaywall`: ```csharp showLineNumbers title="Unity" Adapty.OpenWebPaywall( product, AdaptyWebPresentation.InAppBrowser, // default — ExternalBrowser (error) => { if (error != null) { Debug.LogError($"Failed to open web paywall: {error.Message}"); } else { Debug.Log("Web paywall opened successfully"); } } ); ``` --- # File: unity-use-fallback-paywalls --- --- title: "Unity - Use fallback paywalls" 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ợ bởi Unity 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/StreamingAssets` chung trong dự án của bạn. 2. Gọi phương thức `.setFallback` **trước khi** bạn tải paywall hoặc onboarding mục tiêu. ```csharp using UnityEngine; using AdaptySDK; #if UNITY_IOS string fileName = "ios_fallback.json"; #elif UNITY_ANDROID string fileName = "android_fallback.json"; #else // Optional: handle Editor or other platforms string fileName = "fallback.json"; #endif Adapty.SetFallback(fileName, (error) => { if (error != null) { Debug.LogError($"Failed to set fallback: {error}"); return; } // Fallback set successfully }); ``` Các tham số: | Tham số | Mô tả | |:-------------|:------------------------------------------------------------------------| | **fileName** | Chuỗi chứa tên của tệp cấu hình dự phòng. | --- # File: unity-localizations-and-locale-codes --- --- title: "Sử dụng localization và mã locale trong Unity SDK" description: "Tìm hiểu cách localize paywall trong ứng dụng Unity của bạn với Adapty SDK." --- ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một vài trường hợp 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 từng 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, do sự phức tạp này, điều quan trọng là bạn phải hiểu rõ mình đang gửi gì đến máy chủ của chúng tôi để nhận được localization đúng, và điều gì xảy ra tiếp theo — để bạn 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ể). ## Cách 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 kiếm localization tương ứng cho 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 thế bằng dấu gạch ngang (`-`) 2. Chúng tôi tìm kiếm localization có mã locale khớp hoàn toàn 3. Nếu không tìm thấy kết quả khớp, chúng tôi lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` cho `pt-br`) và tiếp tục tìm kiếm 4. Nếu vẫn không tìm thấy, chúng tôi 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ả như nhau. ## Triển khai localization: cách khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang tìm hiểu về localization, nhiều khả năng bạn đã làm việc với các file chuỗi đã được localize 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 cho từng localization. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như sau: ```csharp showLineNumbers // 1. Modify your localization files (e.g., using Unity's Localization package) /* en.json */ { "adapty_paywalls_locale": "en" } /* es.json */ { "adapty_paywalls_locale": "es" } /* pt-BR.json */ { "adapty_paywalls_locale": "pt-br" } // 2. Extract and use the locale code using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; using AdaptySDK; public class PaywallManager : MonoBehaviour { public async void FetchPaywall() { // Get the current locale from Unity's Localization system var locale = LocalizationSettings.SelectedLocale; var localeCode = GetAdaptyLocaleCode(locale); // Pass locale code to Adapty.GetPaywall or Adapty.GetPaywallForDefaultAudience method Adapty.GetPaywall("placement_id", localeCode, (paywall, error) => { if (error != null) { // handle the error return; } // Use the paywall }); } private string GetAdaptyLocaleCode(Locale locale) { // Convert Unity locale to Adapty format var localeIdentifier = locale.Identifier.Code; return localeIdentifier.ToLower().Replace('_', '-'); } } ``` Cách này giúp bạn kiểm soát hoàn toàn localization nào sẽ được trả về cho từng người dùng trong ứng dụng. ## Triển khai localization: cách khác \{#implementing-localizations-the-other-way\} Bạn có thể đạt 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 tường minh cho từng localization. Cách này có nghĩa là trích xuất mã locale từ các đối tượng khác mà nền tảng cung cấp, như sau: ```csharp showLineNumbers using UnityEngine; using System.Globalization; using AdaptySDK; public class PaywallManager : MonoBehaviour { public void FetchPaywall() { var localeCode = GetSystemLocaleCode(); // Pass locale code to Adapty.GetPaywall or Adapty.GetPaywallForDefaultAudience method Adapty.GetPaywall("placement_id", localeCode, (paywall, error) => { if (error != null) { // handle the error return; } // Use the paywall }); } private string GetSystemLocaleCode() { // Get the system's current culture var culture = CultureInfo.CurrentCulture; var languageCode = culture.TwoLetterISOLanguageName; var regionCode = culture.Name.Contains('-') ? culture.Name.Split('-')[1] : null; if (!string.IsNullOrEmpty(regionCode)) { return $"{languageCode}-{regionCode.ToLower()}"; } return languageCode; } } ``` Lưu ý rằng chúng tôi không khuyến nghị cách này vì một vài 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 hoặc dựa vào logic của Apple (vốn hoạt động tự động nếu dùng cách khuyến nghị với các file chuỗi đã localize), hoặc tự tái tạo lại logic đó. 2. Khó dự đoán chính xác máy chủ của Adapty sẽ nhận được gì. Chẳng hạn, trên iOS, có thể lấy được locale như `ar_OM@numbers='latn'` từ thiết bị và gửi đến máy chủ. Với yêu cầu này, bạn sẽ không nhận được localization `ar-om` như mong đợi, mà thay vào đó là `ar` — điều này có thể ngoài ý muốn. Nếu bạn vẫn quyết định dùng cách này — hãy đảm bảo đã kiểm tra tất cả các trường hợp sử dụng liên quan. --- # File: unity-troubleshoot-paywall-builder --- --- title: "Khắc phục sự cố Paywall Builder trong Unity SDK" description: "Khắc phục sự cố Paywall Builder trong Unity 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 Unity SDK. ## Lấy cấu hình paywall thất bại \{#getting-a-paywall-configuration-fails\} **Vấn đề**: Phương thức `CreateView` 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. <img src="/assets/shared/img/show-on-device.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Số lượt xem paywall quá lớn \{#the-paywall-view-number-is-too-big\} **Vấn đề**: Số lượt xem paywall hiển thị gấp đôi so với mong đợi. **Nguyên nhân**: Có thể bạn đang gọi `LogShowPaywall` trong code, khiến số lượt xem bị tính trùng nếu bạn đang dùng Paywall Builder. Với các paywall được thiết kế bằng Paywall Builder, analytics được theo dõi tự động nên 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 sự cố khác \{#other-issues\} **Vấn đề**: Bạn gặp phải các sự cố liên quan đến Paywall Builder không được đề cập ở trên. **Giải pháp**: Migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](unity-sdk-migration-guides) nếu cần. Nhiều sự cố đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: unity-quickstart-manual --- --- title: "Bật tính năng mua hàng trong paywall tùy chỉnh trong Unity SDK" description: "Tích hợp Adapty SDK vào các paywall Unity tùy chỉnh của bạn để bật tính năng in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Bạn có thể toàn quyền kiểm soát việc triển khai paywall, trong khi Adapty SDK lo việc 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 xây dựng 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 dùng [Adapty Paywall Builder](unity-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall bằng trình chỉnh sửa trực quan không cần code, Adapty 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 kỳ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywall**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được hiển thị. 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 chỉnh sửa sản phẩm, giá cả và ưu đãi mà không cần thay đổi code ứ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 đó gọi chúng theo 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 từng nhóm người dùng. Hãy đảm bảo bạn hiểu các khái niệm này ngay cả khi bạn đang làm việc với paywall tùy chỉnh. Về cơ bản, đây là cách bạn quản lý các sản phẩm bán trong ứng dụng. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy các sản phẩm. Để hiểu những gì bạn cần làm trên dashboard, hãy xem 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 có hoặc không có xác thực backend ở phía bạn. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo cách khác nhau. Đọc [hướng dẫn bắt đầu nhanh về xác định người dùng](unity-quickstart-identify) để hiểu các đặc điểm cụ thể 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 của bạn, bạn cần: 1. Lấy đối tượng `paywall` bằng cách truyền ID [placement](placements) 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`. ```csharp showLineNumbers using AdaptySDK; void LoadPaywall() { Adapty.GetPaywall("YOUR_PLACEMENT_ID", (paywall, error) => { if (error != null) { // Handle the error return; } Adapty.GetPaywallProducts(paywall, (products, productsError) => { if (productsError != null) { // Handle the error return; } // Use products to build your custom paywall UI }); }); } ``` ## Bước 2. Chấp nhận giao dịch mua \{#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. Thao tá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. ```csharp showLineNumbers using AdaptySDK; void PurchaseProduct(AdaptyPaywallProduct product) { Adapty.MakePurchase(product, (result, error) => { if (error != null) { // Handle the error return; } switch (result.Type) { case AdaptyPurchaseResultType.Success: var profile = result.Profile; // Purchase successful, profile updated break; case AdaptyPurchaseResultType.UserCancelled: // User canceled the purchase break; case AdaptyPurchaseResultType.Pending: // Purchase is pending (e.g., user will pay offline with cash) break; } }); } ``` ## Bước 3. Khôi phục giao dịch mua \{#step-3-restore-purchases\} Các cửa hàng ứng dụng yêu cầu tất cả ứng dụng có gói đăng ký phải cung cấp cách để người dùng khôi phục giao dịch mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Thao tác này sẽ đồng bộ lịch sử mua hàng của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```csharp showLineNumbers using AdaptySDK; void RestorePurchases() { Adapty.RestorePurchases((profile, error) => { if (error != null) { // Handle the error return; } // Restore successful, profile updated }); } ``` ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra giao dịch mua hàng của bạn trong [sandbox 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ử nghiệm từ paywall. Tiếp theo, [kiểm tra xem người dùng đã hoàn tất giao dịch mua chưa](unity-check-subscription-status) để xác đị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-unity --- --- title: "Lấy thông tin paywall và sản phẩm cho remote config paywalls trong Unity SDK" description: "Lấy thông tin paywall và sản phẩm trong Adapty Unity 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à 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 paywalls từ Paywall Builder và cấu hình của chúng](unity-get-pb-paywalls). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu lấy paywalls và sản phẩm trong ứng dụng mobile (nhấp để mở rộng)</summary> 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall](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-unity) trong ứng dụng mobile của bạn. </details> ## Lấy thông tin paywall \{#fetch-paywall-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng tại các placement cụ thể trong ứng dụng mobile. Để hiển thị các sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của mình bằng phương thức `getPaywall`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Các paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi hiện có có thể thay đổi bất cứ lúc nào. Ứng dụng của bạn phải xử lý các 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 sản phẩm, hãy hiển thị tất cả chúng mà không cần thay đổi code. ::: ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", "en", (paywall, error) => { if(error != null) { // handle the error return; } // paywall - the resulting object }); ``` | 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này phải là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p></p><p>Xem [Bản địa hóa và mã ngôn ngữ](unity-localizations-and-locale-codes) để biết thêm thông tin về mã ngôn ngữ và cách chúng tôi khuyến nghị sử dụng chúng.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>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 sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất, nhưng họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ như thế nào. Cache được cập nhật thường xuyên, vì vậy việc sử dụng nó trong phiên làm việc để tránh các yêu cầu mạng là an toàn.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywalls trong hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](unity-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywalls 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 paywalls trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet bị hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>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ề.</p><p></p><p>Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì hoạt động có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | Đừng hardcode ID sản phẩm! Vì các paywall được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được 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 của bạn 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 của bạn nên hiển thị cả 3 sản phẩm mà không cần thay đổi code. Thứ duy nhất bạn phải hardcode là placement ID. Tham số phản hồi: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.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ó: ```csharp showLineNumbers Adapty.GetPaywallProducts(paywall, (products, error) => { if(error != null) { // handle the error return; } // products - the requested products array }); ``` Tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall_product.html) gồm: định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn đăng ký, và một số thuộc tính khác. | Khi triển khai thiết kế paywall của riêng bạn, bạn có thể cần truy cập các thuộc tính này từ đối tượng [`AdaptyPaywallProduct`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall_product.html). Dưới đây là các thuộc tính được sử dụng phổ biến nhất, nhưng hãy tham khảo tài liệu được liên kết để biết đầy đủ chi tiết về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | Để hiển thị tiêu đề của sản phẩm, sử dụng `product.LocalizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Price** | Để hiển thị phiên bản bản địa hóa của giá, sử dụng `product.Price.LocalizedString`. Bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.Price.Amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, sử dụng `product.Price.CurrencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), sử dụng `product.Subscription?.LocalizedPeriod`. Bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ đăng ký theo lập trình, sử dụng `product.Subscription?.Period`. Từ đó bạn có thể truy cập enum `Unit` để lấy độ dài (tức là `AdaptySubscriptionPeriodUnit.Day`, `AdaptySubscriptionPeriodUnit.Week`, `AdaptySubscriptionPeriodUnit.Month`, `AdaptySubscriptionPeriodUnit.Year`, hoặc `AdaptySubscriptionPeriodUnit.Unknown`). Giá trị `NumberOfUnits` sẽ cho bạn biết số đơn vị chu kỳ. Ví dụ: với gói đăng ký hàng quý, bạn sẽ thấy `AdaptySubscriptionPeriodUnit.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 cho thấy gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.Subscription?.Offer?.Phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<br/>• `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ẽ là loại `AdaptyPaymentMode.FreeTrial`.<br/>• `Price`: Giá chiết khấu dưới dạng số. Với dùng thử miễn phí, hãy tìm giá trị `0` ở đây.<br/>• `LocalizedNumberOfPeriods`: một chuỗi được bản địa hóa theo ngôn ngữ của thiết bị, mô tả độ dài của ưu đãi. Ví dụ: ưu đãi dùng thử ba ngày hiển thị `"3 days"` trong trường này.<br/>• `SubscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết của 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ả.<br/>• `LocalizedSubscriptionPeriod`: Chu kỳ đăng ký được định dạng theo ngôn ngữ 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, các paywall được lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong những trường hợp bạn có nhiều đối tượng và paywall, và người dùng của bạn có kết nối internet yếu, việc 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 cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `GetPaywallForDefaultAudience`, phương thức này 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 Paywall](#fetch-paywall) ở trên. :::warning Hãy cân nhắc sử dụng `GetPaywall` thay vì `GetPaywallForDefaultAudience`, 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 chính xác. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", không còn khả năng nhắm mục tiêu dựa trên quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ lấy nhanh hơn có giá trị hơn so với những hạn chế này trong trường hợp sử dụng của bạn, hãy dùng `GetPaywallForDefaultAudience` như minh họa bên dưới. Nếu không, hãy dùng `GetPaywall` như mô tả [ở trên](#fetch-paywall). ::: ```csharp showLineNumbers Adapty.GetPaywallForDefaultAudience("YOUR_PLACEMENT_ID", "en", (paywall, error) => { if(error != null) { // handle the error return; } // paywall - the resulting object }); ``` 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của bản địa hóa paywall. Tham số này phải là mã ngôn ngữ gồm một hoặc hai thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>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 sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất, nhưng họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ như thế nào. Cache được cập nhật thường xuyên, vì vậy việc sử dụng nó trong phiên làm việc để tránh các yêu cầu mạng là an toàn.</p><p></p><p>Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywalls cục bộ trong hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và paywall dự phòng. Chúng tôi cũng sử dụng CDN để tải paywalls 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 paywalls trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet bị hạn chế.</p> | --- # File: present-remote-config-paywalls-unity --- --- title: "Hiển thị paywall được thiết kế bằng Remote Config trong Unity SDK" description: "Khám phá cách trình bày paywall dựa trên Remote Config trong Adapty Unity SDK để cá nhân hóa trải nghiệm người dùng." --- Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng nhìn thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn chủ động quyết định những gì được đưa vào và giao diện paywall trông như thế nào. Chúng tôi cung cấp phương thức để lấy cấu hình Remote Config, giúp bạn tự do trình bày paywall tùy chỉnh đã được cấu hình 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, truy cập thuộc tính `remoteConfig` và lấy các giá trị cần thiết. ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", (paywall, error) => { if (error != null) { // handle the error return; } // Access remote config dictionary var dictionary = paywall.RemoteConfig?.Dictionary; var headerText = dictionary?["header_text"] as string; // Or access raw JSON data var jsonData = paywall.RemoteConfig?.Data; }); ``` 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 lại thành một trang có giao diện đẹp mắt. Hãy đảm bảo thiết kế phù hợp với nhiều kích thước màn hình và hướng xoay khác nhau của điện thoại, 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-unity#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho funnel và A/B test. ::: Sau khi hoàn tất việc hiển thị paywall, hãy tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.MakePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.MakePurchase()`, hãy đọc [Thực hiện mua hàng](unity-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](unity-use-fallback-paywalls). Paywall 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 đó. ## Ghi lại sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi chúng tôi tự động thu thập dữ liệu về các giao dịch mua hàng, việc ghi lại lượt xem paywall cần có sự tham gia của bạn vì chỉ bạn mới biết khi nào người dùng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.LogShowPaywall(paywall)` — sự kiện này sẽ được phản ánh trong số liệu 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). ::: ```csharp showLineNumbers Adapty.LogShowPaywall(paywall, (error) => { // handle the error }); ``` Tham số của request: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:------------------------------------------------------------------| | **paywall** | bắt buộc | Một đối tượng [`AdaptyPaywall`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html). | --- # File: unity-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng với Unity SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng là bước quan trọng để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ hiển thị paywall là đủ để hỗ trợ mua hàng nếu bạn sử dụng [Paywall Builder](adapty-paywall-builder) để tuỳ chỉnh paywall của mình. Nếu bạn không dùng Paywall Builder, bạn cần sử dụng một phương thức riêng là `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung mong muốn. Phương thức này là cổng để người dùng tương tác với paywall và thực hiện giao dịch của họ. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm mà người dùng đang muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng. :::warning Lưu ý rằng ưu đãi giới thiệu chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập qua Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh điều kiện của người dùng để nhận ưu đãi giới thiệu trên iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Bỏ qua bước này có thể khiến ứng dụng của bạn bị từ chối khi phát hành. Hơn nữa, đ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 tất cấu hình ban đầu](quickstart) mà không bỏ sót 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 hàng. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang sử dụng [Paywall Builder](adapty-paywall-builder)?** Việc mua hàng được xử lý tự động — bạn có thể bỏ qua bước này. **Cần hướng dẫn từng bước?** Xem [hướng dẫn quickstart](unity-implement-paywalls-manually) để có hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```csharp showLineNumbers using AdaptySDK; void MakePurchase(AdaptyPaywallProduct product) { Adapty.MakePurchase(product, (result, error) => { switch (result.Type) { case AdaptyPurchaseResultType.Pending: // handle pending purchase break; case AdaptyPurchaseResultType.UserCancelled: // handle purchase cancellation break; case AdaptyPurchaseResultType.Success: var profile = result.Profile; // handle successfull purchase break; default: break; } }); } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:------------------------------------------------------------------------------------------------------| | **Product** | bắt buộc | Một đối tượng [`AdaptyPaywallProduct`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall_product.html) lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>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://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_profile.html) cung cấp thông tin đầy đủ về mức độ truy cập, gói đăng ký và các sản phẩm mua một lần của người dùng trong ứng dụng.</p><p>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.</p> | :::warning **Lưu ý:** nếu bạn vẫn đang dùng StoreKit của Apple phiên bản thấp hơn v2.0 và Adapty SDK phiên bản thấp hơn v2.9.0, bạn cần cung cấp [Apple App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple ngừng hỗ trợ. ::: ## 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 hoạt động phụ thuộc vào cửa hàng: - Đối với App Store, gói đăng ký được tự động cập nhật trong cùng nhóm gói đăng ký. Nếu người dùng mua gói đăng ký từ một nhóm trong khi đã có gói đăng ký từ nhóm khác, cả hai gói sẽ được kích hoạt đồng thời. - Đối với Google Play, gói đăng ký không được tự động cập nhật. Bạn cần quản lý việc chuyển đổi trong code ứng dụng như mô tả bên dưới. Để thay thế gói đăng ký bằng một gói khác trên Android, gọi phương thức `.makePurchase()` với tham số bổ sung: ```csharp showLineNumbers // Create subscription update parameters var subscriptionUpdateParams = new AdaptySubscriptionUpdateParameters( "old_product_id", // Product ID of the current subscription AdaptySubscriptionUpdateReplacementMode.WithTimeProration ); Adapty.MakePurchase(product, subscriptionUpdateParams, (profile, error) => { if(error != null) { // Handle the error return; } // successful cross-grade }); ``` Tham số yêu cầu bổ sung: | Tham số | Bắt buộc | Mô tả | | :--------------------------- | :------- |:-------------------------------------------------------------------------------------------------------| | **subscriptionUpdateParams** | bắt buộc | Một đối tượng [`AdaptySubscriptionUpdateParameters`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_subscription_update_parameters.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ị của Google về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Chế độ thay thế [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Lưu ý: phương thức này chỉ áp dụng cho việc nâng cấp gói đăng ký. Không hỗ trợ hạ cấp. - Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Việc thay đổi gói đăng ký thực sự chỉ xảy ra khi chu kỳ thanh toán hiện tại kết thúc. ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} <Details> <summary>Về offer code</summary> Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} :::warning Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. ::: Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác. </Details> Để hiển thị trang đổi mã trong ứng dụng của bạn: ```csharp showLineNumbers Adapty.PresentCodeRedemptionSheet((error) => { // handle the error }); ``` :::danger Theo 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ý prepaid plans (Android) \{#manage-prepaid-plans-android\} Nếu người dùng ứng dụng của bạn có thể mua [prepaid plans](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 [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) cho prepaid plans. ```csharp showLineNumbers title="Unity" using UnityEngine; using AdaptySDK; var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") .SetGoogleEnablePendingPrepaidPlans(true); Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); ``` --- # File: unity-restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng di động với Unity 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ỡ cài đặt rồi 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 phải trả tiề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 dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`: ```csharp showLineNumbers Adapty.RestorePurchases((profile, error) => { if (error != null) { // handle the error return; } var accessLevel = profile.AccessLevels["YOUR_ACCESS_LEVEL"]; if (accessLevel != null && accessLevel.IsActive) { // restore access } }); ``` Tham số trả về: | Tham số | Mô tả | |---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <p>Một đối tượng [`AdaptyProfile`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_profile.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.</p><p>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.</p> | :::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-unity --- --- title: "Implement Observer mode in Unity SDK" description: "Triển khai observer mode trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong Unity SDK." --- Nếu bạn đã có hạ tầng mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp analytics nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics. Nếu điều này đáp ứng nhu cầu của bạn, bạn chỉ cần: 1. Bật tính năng này khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. Làm theo hướng dẫn cài đặt cho [Unity](sdk-installation-unity#activate-adapty-module-of-adapty-sdk). 2. [Báo cáo giao dịch](report-transactions-observer-mode-unity) từ hạ tầng mua hàng hiện có của bạn lên Adapty. ### Cài đặt Observer mode \{#observer-mode-setup\} Bật Observer mode nếu bạn tự xử lý việc mua hàng và trạng thái gói đăng ký, đồng thời sử dụng Adapty để gửi các sự kiện gói đăng ký và analytics. :::important Khi chạy ở Observer mode, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều đó. ::: ```csharp showLineNumbers title="C#" using UnityEngine; using AdaptySDK; public class AdaptyListener : MonoBehaviour, AdaptyEventListener { void Start() { DontDestroyOnLoad(this.gameObject); Adapty.SetEventListener(this); var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetObserverMode(true); // Enable observer mode Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); } public void OnLoadLatestProfile(AdaptyProfile profile) { } public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } public void OnInstallationDetailsFail(AdaptyError error) { } } ``` Tham số: | Tham số | Mô tả | |--------------|-------------------------------------------------------------------------------------------------------------| | observerMode | Giá trị boolean kiểm soát [Observer mode](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng 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ố cấu hình khi ở Observer mode. Ngoài các bước trên, bạn cần thực hiện: 1. Hiển thị paywall như thường lệ đối với [remote config paywalls](present-remote-config-paywalls-unity). 3. [Liên kết paywall](report-transactions-observer-mode-unity) với các giao dịch mua hàng. --- # File: report-transactions-observer-mode-unity --- --- title: "Báo cáo giao dịch trong Observer Mode trong Unity SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trong Unity SDK." --- <Tabs groupId="sdk-version" queryString> <TabItem value="current" label="Adapty SDK v3.4+ (current)" default> Trong Observer Mode, Adapty SDK không thể tự theo dõi các giao dịch mua hàng được thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng của mình. Việc thiết lập điều này **trước** khi phát hành ứng dụng là rất quan trọng để tránh lỗi trong analytics. Sử dụng `reportTransaction` để báo cáo rõ ràng từng giao dịch để Adapty nhận diện. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `ReportTransaction`, Adapty sẽ không nhận diện được giao dịch, giao dịch sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy cung cấp `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. ```csharp showLineNumbers Adapty.ReportTransaction( "YOUR_TRANSACTION_ID", "PAYWALL_VARIATION_ID", // optional (error) => { // handle the error }); ``` Các tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | transactionId | bắt buộc | <ul><li> Đối với iOS: Mã định danh của giao dịch.</li><li> Đối với Android: Mã định danh dạng chuỗi `purchase.getOrderId` của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.</li></ul> | | 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://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html). | </TabItem> <TabItem value="old" label="Adapty SDK 3.3.x (legacy)" default> Trong Observer Mode, Adapty SDK không thể tự theo dõi các giao dịch mua hàng được thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng hoặc khôi phục chúng. Việc thiết lập điều này **trước** khi phát hành ứng dụng là rất quan trọng để tránh lỗi 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 diện được 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 diện được giao dịch, giao dịch sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy cung cấp `PAYWALL_VARIATION_ID` 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. ```csharp showLineNumbers // every time when calling transasction.finish() #if UNITY_ANDROID && !UNITY_EDITOR Adapty.RestorePurchases((profile, error) => { // handle the error }); #endif Adapty.ReportTransaction( "YOUR_TRANSACTION_ID", "PAYWALL_VARIATION_ID", // optional (error) => { // handle the error }); ``` Các tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | transactionId | bắt buộc | <ul><li> Đối với iOS, StoreKit 1: đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).</li><li> Đối với iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).</li><li> Đối với Android: Mã định danh dạng chuỗi (`purchase.getOrderId`) của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.</li></ul> | | 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://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html). | </TabItem> <TabItem value="old2" label="Adapty SDK up to 3.2.x (legacy)" default> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS" default> **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, nên không cần báo cáo thủ công. - Phiên bản 3.2 không hỗ trợ Observer Mode. </TabItem> <TabItem value="kotlin" label="Android and Android-based cross-platforms" default> **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 mã di động](unity-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 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. ::: </TabItem> </Tabs> **Liên kết paywall với giao dịch** Adapty SDK không thể xác định nguồn gốc của 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 với paywall tương ứng trong mã ứng dụng di động của bạn. Điều này rất quan trọng cần thực hiện đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến lỗi trong analytics. ```csharp Adapty.SetVariationForTransaction("<variationId>", "<transactionId>", (error) => { if(error != null) { // handle the error return; } // successful binding }); ``` | Tham số | Bắt buộc | Mô tả | | ------------------------------------------------------ | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | transactionId | bắt buộc | <p>Đối với iOS, StoreKit 1: đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).</p><p>Đối với iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).</p><p>Đối với Android: Mã định danh dạng chuỗi (purchase.getOrderId của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.</p> | | variationId | bắt buộc | 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://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_paywall.html). | </TabItem> </Tabs> --- # File: unity-troubleshoot-purchases --- --- title: "Xử lý sự cố mua hàng trong Unity SDK" description: "Xử lý sự cố mua hàng trong Unity 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 Unity 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**: Thường là do thiếu hoặc sai cấu hình Google Play Store. **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 bị gọi hai lần \{#makepurchase-is-invoked-twice\} **Vấn đề**: Phương thức `makePurchase` bị gọi nhiều lần cho cùng một giao dịch mua. **Nguyên nhân**: Thường xảy ra khi flow mua hàng bị kích hoạt nhiều lần do vấn đề quản lý trạng thái UI 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 trong chế độ observer \{#adaptyerrorcantmakepayments-in-observer-mode\} **Vấn đề**: Bạn nhận được `AdaptyError.cantMakePayments` khi dùng `makePurchase` trong chế độ observer. **Nguyên nhân**: Trong chế độ observer, bạn phải tự xử lý việc mua hàng ở phía mình, không dùng phương thức `makePurchase` của Adapty. **Giải pháp**: Nếu bạn dùng `makePurchase` để xử lý mua hàng, hãy tắt chế độ observer. Bạn cần chọn một trong hai: dùng `makePurchase` hoặc tự xử lý mua hàng trong chế độ observer. Xem [Triển khai chế độ Observer](implement-observer-mode-unity) để 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 của 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**: Thường liên quan đến vấn đề kiểm thử 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ề purchase completion handler liên quan đến sandbox. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn gặp các sự cố liên quan đến mua hàng khác không được đề cập ở trên. **Giải pháp**: Migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](unity-sdk-migration-guides) nếu cần. Nhiều vấn đề đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: unity-identifying-users --- --- title: "Xác định người dùng trong Unity SDK" description: "Tìm hiểu cách xác định người dùng trong ứng dụng Unity của bạn với Adapty SDK." --- 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ông tin này sẽ được gửi đến tất cả các tích hợp. ### Đặt customer user ID khi cấu hình \{#setting-customer-user-id-on-configuration\} Nếu bạn có user ID trong quá trình cấu hình, chỉ cần truyền nó dưới dạng tham số `customerUserId` vào phương thức `.activate()`: ```csharp showLineNumbers using UnityEngine; using AdaptySDK; var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") .SetCustomerUserId("YOUR_USER_ID"); Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); ``` :::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 không có user ID trong cấu hình SDK, bạn có thể đặt nó sau bất kỳ lúc nào bằng phương thức `.identify()`. Các trường hợp phổ biến nhất khi 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. ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { if(error == null) { // successful identify } }); ``` 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ủ của Adapty đã có thông tin về người dùng đó. Trong những trường hợp này, Adapty SDK sẽ tự động chuyển sang làm việc với người dùng mới. Nếu bạn đã truyền bất kỳ 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 nên gửi lại dữ liệu đó cho người dùng đã được xác định. Ngoài ra, điều quan trọng cần lưu ý là bạn nên yêu cầu 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 kỳ lúc nào bằng cách gọi phương thức `.logout()`: ```csharp showLineNumbers Adapty.Logout((error) => { if(error == null) { // successful logout } }); ``` Sau đó bạn có thể đăng nhập lại 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 gắn token này với mọi giao dịch, để backend của bạn có thể khớp dữ liệu App Store với người dùng của mình. Hãy sử dụng một UUID ổn định được tạo cho mỗi người dùng và tái sử dụng nó cho cùng một tài khoản trên các thiết bị khác nhau. Điều này đảm bảo rằng các giao dịch mua và thông báo App Store được liên kết chính xác. Bạn có thể đặt token theo hai cách – trong quá trình 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. ::: ```csharp showLineNumbers title="Unity" using UnityEngine; using AdaptySDK; using System; // During configuration: var appAccountToken = new Guid("YOUR_APP_ACCOUNT_TOKEN"); var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") .SetCustomerUserId("YOUR_USER_ID", appAccountToken); Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); // Or when identifying users Adapty.Identify("YOUR_USER_ID", appAccountToken, (error) => { if (error == null) { // successful identify } }); ``` ## Đặ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ất định nhằm tăng cường quyền riêng tư và bảo mật của 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 để ngăn chặn 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ế. ```csharp showLineNumbers title="Unity" using UnityEngine; using AdaptySDK; // During configuration: var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") .SetCustomerUserId("YOUR_USER_ID", null, "YOUR_OBFUSCATED_ACCOUNT_ID"); Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); // Or when identifying users Adapty.Identify("YOUR_USER_ID", null, "YOUR_OBFUSCATED_ACCOUNT_ID", (error) => { if (error == null) { // successful identify } }); ``` ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: unity-setting-user-attributes --- --- title: "Đặt thuộc tính người dùng trong Unity SDK" description: "Tìm hiểu cách cập nhật thuộc tính người dùng và dữ liệu hồ sơ trong ứng dụng Unity của bạn với Adapty SDK." --- Bạn có thể đặt 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 xem chúng trong CRM. ### Đặt thuộc tính người dùng \{#setting-user-attributes\} Để đặt thuộc tính người dùng, hãy gọi phương thức `.updateProfile()`: ```csharp showLineNumbers var builder = new Adapty.ProfileParameters.Builder() .SetFirstName("John") .SetLastName("Appleseed") .SetBirthday(new DateTime(1970, 1, 3)) .SetGender(ProfileGender.Female) .SetEmail("example@adapty.io"); Adapty.UpdateProfile(builder.Build(), (error) => { if(error != nil) { // handle the error } }); ``` Lưu ý rằng các thuộc tính bạn đã đặt trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các key được phép \{#the-allowed-keys-list\} Các key `<Key>` được phép của `AdaptyProfileParameters.Builder` và các giá trị `<Value>` tương ứng được liệt kê bên dưới: | Key | Value | |---|-----| | <p>email</p><p>phoneNumber</p><p>firstName</p><p>lastName</p> | String | | gender | Enum, các giá trị được phép là: `female`, `male`, `other` | | birthday | Date | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể đặt 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 ngôn ngữ, đó có thể 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. ```csharp showLineNumbers try { builder = builder.SetCustomStringAttribute("string_key", "string_value"); builder = builder.SetCustomDoubleAttribute("double_key", 123.0f); } catch (Exception e) { // handle the exception } ``` Để xóa một key đã có, hãy dùng phương thức `.withRemoved(customAttributeForKey:)`: ```csharp showLineNumbers try { builder = builder.RemoveCustomAttribute("key_to_remove"); } catch (Exception e) { // handle the exception } ``` Đô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 phải là mới nhất, vì thuộc tính người dùng có thể được gửi từ các thiết bị khác nhau bất cứ lúc nào, nên các thuộc tính trên server có thể đã thay đổi kể từ lần đồng bộ hóa 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 không quá 50 ký tự. --- # File: unity-listen-subscription-changes --- --- title: "Kiểm tra trạng thái gói đăng ký trong Unity SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký người dùng trong Adapty để cải thiện việc giữ chân khách hàng trong ứng dụng Unity của bạn." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải tự chèn 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. <details> <summary>Trước khi bắt đầu kiểm tra trạng thái gói đăng ký (Nhấp để mở rộng)</summary> - 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) </details> ## 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://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_profile.html). Chúng tôi khuyến nghị lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](unity-identifying-users#setting-customer-user-id-on-configuration), rồi cập nhật lại mỗi khi có thay đổi. Bằng cách này, bạn có thể sử dụng đối tượng hồ sơ mà không cần gửi yêu cầu lặp đi lặp lại. Để nhận thông báo khi hồ sơ người dùng được cập nhật, hãy lắng nghe các thay đổi hồ sơ như mô tả trong phần [Lắng nghe cập nhật trạng thái gói đăng ký](#listening-for-subscription-status-updates) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ máy chủ \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ máy chủ, sử dụng phương thức `.GetProfile()`: ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access }); ``` Tham số phản hồi: | Tham số | Mô tả | | --------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Profile | <p>Một đối tượng [AdaptyProfile](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_profile.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ơ để xác định người dùng có quyền truy cập premium vào ứng dụng hay không.</p><p></p><p>Phương thức `.getProfile` cung cấp kết quả cập nhật nhất vì nó luôn cố truy vấn API. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ máy chủ, dữ liệu từ cache sẽ được trả về. Cũng cần lưu ý rằng Adapty SDK thường xuyên cập nhật cache `AdaptyProfile` để giữ thông tin này luôn ở trạng thái mới nhất.</p> | Phương thức `.getProfile()` cung cấp cho bạn hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Bạn có thể có nhiều mức độ truy cập cho mỗi ứng dụng. Ví dụ: nếu bạn có ứng dụng báo và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết các trường hợp, bạn chỉ cần một mức độ truy cập — khi đó, bạn có thể sử 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 "premium" mặc định: ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // "premium" is an identifier of default access level var accessLevel = profile.AccessLevels["premium"]; if (accessLevel != null && accessLevel.IsActive) { // grant access to premium features } }); ``` ### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận thông báo từ Adapty, bạn cần thực hiện một số cấu hình bổ sung: ```csharp showLineNumbers // Extend `AdaptyEventListener ` with `OnLoadLatestProfile ` method: public class AdaptyListener : MonoBehaviour, AdaptyEventListener { public void OnLoadLatestProfile(AdaptyProfile 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 gói đăng ký \{#subscription-status-cache\} Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi máy chủ không khả dụng, dữ liệu được lưu trong cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ. Tuy nhiên, cần lưu ý rằng không thể truy vấn dữ liệu trực tiếp từ cache. SDK định kỳ truy vấn máy chủ mỗi phút để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ. 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 nó luôn nhất quán với máy chủ. --- # File: unity-deal-with-att --- --- title: "Xử lý ATT trong Unity SDK" description: "Bắt đầu với Adapty trên Unity để đơ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 tới 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/) tới Adapty. ```csharp showLineNumbers var builder = new Adapty.ProfileParameters.Builder() .SetAppTrackingTransparencyStatus(IOSAppTrackingTransparencyStatus.Authorized); Adapty.UpdateProfile(builder.Build(), (error) => { if(error != null) { // handle the error } }); ``` :::warning Chúng tôi khuyến nghị bạn gửi giá trị này càng sớm càng tốt khi nó thay đổi. Chỉ như vậy, dữ liệu mới được gửi kịp thời tới các tích hợp mà bạn đã cấu hình. ::: --- # File: kids-mode-unity --- --- title: "Chế độ Kids Mode trong Unity SDK" description: "Dễ dàng bật Kids Mode để 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 Unity SDK." --- Nếu ứng dụng Unity 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 bạn đang sử dụng Adapty SDK, một vài bước đơn giản sẽ giúp bạn cấu hình SDK để đáp ứng các chính sách này và vượt qua quá trình xét duyệt của cửa hàng ứng dụng. ## Yêu cầu cần thực hiện? \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt tính năng thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị bạn cẩn thận khi sử dụng customer user ID. User ID theo định dạng `<FirstName.LastName>` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc sử dụng email. Đối với Kids Mode, cách tốt nhất là sử dụng các định danh ngẫu nhiên hoặc đã được ẩn danh hóa (ví dụ: ID đã hash hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ. ## Bật Kids Mode \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\} Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để thực hiện, hãy vào [App settings](https://app.adapty.io/settings/general) và nhấp vào **Disable IP address collection** trong mục **Collect users' IP address**. ### Cập nhật trong code ứng dụng di động của bạn \{#updates-in-your-mobile-app-code\} Hỗ trợ Kids Mode trong Unity sắp ra mắt! Hiện tại, bạn có thể tham khảo các hướng dẫn dành riêng cho từng nền tảng: - [Kids Mode trong iOS SDK](kids-mode) để cấu hình iOS - [Kids Mode trong Android SDK](kids-mode-android) để cấu hình Android --- # File: unity-get-onboardings --- --- title: "Lấy onboarding trong Unity SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho Unity." --- Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Unity của mình. Bước đầu tiên trong quá trình này là lấy onboarding gắn với placement cùng cấu hình view 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 Unity SDK](sdk-installation-unity) phiên bản 3.14.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 và tạo view \{#fetch-onboarding-and-create-view\} Khi bạn tạo một [onboarding](onboardings) bằng builder no-code của chúng tôi, nó được lưu trữ dưới dạng một container 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 hiển thị, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc đầu vào từ form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần triển khai tính năng theo dõi view riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để hình ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy một onboarding, sử dụng phương thức `GetOnboarding`: ```csharp showLineNumbers Adapty.GetOnboarding("YOUR_PLACEMENT_ID", (onboarding, error) => { if (error != null) { // handle the error return; } // the requested onboarding }); ``` 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh ngôn ngữ bản địa hóa của onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân tách bằng ký tự gạch ngang (**-**). Thẻ phụ đầu tiên là cho ngôn ngữ, thẻ thứ hai là cho khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p><p>Xem [Localizations and locale codes](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.</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu được cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>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 sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ có không ổn định đến đâu. Cache được cập nhật thường xuyên, vì vậy việc sử dụng nó trong suốt phiên để tránh các yêu cầu mạng là an toàn.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để lấy onboarding nhanh hơn và một server dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của các onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả trong trường hợp kết nối internet hạn chế.</p> | | **loadTimeout** | mặc định: 5 giây | <p>Giá trị này giới hạn timeout cho phương thức này. Nếu timeout bị vượt quá, dữ liệu cache hoặc fallback cục bộ sẽ được trả về.</p><p>Lưu ý rằng trong những trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.</p> | Tham số phản hồi: | Tham số | Mô tả | |:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://unity.adapty.io/class_adapty_s_d_k_1_1_adapty_onboarding.html) với: định danh và cấu hình onboarding, Remote Config, và một số thuộc tính khác. | Sau khi lấy onboarding, hãy gọi phương thức `CreateOnboardingView`. :::warning Kết quả của phương thức `CreateOnboardingView` chỉ có thể được sử dụng một lần. Nếu bạn cần sử dụng lại, hãy gọi lại phương thức `CreateOnboardingView`. Gọi nó hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```csharp showLineNumbers AdaptyUI.CreateOnboardingView(onboarding, (view, error) => { // handle the result }); ``` Tham số: | Tham số | Bắt buộc | Mô tả | |:---------------| :------------- |:-----------------------------------------------------------------------------| | **onboarding** | bắt buộc | Một đối tượng `AdaptyOnboarding` để lấy view cho onboarding mong muốn. | | **externalUrlsPresentation** | <p>tùy chọn</p><p>mặc định: `InAppBrowser`</p> | <p>Kiểm soát cách các liên kết trong onboarding được mở. Các tùy chọn có sẵn:</p><p>- `AdaptyWebPresentation.InAppBrowser` - Mở liên kết trong trình duyệt trong ứng dụng (mặc định)</p><p>- `AdaptyWebPresentation.ExternalBrowser` - Mở liên kết trong trình duyệt ngoài của thiết bị</p><p>Xem [Customize how links open in onboardings](unity-present-onboardings#customize-how-links-open-in-onboardings) để biết ví dụ sử dụng.</p> | Sau khi tải thành công onboarding và cấu hình view của nó, bạn có thể [hiển thị nó trong ứng dụng di động của mình](unity-present-onboardings). ## Tăng tốc lấy onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong những trường hợp bạn có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào cả. Để giải quyết điều này, bạn có thể sử dụng phương thức `GetOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như được mô tả chi tiết trong phần [Lấy Onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `GetOnboarding` thay vì `GetOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể tạo 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 chính xác. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ việc nhắm mục tiêu dựa trên quốc gia, attribution hoặc thuộc tính tùy chỉnh. Nếu việc lấy nhanh hơn vượt trội so với những nhược điểm này cho 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). ::: ```csharp showLineNumbers Adapty.GetOnboardingForDefaultAudience("YOUR_PLACEMENT_ID", (onboarding, error) => { if (error != null) { // handle the error return; } // the requested onboarding }); ``` 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** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh ngôn ngữ bản địa hóa của onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân tách bằng ký tự gạch ngang (**-**). Thẻ phụ đầu tiên là cho ngôn ngữ, thẻ thứ hai là cho khu vực.</p><p></p><p>Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).</p> | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | <p>Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu được cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.</p><p></p><p>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 sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ có không ổn định đến đâu. Cache được cập nhật thường xuyên, vì vậy việc sử dụng nó trong suốt phiên để tránh các yêu cầu mạng là an toàn.</p><p></p><p>Lưu ý rằng cache vẫn còn nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ onboarding cục bộ ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để lấy onboarding nhanh hơn và một server dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của các onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả trong trường hợp kết nối internet hạn chế.</p> | --- # File: unity-present-onboardings --- --- title: "Hiển thị onboarding trong Unity 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 onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code Unity để hiển thị cho người dùng. Onboarding đó đã chứa đầy đủ thông tin về những gì sẽ hiển thị và cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Unity SDK](sdk-installation-unity) phiên bản 3.14.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Để hiển thị onboarding, sử dụng phương thức `view.Present()` trên `view` đượ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 paywall, hãy gọi `CreateOnboardingView` thêm một lần để tạo instance `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`. ::: ```csharp showLineNumbers title="Unity" view.Present((presentError) => { if (presentError != null) { // 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 các giá trị `AdaptyUIIOSPresentationStyle.FullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.PageSheet`. ```csharp showLineNumbers title="Unity" view.Present(AdaptyUIIOSPresentationStyle.PageSheet, (error) => { // handle the error }); ``` ## Tùy chỉnh cách mở liên kết trong onboarding \{#customize-how-links-open-in-onboardings\} :::important Tính năng tùy chỉnh cách mở liên kết trong onboarding được hỗ trợ từ Adapty SDK v3.15 trở lên. ::: Theo mặc định, các liên kết trong onboarding mở bằng trình duyệt trong ứng dụng, 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 mà không cần chuyển sang ứng dụng khác. Để mở liên kết bằng trình duyệt ngoài thay thế, hãy truyền `AdaptyWebPresentation.ExternalBrowser` vào phương thức `CreateOnboardingView`: ```csharp showLineNumbers title="Unity" AdaptyUI.CreateOnboardingView( onboarding, AdaptyWebPresentation.ExternalBrowser, // default — InAppBrowser (view, error) => { if (error != null) { // handle the error return; } // present the onboarding view view.Present((presentError) => { if (presentError != null) { // handle the error } }); } ); ``` Các tùy chọn có sẵn: - `AdaptyWebPresentation.InAppBrowser` - Mở liên kết bằng trình duyệt trong ứng dụng (mặc định) - `AdaptyWebPresentation.ExternalBrowser` - Mở liên kết bằng trình duyệt ngoài của thiết bị --- # File: unity-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong Unity SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong Unity bằng Adapty." --- Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Unity SDK](sdk-installation-unity) phiên bản 3.14.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể phản hồi. Hãy xem cách xử lý các sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình diễn ra trên màn hình onboarding trong ứng dụng Unity của bạn, hãy triển khai interface `AdaptyOnboardingsEventsListener`. ## Hành động tùy chỉnh \{#custom-actions\} Trong builder, bạn có thể thêm hành động **custom** cho một nút và gán cho nó một ID. <img src="/assets/shared/img/ios-events-1.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. Ví dụ: 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 `OnboardingViewOnCustomAction` sẽ được kích hoạt với tham số `actionId` là **Action ID** từ builder. Bạn có thể tự tạo ID theo ý muốn, ví dụ như "allowNotifications". Để xử lý các sự kiện onboarding, hãy triển khai interface `AdaptyOnboardingsEventsListener`: ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { void Start() { Adapty.SetOnboardingsEventsListener(this); } public void OnboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, string actionId ) { if (actionId == "allowNotifications") { // request notification permissions } } public void OnboardingViewDidFailWithError( AdaptyUIOnboardingView view, AdaptyError error ) { // handle errors } // Implement other required interface methods (see examples below) } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ``` </Details> ## Đóng onboarding \{#closing-onboarding\} Onboarding được coi là đã đóng khi người dùng nhấn vào một nút có hành động **Close** được gán. <img src="/assets/shared/img/ios-events-2.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> :::important Lưu ý rằng bạn cần tự quản lý điều 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 đó. ::: Triển khai phương thức `OnboardingViewOnCloseAction` trong class của bạn: ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { public void OnboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, string actionId ) { view.Dismiss((error) => { if (error != null) { // handle the error } }); } // ... other interface methods } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ``` </Details> ## Mở paywall \{#opening-a-paywall\} :::tip Hãy xử lý sự kiện này để mở paywall nếu bạn muốn hiển thị 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ý [`OnboardingViewOnCloseAction`](#closing-onboarding) và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách làm liền mạch nhất khi làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall. Như vậy, sau sự kiện `OnboardingViewOnPaywallAction`, bạn có thể dùng placement ID để lấy và mở paywall ngay lập tức. Lưu ý rằng, trên iOS, chỉ có thể hiển thị một view (paywall hoặc onboarding) trên màn hình tại một thời điểm. Nếu bạn hiển thị paywall chồng lên onboarding, bạn không thể kiểm soát onboarding ở nền theo cách lập trình. Nếu cố dismiss onboarding, paywall sẽ bị đóng thay vào đó, khiến onboarding vẫn còn hiển thị. Để tránh điều này, hãy luôn dismiss view onboarding trước khi hiển thị paywall. ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { public void OnboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, string actionId ) { // Dismiss onboarding before presenting paywall view.Dismiss((dismissError) => { if (dismissError != null) { // handle the error return; } Adapty.GetPaywall(actionId, (paywall, error) => { if (error != null) { // handle the error return; } AdaptyUI.CreatePaywallView(paywall, (paywallView, createError) => { if (createError != null) { // handle the error return; } paywallView.Present((presentError) => { if (presentError != null) { // handle the error } }); }); }); }); } // ... other interface methods } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ``` </Details> ## Hoàn tất tải onboarding \{#finishing-loading-onboarding\} Khi onboarding tải xong, hãy triển khai phương thức `OnboardingViewDidFinishLoading`: ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { public void OnboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta ) { // handle loading completion } // ... other interface methods } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ``` </Details> ## Theo dõi điều hướng \{#tracking-navigation\} Phương thức `OnboardingViewOnAnalyticsEvent` được gọi khi các sự kiện analytics khác nhau xảy ra trong flow onboarding. Đối tượng `analyticsEvent` có thể là một trong các kiểu sau: | Kiểu | Mô tả | |------------|-------------| | `AdaptyOnboardingsAnalyticsEventOnboardingStarted` | Khi onboarding đã được tải | | `AdaptyOnboardingsAnalyticsEventScreenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `AdaptyOnboardingsAnalyticsEventScreenCompleted` | Khi một màn hình hoàn tất. Bao gồm `ElementId` tùy chọn (định danh của phần tử đã hoàn tất) 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. | | `AdaptyOnboardingsAnalyticsEventSecondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `AdaptyOnboardingsAnalyticsEventUserEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `AdaptyOnboardingsAnalyticsEventOnboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, [hãy gán ID `final` cho màn hình cuối cùng](design-onboarding). | | `AdaptyOnboardingsAnalyticsEventUnknown` | Cho bất kỳ loại sự kiện nào không được nhận dạng. 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 | Dưới đây là ví dụ về cách sử dụng sự kiện analytics để theo dõi: ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { public void OnboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent analyticsEvent ) { switch (analyticsEvent) { case AdaptyOnboardingsAnalyticsEventOnboardingStarted: // track onboarding start TrackEvent("onboarding_started", meta); break; case AdaptyOnboardingsAnalyticsEventScreenPresented: // track screen presentation TrackEvent("screen_presented", meta); break; case AdaptyOnboardingsAnalyticsEventScreenCompleted screenCompleted: // track screen completion with user response TrackEvent("screen_completed", meta, screenCompleted.ElementId, screenCompleted.Reply); break; case AdaptyOnboardingsAnalyticsEventOnboardingCompleted: // track successful onboarding completion TrackEvent("onboarding_completed", meta); break; case AdaptyOnboardingsAnalyticsEventUnknown unknownEvent: // handle unknown events TrackEvent(unknownEvent.Name, meta); break; // handle other cases as needed } } // ... other interface methods } ``` :::note Phương thức `TrackEvent` là một placeholder mà bạn cần tự triển khai để gửi analytics đến dịch vụ analytics mà bạn muốn sử dụng. ::: <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```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 } } ``` </Details> --- # File: unity-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong Unity SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng Unity của bạn với Adapty SDK." --- Khi người dùng trả lời câu hỏi trong quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `OnboardingViewOnStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Triển khai phương thức `OnboardingViewOnStateUpdatedAction` trong class của bạn: ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { public void OnboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, string elementId, AdaptyOnboardingsStateUpdatedParams @params ) { switch (@params) { case AdaptyOnboardingsSelectParams selectParams: // handle single selection break; case AdaptyOnboardingsMultiSelectParams multiSelectParams: // handle multiple selections break; case AdaptyOnboardingsInputParams inputParams: // handle text input break; case AdaptyOnboardingsDatePickerParams datePickerParams: // handle date selection break; } } // ... other interface methods } ``` Các tham số bao gồm: | Tham số | Mô tả | |----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `elementId` | Mã định danh duy nhất cho phần tử nhập liệu. Bạn có thể dùng nó để liên kết câu hỏi với câu trả lời khi lưu dữ liệu. | | `@params` | Đối tượng dữ liệu đầu vào của người dùng. Có thể là một trong các kiểu sau. | | `AdaptyOnboardingsSelectParams` | Chọn một tùy chọn. Chứa `Id`, `Value`, `Label` | | `AdaptyOnboardingsMultiSelectParams` | Chọn nhiều tùy chọn. Chứa danh sách `Params` (mỗi phần tử có `Id`, `Value`, `Label`)<br/>• `input`: Đối tượng với `type`, `value`<br/>• `datePicker`: Đối tượng với `day`, `month`, `year` | | `AdaptyOnboardingsInputParams` | Trường nhập văn bản. Chứa `Input` có thể là `AdaptyOnboardingsTextInput`, `AdaptyOnboardingsEmailInput`, hoặc `AdaptyOnboardingsNumberInput` | | `AdaptyOnboardingsDatePickerParams` | Chọn ngày tháng. Chứa `Day`, `Month`, `Year` (có thể null) | <Details> <summary>Ví dụ dữ liệu đã lưu (có thể khác trong triển khai của bạn)</summary> ```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 } } } ``` </Details> ## Các trường hợp sử dụng \{#use-cases\} ### Bổ sung thông tin vào hồ sơ người dùng \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi lại cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](unity-setting-user-attributes) với dữ liệu đó khi xử lý hành động. 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ị đó làm tên của họ. 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: ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { public void OnboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, string elementId, AdaptyOnboardingsStateUpdatedParams @params ) { if (@params is AdaptyOnboardingsInputParams inputParams) { var builder = new AdaptyProfileParameters.Builder(); switch (elementId) { case "name": if (inputParams.Input is AdaptyOnboardingsTextInput textInput) { builder.SetFirstName(textInput.Value); } break; case "email": if (inputParams.Input is AdaptyOnboardingsEmailInput emailInput) { builder.SetEmail(emailInput.Value); } break; } Adapty.UpdateProfile(builder.Build(), (error) => { if (error != null) { // handle the error } }); } } // ... other interface methods } ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Bằng cách sử dụng quiz 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 thể thao của họ và hiển thị các CTA và sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) trong onboarding builder và gán các ID có ý nghĩa cho các tùy chọn. 2. Xử lý các phản hồi từ quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](unity-setting-user-attributes) cho người dùng. ```csharp showLineNumbers title="Unity" public class OnboardingManager : MonoBehaviour, AdaptyOnboardingsEventsListener { public void OnboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, string elementId, AdaptyOnboardingsStateUpdatedParams @params ) { if (@params is AdaptyOnboardingsSelectParams selectParams) { var builder = new AdaptyProfileParameters.Builder(); switch (elementId) { case "experience": // set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.SetCustomStringAttribute("experience", selectParams.Value); break; } Adapty.UpdateProfile(builder.Build(), (error) => { if (error != null) { // handle the error } }); } } // ... other interface methods } ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho từng phân khúc bạn đã tạo. 5. [Hiển thị paywall](unity-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 hành động của nút đó](unity-handling-onboarding-events#opening-a-paywall). --- # File: unity-sdk-call-order --- --- title: "Thứ tự gọi trong Unity 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 tất 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 callback hoàn tất được kích hoạt, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `Activate()` sẽ thất bại với lỗi [`#2002 notActivated`](unity-handle-errors#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 của người dùng cho đến khi callback của `Identify` được kích hoạt. Các lệnh gọi chạy đua với nó sẽ thất bại với lỗi [`#3006 profileWasChanged`](unity-handle-errors#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à quyền sở hữu cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `Identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo cùng một quy tắc. 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ẽ rơi vào hồ sơ người dùng ẩn danh tạm thời và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Để biết thông tin cụ thể về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai yếu tố: thời điểm bạn biết customer user ID, và liệu 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 của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn có customer user ID ngay khi khởi chạy ứng dụng, hãy truyền trực tiếp vào `Activate()` (bước 2a). Lộ trình này không bao giờ tạo hồ sơ người dùng ẩn danh, 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ụ `getAppsFlyerId`. | | 2a | `Adapty.Activate(builder.Build(), ...)` với `SetCustomerUserId` được đặt trên builder | Khởi chạy ứng dụng, sau bước 1, nếu bạn có customer user ID | Được khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty.Activate(builder.Build(), ...)` không có `SetCustomerUserId` | Khởi chạy ứng dụng, sau bước 1, nếu bạn không có customer user ID (hoặc không bao giờ thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. | | 3 | `Adapty.SetIntegrationIdentifier(key, value, callback)` cho mỗi MMP | Sau bước 2, trước bất kỳ lệnh gọi hành động người dùng nào | Bắt buộc để MMP ID rơi đúng vào hồ sơ người dùng. | | 4 | `Adapty.Identify("YOUR_USER_ID", callback)` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên lộ trình 2b có xác thực | Chờ callback hoàn tất. Các lệnh gọi đồng thời trong `Identify` tạo ra lỗi `#3006 profileWasChanged`. | | 5 | `GetPaywall`, `GetPaywallProducts`, `RestorePurchases`, `MakePurchase`, `UpdateAttribution`, `UpdateProfile` | Sau bước 4 nếu bạn gọi `Identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này dẫn đến mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về cho sai đối tượng. ::: ## Cài đặt Web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng trên web checkout (Stripe, Paddle) và sau đó cài đặt ứng dụng native, `Activate()` đầu tiên của thiết bị sẽ tạo một hồ sơ người dùng ẩn danh mới. Hồ sơ người dùng này không được liên kết với hồ sơ người dùng web. Nếu bạn có thể xác định customer user ID trước khi khởi chạy ứng dụng (từ flow xác thực hoặc install referrer của bạn), 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 đó là `RestorePurchases`. Để biết metadata cần gửi với mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: unity-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc lấy paywall trong Unity SDK" description: "Lấy paywall Adapty đáng tin cậy: thời điểm, caching và các pattern dự phòng cho Unity." --- Một lần lấy paywall đáng tin cậy trong Unity cần làm được ba việc: hiển thị nhanh, trả về đúng paywall theo đối tượng, và dự phòng hợp lý khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, caching và các pattern dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định `Adapty.Activate()` và `Adapty.Identify()` đã được thực thi xong. Xem [Thứ tự gọi trong Unity SDK](unity-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 | |---|---|---| | Lấy placement bạn sắp hiển thị. | Prefetch tất cả các placement đồng thời lúc khởi động. | Bulk prefetch chặn main thread và gây màn hình đen trong quá trình tải. | | Gọi `GetPaywall` sau khi attribution đã có thời gian xử lý xong — ví dụ, 1–2 giây sau `Activate` hoặc sau khi `OnLoadLatestProfile` kích hoạt. | Gọi `GetPaywall` trong `Awake()`. | Attribution chưa được cập nhật. Paywall sẽ được xử lý 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. | Không có timeout, người dùng có kết nối kém sẽ thấy màn hình trắng cho đến khi mạng phục hồi — hoặc thoát app. | Xem [Lấy paywall và sản phẩm](fetch-paywalls-and-products-unity) để 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 những thị trường có kết nối liên tục kém (vùng nông thôn, khi đang di chuyển, khu vực bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy` thành `AdaptyPlacementFetchPolicy.ReturnCacheDataElseLoad` cho mọi lần lấy 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 fallback khi timeout kích hoạt. - Đừng chặn việc hiển thị paywall bằng `GetProfile`. Gọi `GetPaywall` độc lập để một profile chậm không chặn giao diện người dùng. --- # File: unity-test --- --- title: "Test & release in Unity SDK" description: "Tìm hiểu cách kiểm thử và phát hành ứng dụng Unity của bạn với Adapty SDK." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng Unity, bạn cần 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. Điều này bao gồm việc kiểm thử cả tích hợp SDK lẫn luồng mua thực tế với môi trường sandbox của Apple và môi trường kiểm thử của Google Play. ## Kiểm thử ứng dụng \{#test-your-app\} Để kiểm thử toàn diện các in-app purchase, hãy xem hướng dẫn kiểm thử theo nền tảng của chúng tôi: [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 gửi ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối cửa hàng và thông báo máy chủ đã được cấu hình - Các giao dịch mua hoàn tất và được báo cáo về Adapty - Quyền 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à đánh giá đã được đáp ứng --- # File: InvalidProductIdentifiers-unity --- --- title: "Sửa lỗi Code-1000 noProductIDsFound trong Unity SDK" description: "Giải quyết lỗi identifier 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 liệt kê ở đó. Lỗi này đôi khi đ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 đang gặp lỗi `noProductIDsFound`, hãy làm theo các bước sau để giải quyết: ## Bước 1. Kiểm tra bundle ID \{#step-2-check-bundle-id\} 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <Zoom> <img src="/docs/img/afd5012-bundle_id_apple.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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**. <Zoom> <img src="/docs/img/2d64163-bundle_id.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> </Zoom> 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. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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**. <img src="/assets/shared/img/ready-to-submit.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/product-id-copy.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## 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ự. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Nhấp vào tên nhóm gói đăng ký để xem các sản phẩm của bạn. 3. Chọn sản phẩm bạn đang kiểm tra. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn đến 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. <img src="/assets/shared/img/product-availability.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## Bước 4. Kiểm tra giá sản phẩm \{#step-5-check-product-prices\} 1. Truy cập lại phần **Monetization** → **Subscriptions** trong **App Store Connect**. <img src="/assets/shared/img/subscription_group_open.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 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. <img src="/assets/shared/img/click-product.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 4. Cuộn xuống phần **Subscription Pricing** và mở rộng phần **Current Pricing for New Subscribers**. <img src="/assets/shared/img/check-prices.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 5. Đảm bảo tất cả các mức giá cần thiết đều được liệt kê. <img src="/assets/shared/img/product-pricing.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> ## 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 hoạt động 1. Trên trang chủ [**App Store Connect**](https://appstoreconnect.apple.com/), nhấp vào **Business**. <img src="/assets/shared/img/business.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 2. Chọn tên công ty của bạn. <img src="/assets/shared/img/business-name.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> 3. Cuộn xuống và kiểm tra xem **Paid Apps Agreement**, **Bank Account** và **Tax forms** của bạn đều hiển thị trạng thái **Active** hay chưa. <img src="/assets/shared/img/appstore-connect-status.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Bằng cách làm theo các bước trên, bạn sẽ có thể giải quyết 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 Các bước 1–5 có thể đều đạt — 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 được hiển thị qua đường 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 product ID. Sau khi tạo lại, hãy chờ tối đa 24 giờ để thay đổi được áp dụng. --- # File: cantMakePayments-unity --- --- title: "Cách sửa lỗi Code-1003 cantMakePayment trong Unity SDK" description: "Khắc phục lỗi không thể thanh toán khi quản lý gói đăng ký trong Adapty." --- Lỗi 1003, `cantMakePayments`, cho biết thiết bị không thể thực hiện in-app purchase. Nếu bạn gặp lỗi `cantMakePayments`, thường là do một trong các nguyên nhân sau: - Giới hạn thiết bị: Lỗi này không liên quan đến Adapty. Xem cách khắc phục bên dưới. - Cấu hình Observer mode: Không thể dùng đồng thời phương thức `makePurchase` và Observer mode. Xem phần bên dưới. ## Sự cố: Giới hạn thiết bị \{#issue-device-restrictions\} | Sự cố | Giải pháp | |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------| | Giới hạn Screen Time | Tắt giới hạn In-App Purchase trong [Screen Time](https://support.apple.com/en-us/102470) | | Tài khoản bị tạm khóa | Liên hệ Apple Support để giải quyết vấn đề tài khoản | | Giới hạn khu vực | Sử dụng tài khoản App Store từ vùng được hỗ trợ | ## Sự cố: Dùng đồng thời Observer mode và makePurchase \{#issue-using-both-observer-mode-and-makepurchase\} Nếu bạn đang dùng `makePurchases` để xử lý giao dịch mua, bạn không cần dùng Observer mode. [Observer mode](observer-vs-full-mode) chỉ cần thiết khi bạn tự triển khai logic mua hàng. Vì vậy, nếu bạn đang dùng `makePurchase`, bạn có thể xóa phần kích hoạt Observer mode khỏi code khởi tạo SDK một cách an toàn. --- # File: migration-to-unity-sdk-314 --- --- title: "Migrate Adapty Unity SDK to v3.14" description: "Migrate to Adapty Unity SDK v3.14 for better performance and new monetization features." --- Adapty SDK 3.14.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. Event listener riêng cho các sự kiện paywall. 2. Đổi tên `AdaptyUI.CreateView` thành `AdaptyUI.CreatePaywallView` và các phương thức liên quan. 3. Cập nhật phương thức `MakePurchase` để sử dụng `AdaptyPurchaseParameters` thay vì các tham số riêng lẻ. 4. Thay thế `SetFallbackPaywalls` bằng phương thức `SetFallback`. 5. Cập nhật cách truy cập thuộc tính paywall để sử dụng `AdaptyPlacement`. 6. Cập nhật cách truy cập Remote Config để sử dụng đối tượng `AdaptyRemoteConfig`. 7. Thay thế `VendorProductIds` bằng `ProductIdentifiers` trong model `AdaptyPaywall`. 8. Cập nhật fetch policy của `GetPaywall` để sử dụng `AdaptyFetchPolicy`. ## Event listener riêng cho các sự kiện paywall \{#separate-event-listener-for-paywall-events\} Nếu bạn hiển thị các paywall được thiết kế bằng [Paywall Builder](adapty-paywall-builder), các sự kiện của paywall view giờ đây sử dụng interface `AdaptyPaywallsEventsListener` và phương thức `SetPaywallsEventsListener` riêng biệt. Interface `AdaptyEventListener` cốt lõi vẫn dùng để nhận cập nhật hồ sơ người dùng và thông tin chi tiết về cài đặt. ```diff showLineNumbers using UnityEngine; using AdaptySDK; public class AdaptyListener : MonoBehaviour, - AdaptyEventListener { + AdaptyEventListener, + AdaptyPaywallsEventsListener { void Start() { Adapty.SetEventListener(this); + Adapty.SetPaywallsEventsListener(this); } // AdaptyEventListener methods public void OnLoadLatestProfile(AdaptyProfile profile) { } public void OnInstallationDetailsSuccess(AdaptyInstallationDetails details) { } public void OnInstallationDetailsFail(AdaptyError error) { } + // AdaptyPaywallsEventsListener methods + // Implement paywall event handlers here } ``` [Tìm hiểu thêm về cách xử lý sự kiện paywall](unity-handling-events). ## Đổi tên các phương thức tạo và hiển thị view \{#rename-view-creation-and-presentation-methods\} Các phương thức tạo và hiển thị view đã được đổi tên: ```diff showLineNumbers using AdaptySDK; - AdaptyUI.CreateView(paywall, parameters, (view, error) => { + AdaptyUI.CreatePaywallView(paywall, parameters, (view, error) => { if (error != null) { // handle the error return; } - AdaptyUI.PresentView(view, (error) => { + AdaptyUI.PresentPaywallView(view, (error) => { // handle the error }); }); } ``` Tương tự, phương thức dismiss cũng được đổi tên: ```diff showLineNumbers - AdaptyUI.DismissView(view, (error) => { + AdaptyUI.DismissPaywallView(view, (error) => { // handle the error }); ``` ## Cập nhật phương thức MakePurchase \{#update-makepurchase-method\} Phương thức `MakePurchase` giờ đây 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 an toàn kiểu dữ liệu tốt hơn và cho phép mở rộng các tham số mua hàng trong tương lai. ```diff showLineNumbers using AdaptySDK; void MakePurchase( AdaptyPaywallProduct product, AdaptySubscriptionUpdateParameters subscriptionUpdate, bool? isOfferPersonalized ) { - Adapty.MakePurchase(product, subscriptionUpdate, isOfferPersonalized, (result, error) => { + var parameters = new AdaptyPurchaseParametersBuilder() + .SetSubscriptionUpdateParams(subscriptionUpdate) + .SetIsOfferPersonalized(isOfferPersonalized) + .Build(); + + Adapty.MakePurchase(product, parameters, (result, error) => { switch (result.Type) { case AdaptyPurchaseResultType.Pending: // handle pending purchase break; case AdaptyPurchaseResultType.UserCancelled: // handle purchase cancellation break; case AdaptyPurchaseResultType.Success: var profile = result.Profile; // handle successful purchase break; default: break; } }); } ``` Nếu không cần thêm tham số nào, bạn có thể dùng đơn giản như sau: ```csharp showLineNumbers using AdaptySDK; void MakePurchase(AdaptyPaywallProduct product) { Adapty.MakePurchase(product, (result, error) => { // handle purchase result }); } ``` ## Cập nhật phương thức fallback \{#update-fallback-method\} :::important Khi nâng cấp lên Unity SDK 3.14, bạn cần tải xuống các file fallback mới từ Adapty dashboard và thay thế các file hiện có trong dự án của mình. ::: Phương thức để thiết lập fallback đã được cập nhật. Phương thức `SetFallbackPaywalls` đã được đổi tên thành `SetFallback`: ```diff showLineNumbers using AdaptySDK; void SetFallBackPaywalls() { #if UNITY_IOS var assetId = "adapty_fallback_ios.json"; #elif UNITY_ANDROID var assetId = "adapty_fallback_android.json"; #else var assetId = ""; #endif - Adapty.SetFallbackPaywalls(assetId, (error) => { + Adapty.SetFallback(assetId, (error) => { // handle the error }); } ``` Xem ví dụ code hoàn chỉnh tại trang [Sử dụng paywall dự phòng trong Unity](unity-use-fallback-paywalls). ## Cập nhật cách truy cập thuộc tính paywall \{#update-paywall-property-access\} Các thuộc tính sau đã được chuyển từ `AdaptyPaywall` sang `AdaptyPlacement`: ```diff showLineNumbers using AdaptySDK; void ProcessPaywall(AdaptyPaywall paywall) { - var abTestName = paywall.ABTestName; - var audienceName = paywall.AudienceName; - var revision = paywall.Revision; - var placementId = paywall.PlacementId; + var abTestName = paywall.Placement.ABTestName; + var audienceName = paywall.Placement.AudienceName; + var revision = paywall.Placement.Revision; + var placementId = paywall.Placement.Id; } ``` ## Cập nhật cách truy cập Remote Config \{#update-remote-config-access\} Các thuộc tính Remote Config đã được tái cấu trúc vào đối tượng `AdaptyRemoteConfig` để tổ chức tốt hơn: ```diff showLineNumbers using AdaptySDK; void ProcessRemoteConfig(AdaptyPaywall paywall) { - var remoteConfigString = paywall.RemoteConfigString; - var locale = paywall.Locale; - var remoteConfigDict = paywall.RemoteConfig; + var remoteConfigString = paywall.RemoteConfig.Data; + var locale = paywall.RemoteConfig.Locale; + var remoteConfigDict = paywall.RemoteConfig.Dictionary; } ``` ## Cập nhật cách sử dụng model AdaptyPaywall \{#update-adapty-paywall-model-usage\} Thuộc tính `VendorProductIds` đã bị deprecated và 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 using AdaptySDK; void ProcessPaywallProducts(AdaptyPaywall paywall) { - var productIds = paywall.VendorProductIds; - foreach (var vendorId in productIds) { - // use vendorId - } + var productIdentifiers = paywall.ProductIdentifiers; + foreach (var productId in productIdentifiers) { + var vendorId = productId.VendorProductId; + // use vendorId + } } ``` Đối tượng `AdaptyProductIdentifier` cho phép truy cập ID sản phẩm của nhà cung cấp thông qua thuộc tính `VendorProductId`, duy trì chức năng tương đương đồng thời cung cấp cấu trúc tốt hơn cho các cải tiến trong tương lai. ## Cập nhật fetch policy của GetPaywall \{#update-getpaywall-fetch-policy\} Kiểu tham số `fetchPolicy` trong phương thức `GetPaywall` đã được đổi từ `AdaptyPaywallFetchPolicy` sang `AdaptyPlacementFetchPolicy`. Thay đổi này giúp thống nhất cách sử dụng fetch policy trong toàn bộ SDK. ```diff showLineNumbers using AdaptySDK; void GetPaywall(string placementId) { - Adapty.GetPaywall(placementId, AdaptyPaywallFetchPolicy.ReloadRevalidatingCacheData, null, (paywall, error) => { + Adapty.GetPaywall(placementId, AdaptyPlacementFetchPolicy.ReloadRevalidatingCacheData, null, (paywall, error) => { // handle the result }); } ``` --- # File: migration-to-unity-sdk-34 --- --- title: "Migrate Adapty Unity SDK to v3.4" description: "Migrate lên Adapty Unity SDK v3.4 để cải thiện hiệu suất và các tính năng kiếm tiền mới." --- Adapty SDK 3.4.0 là một bản phát hành lớn với những cải tiến yêu cầu bạn thực hiện các bước migration. ## Cập nhật 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 về 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](unity-use-fallback-paywalls) bằng các file mới. ## Cập nhật cài đặt 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ài đặt 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` cầ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 giao dịch đượ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 integration. ::: ```diff showLineNumbers - #if UNITY_ANDROID && !UNITY_EDITOR - Adapty.RestorePurchases((profile, error) => { - // handle the error - }); - #endif Adapty.ReportTransaction( "YOUR_TRANSACTION_ID", "PAYWALL_VARIATION_ID", // optional (error) => { // handle the error }); ``` --- # File: migration-to-unity330 --- --- title: "Migrate Adapty Unity SDK to v3.3" description: "Migrate to Adapty Unity SDK v3.3 for better performance and new monetization features." --- 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. Nâng cấp lên Adapty SDK v3.3.x. 2. Đổi tên nhiều class, thuộc tính và phương thức trong các module Adapty và AdaptyUI của Adapty SDK. 3. Từ nay, phương thức `SetLogLevel` nhận một callback làm đối số. 4. Từ nay, phương thức `PresentCodeRedemptionSheet` nhận một callback làm đối số. 5. Thay đổi cách tạo paywall view. 6. Xóa phương thức `GetProductsIntroductoryOfferEligibility`. 7. Lưu paywall dự phòng vào các file riêng biệt (một file cho mỗi nền tảng) trong `Assets/StreamingAssets/` và truyền tên file vào phương thức `SetFallbackPaywalls`. 8. Cập nhật cách thực hiện mua hàng. 9. Cập nhật xử lý sự kiện Paywall Builder. 10. Cập nhật xử lý lỗi paywall trong Paywall Builder. 11. Cập nhật cấu hình tích hợp cho Adjust, Amplitude, AppMetrica, Appsflyer, Branch, Firebase và Google Analytics, Mixpanel, OneSignal, Pushwoosh. 13. Cập nhật cài đặt Observer mode. 14. Cập nhật khởi tạo Unity plugin với lệnh gọi `Activate` tường minh. ## Nâng cấp Adapty Unity SDK lên 3.3.x \{#upgrade-adapty-unity-sdk-to-33x\} Cho đến phiên bản này, Adapty SDK là SDK cốt lõi và bắt buộc để Adapty hoạt động đúng trong ứng dụng của bạn, còn AdaptyUI SDK là SDK tùy chọn chỉ cần thiết khi bạn sử dụng Adapty Paywall Builder. Bắt đầu từ phiên bản 3.3.0, AdaptyUI SDK đã bị deprecated và AdaptyUI được tích hợp vào Adapty SDK dưới dạng một module. Do những thay đổi này, bạn cần xóa AdaptyUISDK và cài đặt lại AdaptySDK. 1. Xóa cả hai dependency **AdaptySDK** và **AdaptyUISDK** khỏi dự án của bạn. 2. Xóa các thư mục **AdaptySDK** và **AdaptyUISDK**. 3. Import lại package AdaptySDK như mô tả trong trang [Cài đặt & cấu hình Adapty SDK cho Unity](sdk-installation-unity). ## Đổi tên \{#renamings\} 1. Đổi tên trong module Adapty: | Phiên bản cũ | Phiên bản mới | | ------------------------- | ------------------------ | | Adapty.sdkVersion | Adapty.SDKVersion | | Adapty.LogLevel | AdaptyLogLevel | | Adapty.Paywall | AdaptyPaywall | | Adapty.PaywallFetchPolicy | AdaptyPaywallFetchPolicy | | PaywallProduct | AdaptyPaywallProduct | | Adapty.Profile | AdaptyProfile | | Adapty.ProfileParameters | AdaptyProfileParameters | | ProfileGender | AdaptyProfileGender | | Error | AdaptyError | 2. Đổi tên trong module AdaptyUI: | Phiên bản cũ | Phiên bản mới | | ------------------ | ------------------ | | CreatePaywallView | CreateView | | PresentPaywallView | PresentView | | DismissPaywallView | DismissView | | AdaptyUI.View | AdaptyUIView | | AdaptyUI.Action | AdaptyUIUserAction | ## Thay đổi phương thức SetLogLevel \{#change-the-setloglevel-method\} Từ nay, phương thức `SetLogLevel` nhận một callback làm đối số. ```diff showLineNumbers - Adapty.SetLogLevel(Adapty.LogLevel.Verbose); + Adapty.SetLogLevel(Adapty.LogLevel.Verbose, null); // or you can pass the callback to handle the possible error ``` ## Thay đổi phương thức PresentCodeRedemptionSheet \{#change-the-presentcoderedemptionsheet-method\} Từ nay, phương thức `PresentCodeRedemptionSheet` nhận một callback làm đối số. ```diff showLineNumbers - Adapty.PresentCodeRedemptionSheet(); + Adapty.PresentCodeRedemptionSheet(null); // or you can pass the callback to handle the possible error ``` ## Thay đổi cách tạo paywall view \{#change-how-the-paywall-view-is-created\} Để xem ví dụ code đầy đủ, hãy xem phần [Lấy cấu hình view của paywall được thiết kế bằng Paywall Builder](unity-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). ```diff showLineNumbers + var parameters = new AdaptyUICreateViewParameters() + .SetPreloadProducts(true); - AdaptyUI.CreatePaywallView( + AdaptyUI.CreateView( paywall, - preloadProducts: true, + parameters, (view, error) => { // use the view }); ``` ## Xóa phương thức GetProductsIntroductoryOfferEligibility \{#remove-the-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. Hiện tại, đố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 phương thức cung cấp paywall dự phòng \{#update-method-for-providing-fallback-paywalls\} Cho đến phiên bản này, các paywall dự phòng được truyền dưới dạng JSON đã được serialize. Bắt đầu từ v3.3.0, cơ chế đã thay đổi: 1. Lưu các paywall dự phòng vào file trong `/Assets/StreamingAssets/`, 1 file cho Android và 1 file cho iOS. 2. Truyền tên file vào phương thức `SetFallbackPaywalls`. Code của bạn sẽ thay đổi như sau: ```diff showLineNumbers using AdaptySDK; void SetFallBackPaywalls() { + #if UNITY_IOS + var assetId = "adapty_fallback_ios.json"; + #elif UNITY_ANDROID + var assetId = "adapty_fallback_android.json"; + #else + var assetId = ""; + #endif - Adapty.SetFallbackPaywalls("FALLBACK_PAYWALLS_JSON_STRING", (error) => { + Adapty.SetFallbackPaywalls(assetId, (error) => { // handle the error }); } ``` Xem ví dụ code đầy đủ trong trang [Sử dụng paywall dự phòng trong Unity](unity-use-fallback-paywalls). ## Cập nhật cách thực hiện mua hàng \{#update-making-purchase\} Trước đây, các giao dịch bị hủy và đang chờ xử lý được coi là lỗi và trả về các mã `PaymentCancelled` và `PendingPurchase` tương ứng. Hiện tại, một class `AdaptyPurchaseResultType` mới được sử dụng để xử lý các giao dịch bị hủy, thành công và đang chờ xử lý. Cập nhật code mua hàng theo cách sau: ```diff showLineNumbers using AdaptySDK; void MakePurchase(AdaptyPaywallProduct product) { - Adapty.MakePurchase(product, (profile, error) => { - // handle successfull purchase + Adapty.MakePurchase(product, (result, error) => { + switch (result.Type) { + case AdaptyPurchaseResultType.Pending: + // handle pending purchase + break; + case AdaptyPurchaseResultType.UserCancelled: + // handle purchase cancellation + break; + case AdaptyPurchaseResultType.Success: + var profile = result.Profile; + // handle successful purchase + break; + default: + break; } }); } ``` Xem ví dụ code đầy đủ trong trang [Thực hiện mua hàng trong ứng dụng](unity-making-purchases). ## Cập nhật xử lý sự kiện Paywall Builder \{#update-handling-of-paywall-builder-events\} Các giao dịch bị hủy và đang chờ xử lý không còn được coi là lỗi nữa, tất cả các trường hợp này được xử lý bằng phương thức `PaywallViewDidFinishPurchase`. 1. Xóa xử lý sự kiện mua hàng bị hủy. 2. Cập nhật xử lý sự kiện mua hàng thành công như sau: ```diff showLineNumbers - public void OnFinishPurchase( - AdaptyUI.View view, - Adapty.PaywallProduct product, - Adapty.Profile profile - ) { } + public void PaywallViewDidFinishPurchase( + AdaptyUIView view, + AdaptyPaywallProduct product, + AdaptyPurchaseResult purchasedResult + ) { } ``` 3. Cập nhật xử lý các action: ```diff showLineNumbers - public void OnPerformAction( - AdaptyUI.View view, - AdaptyUI.Action action - ) { + public void PaywallViewDidPerformAction( + AdaptyUIView view, + AdaptyUIUserAction action + ) { switch (action.Type) { - case AdaptyUI.ActionType.Close: + case AdaptyUIUserActionType.Close: view.Dismiss(null); break; - case AdaptyUI.ActionType.OpenUrl: + case AdaptyUIUserActionType.OpenUrl: var urlString = action.Value; if (urlString != null { Application.OpenURL(urlString); } default: // handle other events break; } } ``` 4. Cập nhật xử lý khi bắt đầu mua hàng: ```diff showLineNumbers - public void OnSelectProduct( - AdaptyUI.View view, - Adapty.PaywallProduct product - ) { } + public void PaywallViewDidSelectProduct( + AdaptyUIView view, + string productId + ) { } ``` 5. Cập nhật xử lý khi mua hàng thất bại: ```diff showLineNumbers - public void OnFailPurchase( - AdaptyUI.View view, - Adapty.PaywallProduct product, - Adapty.Error error - ) { } + public void PaywallViewDidFailPurchase( + AdaptyUIView view, + AdaptyPaywallProduct product, + AdaptyError error + ) { } ``` 6. Cập nhật xử lý sự kiện khôi phục thành công: ```diff showLineNumbers - public void OnFailRestore( - AdaptyUI.View view, - Adapty.Error error - ) { } + public void PaywallViewDidFailRestore( + AdaptyUIView view, + AdaptyError error + ) { } ``` Xem ví dụ code đầy đủ trong trang [Xử lý sự kiện paywall](unity-handling-events). ## Cập nhật xử lý lỗi paywall trong Paywall Builder \{#update-handling-of-paywall-builder-paywall-errors\} Cách xử lý lỗi cũng đã thay đổi, hãy cập nhật code của bạn theo hướng dẫn bên dưới. 1. Cập nhật xử lý lỗi tải sản phẩm: ```diff showLineNumbers - public void OnFailLoadingProducts( - AdaptyUI.View view, - Adapty.Error error - ) { } + public void PaywallViewDidFailLoadingProducts( + AdaptyUIView view, + AdaptyError error + ) { } ``` 2. Cập nhật xử lý lỗi rendering: ```diff showLineNumbers - public void OnFailRendering( - AdaptyUI.View view, - Adapty.Error error - ) { } + public void PaywallViewDidFailRendering( + AdaptyUIView view, + AdaptyError error + ) { } ``` ## Cập nhật cấu hình SDK tích hợp bên thứ ba \{#update-third-party-integration-sdk-configuration\} Bắt đầu từ Adapty Unity SDK 3.3.0, chúng tôi đã cập nhật public API cho phương thức `updateAttribution`. Trước đây, nó nhận một dictionary `[AnyHashable: Any]`, cho phép bạn truyền trực tiếp các đối tượng attribution từ các dịch vụ khác nhau. Bây giờ, nó yêu cầu `[String: any Sendable]`, vì vậy bạn cần chuyển đổi các đối tượng attribution trước khi truyền vào. Để đảm bảo các tích hợp hoạt động đúng với Adapty Unity SDK 3.3.0 trở lên, hãy cập nhật cấu hình SDK cho các tích hợp sau theo mô tả trong các phần bên dưới. ### Adjust Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Adjust](adjust#connect-your-app-to-adjust). ```diff showLineNumbers - using static AdaptySDK.Adapty; using AdaptySDK; Adjust.GetAdid((adid) => { - Adjust.GetAttribution((attribution) => { - Dictionary<String, object> data = new Dictionary<String, object>(); - - data["network"] = attribution.Network; - data["campaign"] = attribution.Campaign; - data["adgroup"] = attribution.Adgroup; - data["creative"] = attribution.Creative; - - String attributionString = JsonUtility.ToJson(data); - Adapty.UpdateAttribution(attributionString, AttributionSource.Adjust, adid, (error) => { - // handle the error - }); + if (adid != null) { + Adapty.SetIntegrationIdentifier( + "adjust_device_id", + adid, + (error) => { + // handle the error + }); } }); Adjust.GetAttribution((attribution) => { Dictionary<String, object> data = new Dictionary<String, object>(); data["network"] = attribution.Network; data["campaign"] = attribution.Campaign; data["adgroup"] = attribution.Adgroup; data["creative"] = attribution.Creative; String attributionString = JsonUtility.ToJson(data); - Adapty.UpdateAttribution(attributionString, AttributionSource.Adjust, adid, (error) => { + Adapty.UpdateAttribution(attributionString, "adjust", (error) => { // 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 phần [Cấu hình SDK cho tích hợp Amplitude](amplitude#sdk-configuration). ```diff showLineNumbers using AdaptySDK; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetAmplitudeUserId("YOUR_AMPLITUDE_USER_ID"); - builder.SetAmplitudeDeviceId(amplitude.getDeviceId()); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + Adapty.SetIntegrationIdentifier( + "amplitude_user_id", + "YOUR_AMPLITUDE_USER_ID", + (error) => { + // handle the error + }); + Adapty.SetIntegrationIdentifier( + "amplitude_device_id", + amplitude.getDeviceId(), + (error) => { + // 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 phần [Cấu hình SDK cho tích hợp AppMetrica](appmetrica#sdk-configuration). ```diff showLineNumbers using AdaptySDK; - var deviceId = AppMetrica.GetDeviceId(); - if (deviceId != null { - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetAppmetricaProfileId("YOUR_ADAPTY_CUSTOMER_USER_ID"); - builder.SetAppmetricaDeviceId(deviceId); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); - } + var deviceId = AppMetrica.GetDeviceId(); + if (deviceId != null { + Adapty.SetIntegrationIdentifier( + "appmetrica_device_id", + deviceId, + (error) => { + // handle the error + }); + + Adapty.SetIntegrationIdentifier( + "appmetrica_profile_id", + "YOUR_ADAPTY_CUSTOMER_USER_ID", + (error) => { + // 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 phần [Cấu hình SDK cho tích hợp AppsFlyer](appsflyer#connect-your-app-to-appsflyer). ```diff showLineNumbers using AppsFlyerSDK; using AdaptySDK; // before SDK initialization AppsFlyer.getConversionData(this.name); // in your IAppsFlyerConversionData void onConversionDataSuccess(string conversionData) { // It's important to include the network user ID - string appsFlyerId = AppsFlyer.getAppsFlyerId(); - Adapty.UpdateAttribution(conversionData, AttributionSource.Appsflyer, appsFlyerId, (error) => { + string appsFlyerId = AppsFlyer.getAppsFlyerId(); + + Adapty.SetIntegrationIdentifier( + "appsflyer_id", + appsFlyerId, + (error) => { // handle the error }); + + Adapty.UpdateAttribution( + conversionData, + "appsflyer", + (error) => { + // handle the error + }); } ``` ### 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 phần [Cấu hình SDK cho tích hợp Branch](branch#connect-your-app-to-branch). ```diff showLineNumbers using AdaptySDK; - class YourBranchImplementation { - func initializeBranch() { - Branch.getInstance().initSession(launchOptions: launchOptions) { (data, error) in - if let data { - Adapty.updateAttribution(data, source: .branch) - } - } - } - } + Branch.initSession(delegate(Dictionary<string, object> parameters, string error) { + string attributionString = JsonUtility.ToJson(parameters); + + Adapty.UpdateAttribution( + attributionString, + "branch", + (error) => { + // handle the error + }); + }); ``` ### Firebase và Google Analytics Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem phần [Cấu hình SDK cho tích hợp Firebase và Google Analytics](firebase-and-google-analytics). ```diff showLineNumbers // We suppose FirebaseAnalytics Unity Plugin is already installed using AdaptySDK; Firebase.Analytics .FirebaseAnalytics .GetAnalyticsInstanceIdAsync() .ContinueWithOnMainThread((task) => { if (!task.IsCompletedSuccessfully) { // handle error return; } var firebaseId = task.Result var builder = new Adapty.ProfileParameters.Builder(); - builder.SetFirebaseAppInstanceId(firebaseId); - - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error + Adapty.SetIntegrationIdentifier( + "firebase_app_instance_id", + firebaseId, + (error) => { + // 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 phần [Cấu hình SDK cho tích hợp Mixpanel](mixpanel#sdk-configuration). ```diff showLineNumbers using AdaptySDK; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetMixpanelUserId(Mixpanel.DistinctId); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + var distinctId = Mixpanel.DistinctId; + if (distinctId != null) { + Adapty.SetIntegrationIdentifier( + "mixpanel_user_id", + distinctId, + (error) => { + // 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 phần [Cấu hình SDK cho tích hợp OneSignal](onesignal#sdk-configuration). ```diff showLineNumbers using AdaptySDK; - using OneSignalSDK; - var pushUserId = OneSignal.Default.PushSubscriptionState.userId; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetOneSignalPlayerId(pushUserId); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + var distinctId = Mixpanel.DistinctId; + if (distinctId != null) { + Adapty.SetIntegrationIdentifier( + "mixpanel_user_id", + distinctId, + (error) => { + // handle the 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 phần [Cấu hình SDK cho tích hợp Pushwoosh](pushwoosh#sdk-configuration). ```diff showLineNumbers using AdaptySDK; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetPushwooshHWID(Pushwoosh.Instance.HWID); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + Adapty.SetIntegrationIdentifier( + "pushwoosh_hwid", + Pushwoosh.Instance.HWID, + (error) => { + // 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 các paywall với 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 đủ trong trang [Liên kết paywall với giao dịch mua hàng trong Observer mode](report-transactions-observer-mode-unity). ```diff showLineNumbers // every time when calling transaction.finish() - Adapty.SetVariationForTransaction("<variationId>", "<transactionId>", (error) => { - if(error != null) { - // handle the error - return; - } - - // successful binding - }); + Adapty.ReportTransaction( + "YOUR_TRANSACTION_ID", + "PAYWALL_VARIATION_ID", // optional + (error) => { + // handle the error + }); ``` ## Cập nhật khởi tạo Unity plugin \{#update-the-unity-plugin-initialization\} Bắt đầu từ Adapty Unity SDK 3.3.0, việc gọi tường minh phương thức `Activate` trong quá trình khởi tạo plugin là bắt buộc: ```csharp showLineNumbers Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); ``` --- # File: migration-to-unity-sdk-v3 --- --- title: "Migrate Adapty Unity SDK to v3.0" description: "Migrate to Adapty Unity SDK v3.0 for better performance and new monetization features." --- Adapty SDK v3.0 mang đến hỗ trợ cho [Adapty Paywall Builder](adapty-paywall-builder) — phiên bản mới của công cụ tạo paywall trực quan không cần code. Với tính linh hoạt tối đa và khả năng thiết kế phong phú, các paywall của bạn sẽ trở nên hiệu quả và sinh lời hơn. ## Quy trình nâng cấp \{#upgrade-process\} Quy trình nâng cấp cho Unity bao gồm các bước tương tự như các nền tảng khác: 1. Nâng cấp lên Adapty SDK v3.x 2. Migrate các paywall hiện có sang Paywall Builder mới Để biết hướng dẫn migration chi tiết dành riêng cho Unity, vui lòng tham khảo [hướng dẫn cài đặt Unity SDK](sdk-installation-unity) và làm theo các bước migration chung được trình bày trong hướng dẫn migration chính. --- # File: unity-migration-guide --- --- title: "Hướng dẫn migration SDK" description: "Hướng dẫn migration cho Unity Adapty SDK." --- ## Hướng dẫn Migration \{#migration-guides\} ### [Hướng dẫn migration lên Unity Adapty SDK 3.x](unity-sdk-migration-guides) \{#migration-guide-to-unity-adapty-sdk-3x\} Tìm hiểu cách migrate từ các phiên bản cũ lên Unity Adapty SDK 3.x. ## Tính năng mới \{#whats-new\} ### Phiên bản 3.x \{#version-3x\} - Cải tiến cách hiển thị paywall - Xử lý lỗi tốt hơn - Hỗ trợ C# tốt hơn - Tối ưu hóa hiệu suất ### Phiên bản 2.x \{#version-2x\} - Tính năng onboarding mới - Cải tiến analytics - Cải thiện flow mua hàng - Sửa lỗi và cải thiện độ ổn định ## Thay đổi Breaking \{#breaking-changes\} ### Phiên bản 3.x \{#version-3x-breaking\} - Cập nhật observer API - Thay đổi phương thức hiển thị paywall - Thay đổi cấu trúc xử lý lỗi ### Phiên bản 2.x \{#version-2x-breaking\} - Cập nhật onboarding API - Thay đổi cấu trúc hồ sơ người dùng - Thay đổi flow mua hàng ## Danh sách kiểm tra Migration \{#migration-checklist\} Khi migrate lên phiên bản mới: - [ ] Xem xét các thay đổi breaking - [ ] Cập nhật các lời gọi API - [ ] Kiểm tra toàn bộ chức năng - [ ] Cập nhật xử lý lỗi - [ ] Xác minh theo dõi analytics - [ ] Kiểm tra trên tất cả các nền tảng --- # End of Documentation _Generated on: 2026-07-24T13:01:43.701Z_ _Successfully processed: 41/41 files_