# Adapty Documentation (Full Content) > Complete documentation content across all platforms. Locale: vi Generated on: 2026-06-24T14:36:39.020Z --- # ANDROID - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-06-24T14:36:38.614Z Total files: 40 --- # 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ý." --- 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**: Đây là SDK cốt lõi, 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 để tạo paywall đa nền tảng. AdaptyUI được kích hoạt tự động cùng với module cốt lõ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](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). ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ## Cài đặt Adapty SDK \{#install-adapty-sdk\} Chọn phương thức thiết lập dependency: - Gradle thông thường: Thêm dependency vào file `build.gradle` ở **cấp module** - Nếu dự án của bạn dùng file `.gradle.kts`, thêm dependency vào `build.gradle.kts` ở cấp module - Nếu bạn dùng version catalog, thêm dependency vào file `libs.versions.toml` rồi tham chiếu 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 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() } } ``` Ngược lại, 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() } } ```
## 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 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. 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 `"YOUR_PUBLIC_SDK_KEY"` trong code. :::important - Đảm bảo bạn dùng **Public SDK key** để khởi tạo Adapty, còn **Secret key** chỉ 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 chắc chắn 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 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 làm theo [hướng dẫn nhanh Paywall Builder](android-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](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\} ### Logging \{#logging\} #### Thiết lập hệ thống logging \{#set-up-the-logging-system\} Adapty ghi lại lỗi và các 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 hiện có: | Cấp độ | Mô tả | | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------- | | `AdaptyLogLevel.NONE` | Không có gì được ghi lại. Giá trị mặc định | | `AdaptyLogLevel.ERROR` | Chỉ ghi lại lỗi | | `AdaptyLogLevel.WARN` | Ghi lại lỗi và các thông báo từ SDK không gây ra lỗi nghiêm trọng nhưng cần chú ý. | | `AdaptyLogLevel.INFO` | Ghi lại lỗi, cảnh báo và các thông báo thông tin. | | `AdaptyLogLevel.VERBOSE` | Ghi lại thêm các thông tin có thể hữu ích khi gỡ lỗi, như các lời gọi hàm, truy vấn API, v.v. | Bạn có thể đặt cấp độ 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 của hệ thống logging \{#redirect-the-logging-system-messages\} Nếu vì lý do nào đó bạn cần gửi thông báo từ Adapty đến hệ thống của mình hoặc lưu 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 gửi dữ liệu đó một cách rõ ràng, 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 thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt việc thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. 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 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. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build(); ``` #### Tắt 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` để tắt 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`. 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 quyền advertising ID, 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 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 media cache cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Mặc định, AdaptyUI lưu cache media (như hình ảnh và video) để cải thiện hiệu suất và giảm sử dụng mạng. Bạn có thể tùy chỉnh cài đặt cache bằng cách cung cấp cấu hình 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, 7 ngày hiệu lực). ```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 dung lượng 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 cache còn hiệu lực. Mặc định là 7 ngày. | :::tip Bạn có thể xóa media cache trong lúc chạy bằ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 trong một số trường hợp để 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 nhận diện giao dịch mua trong khi vẫn giữ thông tin người dùng ẩn danh, đặc biệt quan trọng cho việc 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 phải tuân thủ các quy định 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 tiết lộ định danh người dùng thực. ```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 chỉ khởi tạo Adapty một lần; nếu không, có thể xảy ra 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 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 process khác mà không đặt giá trị này, SDK sẽ ghi lại cảnh báo và bỏ qua việc kích hoạt. ### Bật local access levels \{#enable-local-access-levels\} Mặc định, [local access levels](local-access-levels) bị tắt trên Android. Để bật chúng, đặ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(); ``` ## Xử lý sự cố \{#troubleshooting\} #### Quy tắc Android backup (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 dùng nhiều SDK định nghĩa các quy tắc backup, Android manifest merger có thể thất bại với lỗi đề cập đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi điển hình: `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 giá trị của ứng dụng bạn 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 (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 `AndroidManifest.xml` của ứng dụng, cập nhật thẻ `` để ứng dụng của bạn cung cấp 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 bất kỳ SDK nào cũng đặt `android:allowBackup`, hãy đưa nó vào `tools:replace` như sau: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Tạo các file quy tắc backup đã 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 quy tắc của Adapty với quy tắc từ các SDK khác. Android sử dụng các định dạng quy tắc backup 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 tính 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ụ dưới đây dùng AppsFlyer làm SDK bên thứ ba mẫu. Hãy thay thế hoặc thêm các quy tắc cho bất kỳ SDK nào khác bạn đang sử dụng trong ứng dụng. ::: **Dành cho Android 12 trở lên** (sử dụng định dạng data extraction rules mới): ```xml title="sample_data_extraction_rules.xml" ``` **Dành cho Android 11 trở xuống** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" ``` Với thiết lập này: - Các quy tắc loại trừ backup của Adapty (`AdaptySDKPrefs.xml`) được giữ lại. - Các quy tắc loại trừ của các 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 bạn và không còn thất bại do các thuộc tính backup xung đột. #### Giao dịch mua thất bại sau khi quay lại từ ứng dụng 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 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ị xử lý như đã hủy. Để đảm bảo giao dịch mua hoạt động chính xác, chỉ sử dụng chế độ khởi động `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 ``` --- # File: android-quickstart-paywalls --- --- title: "Kích hoạt mua hàng bằng cách sử dụng paywall trong Android SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký trong ứng dụng." --- Để kích hoạt in-app purchase, bạn cần nắm 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) - [**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. - [**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 trên dashboard, sau đó gọi 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 ba cách để kích hoạt mua hàng trong ứng dụng. 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 sử dụng | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Dễ | Bạn [tạo paywall hoàn chỉnh, sẵn sàng để mua hàng trong trình tạo no-code](quickstart-paywalls). Adapty tự động render và xử lý toàn bộ luồng mua hàng, xác thực biên lai và quản lý gói đăng ký ở phía sau. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự tạo giao diện paywall trong code, nhưng vẫn lấy đối tượng paywall 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ó hạ tầng xử lý mua hàng riêng và muốn tiếp tục sử dụng. Lưu ý rằng chế độ observer 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, xem [hướng dẫn xử lý mua hàng trong paywall tự tạo](android-making-purchases). ::: Để hiển thị paywall được tạo trong Adapty Paywall Builder, trong code ứng dụng, bạn chỉ cần: 1. **Lấy paywall**: Lấy paywall từ Adapty. 2. **Hiển thị paywall và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị container paywall bạn đã lấy 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 paywall với phản hồi của ứng dụng. Ví dụ, mở liên kết hoặc đóng paywall khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. [Kết nối ứng dụng với 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-android) 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 bắt đầu nhanh](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy paywall \{#1-get-the-paywall\} Các paywall của bạn được liên kết với các placement được cấu hình trên dashboard. Placement cho phép bạn chạy các paywall khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy paywall được tạo trong Adapty Paywall Builder, bạn cần: 1. Lấy đối tượng `paywall` theo [placement](placements) ID bằng phương thức `getPaywall` và kiểm tra xem đó có phải là paywall được tạo trong builder hay không. 2. Lấy cấu hình view của paywall bằng phương thức `getViewConfiguration`. Cấu hình view chứa các phần tử UI và kiểu dáng cần thiết để hiển thị paywall. :::important Để lấy cấu hình view, bạn phải bật toggle **Show on device** trong Paywall Builder. Nếu không, bạn sẽ nhận được cấu hình view rỗng và paywall sẽ không được hiển thị. ::: ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID") { result -> if (result is AdaptyResult.Success) { val paywall = result.value if (!paywall.hasViewConfiguration) { return@getPaywall } AdaptyUI.getViewConfiguration(paywall) { configResult -> if (configResult is AdaptyResult.Success) { val viewConfiguration = configResult.value } } } } ``` ```java showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); if (!paywall.hasViewConfiguration()) { return; } AdaptyUI.getViewConfiguration(paywall, configResult -> { if (configResult instanceof AdaptyResult.Success) { AdaptyUI.LocalizedViewConfiguration viewConfiguration = ((AdaptyResult.Success) configResult).getValue(); // use loaded configuration } }); } }); ``` ## 2. Hiển thị paywall \{#2-display-the-paywall\} Bây giờ khi đã có cấu hình paywall, chỉ cần thêm vài dòng code để hiển thị paywall của bạn. Để hiển thị paywall trên màn hình thiết bị, trước tiên bạn phải cấu hình nó. Để làm vậ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, null, // products = null means auto-fetch eventListener, ) ``` ```kotlin showLineNumbers val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { showPaywall( viewConfiguration, null, // products = null means auto-fetch eventListener, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, null, // products = null means auto-fetch eventListener, ); ``` ```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); ``` ```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ị paywall, xem [hướng dẫn](android-present-paywalls) của chúng tôi. ::: ## 3. Xử lý các hành động nút \{#3-handle-button-actions\} Khi người dùng nhấn nút trong paywall, Android SDK tự động xử lý mua hàng, khôi phục, đóng paywall và mở liên kết. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được định nghĩa 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ụ, đâ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. :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [hành động](android-handle-paywall-actions) và [sự kiện](android-handling-events) của nút. ::: ```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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. [Kiểm tra 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\} Dưới đây là cách tất cả các bước đó có thể được tích hợp cùng nhau trong ứng dụng của bạn. ```kotlin showLineNumbers title="Kotlin" class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Adapty.getPaywall("YOUR_PLACEMENT_ID") { paywallResult -> if (paywallResult is AdaptyResult.Success) { val paywall = paywallResult.value if (!paywall.hasViewConfiguration) { // Use custom logic return@getPaywall } AdaptyUI.getViewConfiguration(paywall) { configResult -> if (configResult is AdaptyResult.Success) { val viewConfiguration = configResult.value val paywallView = AdaptyUI.getPaywallView( this, viewConfiguration, null, // products = null means auto-fetch object : AdaptyUIEventListener { override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.Close -> { (context as? Activity)?.onBackPressed() } } } } ) setContentView(paywallView) } } } } } } ``` ```java showLineNumbers public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Adapty.getPaywall("YOUR_PLACEMENT_ID", paywallResult -> { if (paywallResult instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) paywallResult).getValue(); if (!paywall.hasViewConfiguration()) { // Use custom logic return; } AdaptyUI.getViewConfiguration(paywall, configResult -> { if (configResult instanceof AdaptyResult.Success) { AdaptyUI.LocalizedViewConfiguration viewConfiguration = ((AdaptyResult.Success) configResult).getValue(); AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( this, viewConfiguration, null, // products = null means auto-fetch new AdaptyUIEventListener() { @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(paywallView); } }); } }); } } ``` --- # 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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-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 paywall được thiết kế bằng Paywall Builder và cấu hình của nó trong Android SDK" description: "Tìm hiểu cách lấy PB paywalls trong Adapty để kiểm soát gói đăng ký tốt hơn trong ứng dụng Android của bạn." --- Sau khi [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 mobile của mình. Bước đầu tiên trong quy trình này là lấy paywall gắn với placement cùng 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 Android SDK phiên bản 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, vui lòng tham khảo chủ đề [Lấy paywalls và sản phẩm cho remote config paywalls trong ứng dụng mobile](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ị paywalls trong ứng dụng mobile của bạn (nhấn để mở rộng) 1. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 2. [Tạo một paywall và thêm 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 mobile của bạn.
## Lấy paywall được thiết kế bằng Paywall Builder \{#fetch-paywall-designed-with-paywall-builder\} Nếu bạn đã [thiết kế paywall bằng Paywall Builder](adapty-paywall-builder), bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Một paywall như vậy chứa cả nội dung cần 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ó qua placement, cấu hình view, rồi trình bày nó trong ứng dụng mobile. Để đảm bảo hiệu suất tốt nhất, việc lấy paywall và [cấu hình view](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) càng sớm càng tốt là rất quan trọng, cho phép đủ thời gian để tải hình ảnh trước khi hiển thị cho người dùng. Để lấy một 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 | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này cần 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à 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.

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

Tuy nhiên, nếu bạn cho rằng người dùng của mình 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 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, nên an toàn khi sử dụng trong suốt 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 gỡ cài đặt ứng dụng hoặc dọn dẹp thủ công.

Adapty SDK lưu trữ paywalls 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 paywalls 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 paywalls trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.

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

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

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

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

| Tham số phản hồi: | Tham số | Mô tả | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | Một đối tượng [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) với danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình view của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Đả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 — điều này cho biết paywall được tạo bằng Paywall Builder. Thông tin này sẽ hướng dẫn bạn cách hiển thị paywall. Nếu có `ViewConfiguration`, hãy xử lý nó như một paywall Paywall Builder; nếu không, [xử lý nó như một remote config paywall](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 | 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 `loadTimeout` đã chỉ định, vì thao tác có thể bao gồm nhiều yêu cầu 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 | 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 `loadTimeout` đã chỉ định, vì thao tác có thể bao gồm nhiều yêu cầu 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ã locale đúng cách [tại đây](android-localizations-and-locale-codes). ::: Sau khi tải xong, [trình bày 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, paywalls đượ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à paywalls, 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 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 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ị 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 có phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được render. - **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**, có nghĩa là bạn mất 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 bạn). Nếu bạn sẵn sàng chấp nhận những nhược điểm này để được 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` như đã 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` có sẵn 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 cần là mã ngôn ngữ gồm một hoặc nhiều 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à 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.

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

Tuy nhiên, nếu bạn cho rằng người dùng của mình 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 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, nên an toàn khi sử dụng trong suốt 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 gỡ cài đặt ứng dụng hoặc dọn dẹp thủ công.

| ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall, hãy triển khai các custom assets. Hình ảnh hero và video có các ID được định nghĩa sẵn: `hero_image` và `hero_video`. Trong một bundle custom asset, 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ị ả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 chạy 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 custom assets 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ẽ quay về giao diện mặc định của nó. ::: --- # File: android-present-paywalls --- --- title: "Android - Hiển thị paywall mới với Paywall Builder" description: "Tìm hiểu cách hiển thị paywall trên Android để tối ưu hóa monetization." --- 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 hiển thị. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** yêu cầu SDK v3.0. Quy trình hiển thị paywall khác nhau tùy theo phiên bản Paywall Builder được sử dụng, paywall remote config, và [chế độ Observer](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). - Để 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 [Fetch 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ị, bạn phải 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 `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 hệ thống view và hiển thị trên màn hình thiết bị. Nếu bạn lấy `AdaptyPaywallView` _không_ thông qua `AdaptyUI.getPaywallView()`, bạn cũng cần gọi phương thức `.showPaywall()`. Để hiển thị paywall 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, sử dụng composable function sau: ```kotlin showLineNumbers AdaptyPaywallScreen( viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver, ) ``` Các 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 trực quan của paywall. Dùng phương thức `Adapty.getViewConfiguration(paywall)` để tải nó. Tham khảo chủ đề [Fetch cấu hình trực quan của paywall](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) để biết thêm chi tiết. | | **products** | tùy chọn | Cung cấp 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 fetch các sản phẩm cần thiết. | | **eventListener** | tùy chọn | Cung cấp `AdaptyUiEventListener` để theo dõi các sự kiện paywall. Nên extend `AdaptyUiDefaultEventListener` để dễ sử dụng hơn. Tham khảo chủ đề [Xử lý sự kiện paywall](android-handling-events) để biết thêm chi tiết. | | **insets** | tùy chọn |

Insets là các khoảng cách xung quanh paywall để ngăn các phần tử tương tác 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 paywall 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 được mô tả trong phần [Thay đổi insets 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 | Dùng `AdaptyUiTagResolver` để resolve các custom tag trong văn bản paywall. Resolver này nhận tham số tag và resolve thành chuỗi tương ứng. Tham khảo chủ đề Custom tags trong Paywall Builder để biết thêm chi tiết. | | **timerResolver** | tùy chọn | Truyền resolver vào đây nếu bạn sử dụng chức năng timer 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 paywall \{#change-paywall-insets\} Insets là các khoảng cách xung quanh paywall để ngăn các phần tử tương tác bị ẩn sau thanh hệ thống. Mặc định, Adapty sẽ tự động điều chỉnh insets, phù hợp với paywall 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: - Nếu cả thanh trạng thái lẫn thanh điều hướng đều không che `AdaptyPaywallView`, dùng `AdaptyPaywallInsets.NONE`. - Với các cấu hình tùy chỉnh hơn, ví dụ nếu paywall của bạn chồng lên thanh trạng thái trên cùng 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ụ bên dưới: ```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 lập trình viên định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer tùy chỉnh trong ứng dụng, hãy tạo đối tượng `timerResolver`—một dictionary hoặc map ánh xạ các timer tùy chỉnh với giá trị chuỗi sẽ thay thế chúng khi paywall được render. 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 đị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 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 timer, chẳng hạn 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, hãy tạo đố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 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 ứ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 vào `tagResolver` ngay trước khi hiển thị paywall. Khi đã sẵn sàng, truyền nó vào phương thức AdaptyUI mà bạn dùng để hiển thị paywall. ## Thay đổi màu loading indicator của 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: "Xử lý hành động nút trong Android SDK" description: "Xử lý các hành động nút trên paywall trong Android sử dụng Adapty để tối ưu hóa việc kiếm tiền từ ứng dụng." --- Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm [nút trong paywall builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một ID hành động tùy chỉnh. 2. Viết code trong ứng dụng của bạn để xử lý từng hành động đã gán. Hướng dẫn này hướng dẫn cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn. :::warning **Chỉ cá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 của bạn: 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 một handler cho hành động `close` để đóng paywall. :::info Trong Android SDK, hành động `close` mặc định sẽ kích hoạt việc đóng paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một paywall có thể kích hoạt mở paywall khác. ::: ```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 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 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 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ẽ kích hoạt việc 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 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 thực người dùng. ```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ý 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, 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ý 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ị 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: "Android - Xử lý sự kiện paywall" description: "Xử lý hiệu quả các sự kiện đăng ký trên Android với các công cụ theo dõi sự kiện của Adapty." --- :::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](android-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 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. 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 muốn kiểm soát hoặc theo dõi các quy trình diễn ra trên màn hình mua hàng, hãy triển khai các phương thức `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ể kế thừa `AdaptyUiDefaultEventListener` và chỉ ghi đè 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\} Phương thức này sẽ đượ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): ```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\} Phương thức này sẽ được gọi khi người dùng bắt đầu quá trình mua hàng: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseStarted( 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" } } ```
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 chi tiết. #### Mua hàng thành công, đã hủy hoặc đang chờ xử lý \{#successful-canceled-or-pending-purchase\} Phương thức này sẽ được gọi khi giao dịch mua thành công: ```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ấp để 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 này. 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 chi tiết. #### Mua hàng thất bại \{#failed-purchase\} Phương thức này sẽ được gọi khi giao dịch mua thất bại do lỗ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 vào đó, và các khoản thanh toán đ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ấp để 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 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ó thao tác 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: ```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. 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ấp để 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\} Phương thức này sẽ được gọi khi khôi phục giao dịch mua thành công: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreSuccess( profile: AdaptyProfile, context: Context, ) {} ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } ```
Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` cần thiết. Tham khảo chủ đề [Trạng thá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\} Phương thức này sẽ được gọi nếu `Adapty.restorePurchases()` thất bạ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 đang có gói đăng ký khác 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 ghi đè phương thức này. Bạn có hai tùy 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 ghi đè 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(...)) // tùy chọn - để thay thế gói đăng ký hiện tại .withOfferPersonalized(true) // tùy chọn - nếu sử dụng giá cá nhân hóa .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 ghi đè 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 cần tiếp tục và gói mới được thêm riêng biệt, 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 các 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 cáo lỗi bằng cách gọi phương thức này: ```kotlin showLineNumbers title="Kotlin" public override fun onLoadingProductsFailure( error: AdaptyError, context: Context, ): Boolean = false ```
Ví dụ sự kiện (Nhấp để 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ử lại yêu cầu sau 2 giây. #### Lỗi hiển thị \{#rendering-errors\} Nếu xảy ra lỗi trong quá trình hiển thị giao diện, lỗi đó sẽ được báo cáo bằng cách gọi phương thức này: ```kotlin showLineNumbers title="Kotlin" public override fun onRenderingError( error: AdaptyError, context: Context, ) {} ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ```
Trong điều kiện bình thường, các lỗi như vậy không nên xảy ra, vì vậy nếu bạn gặp phải, vui lòng 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` trong dự án Android của bạn. 2. Gọi phương thức `.setFallback` **trước khi** bạn lấy 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ố: | 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 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: "Khắc phục sự cố Paywall Builder trong Android SDK" description: "Khắc phục sự cố Paywall Builder trong Android 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 Android SDK. ## Lấy cấu hình paywall thất bại \{#getting-a-paywall-configuration-fails\} **Vấn đề**: Phương thức `getViewConfiguration` không thể truy xuất cấu hình paywall. **Nguyên nhân**: Paywall chưa được bật hiển thị trên thiết bị trong Paywall Builder. **Giải pháp**: Bật toggle **Show on device** trong Paywall Builder. ## Số lượt xem paywall 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 `logShowPaywall` trong code, điều này làm nhân đôi số lượt xem 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, vì vậy bạn không cần dùng phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowPaywall` trong code khi đang sử dụng Paywall Builder. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các sự cố liên quan đến Paywall Builder chưa được đề cập ở trên. **Giải pháp**: 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 vấn đề đã được giải quyết trong các phiên bản SDK mới hơn. --- # File: android-quickstart-manual --- --- title: "Bật tính năng mua hàng trong custom paywall trên Android SDK" description: "Tích hợp Adapty SDK vào custom paywall Android để 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 custom paywall của bạn. Bạ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ý giao dịch mới và khôi phục giao dịch cũ. :::important **Hướng dẫn này dành cho các nhà phát triển đang triển khai custom paywall.** Nếu bạn muốn cách đơn giản nhất để bật tính năng mua hàng, hãy sử dụng [Adapty Paywall Builder](android-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 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á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 thay đổi sản phẩm, giá cả và ưu đãi mà không cần chỉnh sửa code ứng dụng. - [**Placement**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (như `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trên dashboard, sau đó gọi 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ị 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 làm việc với custom paywall. 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 custom paywall, 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 trên dashboard, hãy theo dõi hướng dẫn khởi đầ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 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 sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho custom paywall, 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`. ```kotlin showLineNumbers fun loadPaywall() { Adapty.getPaywall("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value Adapty.getPaywallProducts(paywall) { 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.getPaywall("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); Adapty.getPaywallProducts(paywall, 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. Nhận giao dịch mua hàng \{#step-2-accept-purchases\} Khi người dùng nhấn vào một sản phẩm trong custom paywall, 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. ```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(); // Purchase successful, profile updated } else if (purchaseResult instanceof AdaptyPurchaseResult.UserCanceled) { // User canceled the purchase } else if (purchaseResult instanceof AdaptyPurchaseResult.Pending) { // Purchase is pending (e.g., user will pay offline with cash) } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); } ``` ## Bước 3. Khôi phục giao dịch mua hàng \{#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 hàng 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. ```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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. [Kiểm thử giao dịch mua hàng trên Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một giao dịch 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 [ProductListFragment.kt](https://github.com/adaptyteam/AdaptySDK-Android/blob/master/app/src/main/java/com/adapty/example/ProductListFragment.kt) trong ứng dụng ví dụ của chúng tôi, minh họa cách xử lý giao dịch mua hàng với error handling đầy đủ, 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 hàng chưa](android-check-subscription-status) để quyết định có 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 thông tin paywall và sản phẩm cho paywall remote config trong Android SDK" description: "Lấy thông tin paywall và sản phẩm trong Adapty Android SDK để tối ưu hóa doanh thu." --- Trước khi hiển thị remote config và các paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến remote config và các paywall tùy chỉnh. Để biết hướng dẫn lấy 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 (nhấn để mở rộng) 1. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào 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-android) trong ứng dụng của bạn.
## Lấy thông tin paywall \{#fetch-paywall-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng tại các placement cụ thể trong ứng dụ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 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ý các thay đổi này một cách linh động — 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("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 | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

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

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

Tuy nhiên, nếu bạn cho rằng người dùng của mình có kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ trải nghiệm thời gian tải 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 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 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 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 để lấy paywall nhanh hơn và một server dự phòng riêng 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 yếu.

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

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

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

| Đừng hardcode ID sản phẩm! Vì paywall được cấu hình từ xa, các sản phẩm khả dụng, số lượng sản phẩm và ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được các tình huống này. Ví dụ, nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị 2 sản phẩm đó. Tuy nhiên, nếu sau này bạn lấy được 3 sản phẩm, ứng dụng nên hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn phải hardcode là placement ID. Tham số phản hồi: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Đối tượng [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) với: danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy 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/) với: định danh sản phẩm, tên sản phẩm, giá, tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi tự xây dựng giao diện paywall, bạn có thể 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, nhưng hãy tham khảo tài liệu được liên kết để xem đầy đủ tất cả các thuộc tính. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | Để hiển thị tiêu đề sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải locale của thiết bị. | | **Price** | Để hiển thị phiên bản đã được bản địa hóa của giá, dùng `product.price.localizedString`. Bản địa hóa này dựa trên thông tin locale của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price.amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscriptionDetails?.localizedSubscriptionPeriod`. 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 kiểu 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ố đơn vị chu kỳ. Ví dụ, với gói đăng ký hàng 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 cho thấy gói đăng ký có ưu đãi giới thiệu, hãy xem 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`: 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í, hãy tìm `0` ở đây.
• `localizedNumberOfPeriods`: chuỗi được bản địa hóa theo locale của thiết bị mô tả độ dài ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày hiển thị `3 days` trong trường này.
• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy chi tiết riêng lẻ của chu kỳ ưu đãi với thuộc tính này. Nó hoạt động tương tự cho ưu đãi như phần trước đã mô tả.
• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo locale của người dùng. | ## 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 trường hợp bạn có nhiều đối tượng và paywall, và người dùng có kết nối internet yếu, việc lấy paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị 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 điều này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement đã 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 vài 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 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 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 nhược điểm này để có lợi ích từ việc lấy 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` đượ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 được xác định là mã ngôn ngữ gồm một hoặc nhiều thẻ con phân cách bởi 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](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.

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

Tuy nhiên, nếu bạn cho rằng người dùng của mình có kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ trải nghiệm thời gian tải 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 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 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 dọn dẹp thủ công.

| --- # File: present-remote-config-paywalls-android --- --- title: "Hiển thị paywall được thiết kế bằng Remote Config trong 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ự 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 linh hoạt và tùy biến theo 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 paywall sẽ 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 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à 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 } }); ``` Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang trực quan hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng xoay 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 các thiết bị khác nhau. :::warning Hãy đảm bảo [ghi nhận 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 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 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 tình huống đó. ## Ghi nhận 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ự 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 paywall. Để ghi nhận một sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị paywall được tạo trong [paywall builder](adapty-paywall-builder). ::: ```kotlin showLineNumbers 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://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 ở chế độ Observer trên Android SDK" description: "Tìm hiểu cách hiển thị paywall ở chế độ observer bằng Paywall Builder của Adapty." --- 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 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 tham khảo chủ đề [Android - Hiển thị paywall bằng Paywall Builder](android-present-paywalls). :::
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 Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo 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 cho chúng](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 5. [Tạo placement và gán paywall vào chúng](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 của bạn.

1. Triển khai `AdaptyUiObserverModeHandler`. Sự kiện `onPurchaseInitiated` sẽ thông báo cho bạn khi người dùng bắt đầu thực hiện mua hàng. 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 giao dịch ở chế độ Observer, 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 triển khai khôi phục tùy chỉnh: ```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 để paywall 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 đã kết thúc. | | 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 đã kết thúc. | 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 đó, 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, 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 đó, sử dụng hàm composable sau: ```kotlin showLineNumbers AdaptyPaywallScreen( viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, ) ``` 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 gian 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. | | **ViewConfiguration** | bắt buộc | Cung cấp đối tượng `AdaptyViewConfiguration` chứa thông tin trực quan của paywall. Sử dụng phương thức `Adapty.getViewConfiguration(paywall)` để tải nó. Tham khảo chủ đề [Lấy cấu hình trực quan của paywall](#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) để biết thêm chi tiết. | | **EventListener** | tùy chọn | Cung cấp `AdaptyUiEventListener` để theo dõi các sự kiện paywall. Khuyến nghị mở rộng 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á 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` để phân giải các custom tag trong văn bản paywall. Resolver này nhận tham số tag và phân giải 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 cho chế độ Observer | `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ó bằng 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, StoreKit1: đố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) trong 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 Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo 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 vào chúng](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 của bạn.
1. Triển khai `AdaptyUiObserverModeHandler`. Callback của `AdaptyUiObserverModeHandler` (`onPurchaseInitiated`) thông báo cho bạn khi người dùng bắt đầu thực hiện mua hàng. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình trong 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 cho AdaptyUI. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị loader: | Callback trong Kotlin | Callback trong Java | Mô tả | | :----------------- | :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------- | | onStartPurchase() | onStartPurchase.invoke() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã bắt đầu. | | onFinishPurchase() | onFinishPurchase.invoke() | Callback cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã hoàn thành (thành công, 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 đó, 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ị. 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 gian 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. | | **ViewConfiguration** | bắt buộc | Cung cấp đối tượng `AdaptyViewConfiguration` chứa thông tin trực quan của paywall. Sử dụng phương thức `Adapty.getViewConfiguration(paywall)` để tải nó. Tham khảo chủ đề [Lấy cấu hình trực quan 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 | Định nghĩa đối tượng `AdaptyPaywallInsets` chứa thông tin về vùng bị system bar che khuất, tạo margin dọc cho nội dung. Nếu cả status bar lẫn navigation bar không che `AdaptyPaywallView`, truyền `AdaptyPaywallInsets.NONE`. Đối với chế độ toàn màn hình khi system bar che một phần giao diện, lấy insets theo hướng dẫn bên dưới bảng. | | **EventListener** | tùy chọn | Cung cấp `AdaptyUiEventListener` để theo dõi các sự kiện paywall. Khuyến nghị mở rộng 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á 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` để phân giải các custom tag trong văn bản paywall. Resolver này nhận tham số tag và phân giải 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 cho chế độ Observer | `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ó bằng 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, StoreKit1: đố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) trong thư viện billing.

| Đối với chế độ toàn màn hình khi system bar che một phần giao diện, 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` | Đối tượng đạ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. :::
--- # 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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: 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." --- Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng trình thiết kế 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 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 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 thiết kế 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 lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần triển khai theo dõi view riêng biệt. Để 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 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 | 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 được kỳ vọng là mã ngôn ngữ bao gồm một hoặc hai subtag được phân cách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.

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

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

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

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

Tuy nhiên, nếu bạn cho rằng người dùng của mình có kết nối internet không ổn định, hãy xem xét 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ó tốc độ 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 để tránh các yêu cầu mạng là an toàn.

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

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 để 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 onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả trong trường hợp kết nối internet kém.

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

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

Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với thời gian đượ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.

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

| Tham số phản hồi: | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://android.adapty.io/adapty/com.adapty.models/-adapty-onboarding/) với: 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 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 đó, 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à hơn là 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 lấy onboarding 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 onboarding bằng phương thức `getOnboarding`, như được mô tả chi tiết trong phần [Lấy Onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, đòi hỏi thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không đúng. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ việc nhắm mục tiêu 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 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 dùng `getOnboardingForDefaultAudience` như bên dưới. Nếu không, hãy sử dụng `getOnboarding` như được 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 được kỳ vọng là mã ngôn ngữ bao gồm một hoặc hai subtag được phân cách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.

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

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

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

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

Tuy nhiên, nếu bạn cho rằng người dùng của mình có kết nối internet không ổn định, hãy xem xét 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ó tốc độ 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 để tránh các yêu cầu mạng là an toàn.

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

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 để 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 onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả trong trường hợp kết nối internet kém.

| --- # 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 mức độ tương tác với người dùng." --- 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 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 đó đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. Để hiển thị onboarding trực quan trên màn hình thiết bị, trước tiên bạn phải cấu hình nó. Để làm điều nà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ị. 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 màn hình mượt mà giữa splash screen và onboarding \{#add-smooth-transitions-between-the-splash-screen-and-onboarding\} Theo mặc định, giữa splash screen và onboarding, bạn sẽ thấy màn hình loading cho đến khi onboarding được tải xong. Tuy nhiên, nếu bạn muốn tạo hiệu ứng chuyển màn hình mượt mà hơn, bạn có thể tùy chỉnh để 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 placeholder (nội dung sẽ được hiển thị trong khi onboarding đang tải) ở đó. Nếu bạn định nghĩa một placeholder, onboarding sẽ được tải ở nền và tự động hiển thị khi sẵn sàng. ## Tắt safe area padding \{#disable-safe-area-paddings\} Theo mặc định, onboarding view tự động áp dụng safe area padding để tránh các thành phần UI hệ thống như status bar và navigation bar. 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ể làm 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 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 ra toàn màn hình mà không có padding tự động, 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 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ở đi. ::: Theo mặc định, các liên kết trong onboarding mở trong trình duyệt in-app. Đ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, cho phép họ xem mà không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết trong trình duyệt 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." --- 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). 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. Để 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 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 **tùy chỉnh** cho một nút và gán cho nó một ID. Sau đó, bạn có thể 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 `onCustomAction` sẽ được kích hoạt với action ID từ builder. Bạn có thể tự tạo ID, chẳng hạn 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 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 đó. ::: 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 onboarding đã đóng, 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 flow onboarding. Đối tượng `event` có thể là một trong các loại sau: |Loại | Mô tả | |------------|-------------| | `OnboardingStarted` | Khi onboarding đã được tải | | `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. | | `Unknown` | 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 | | `totalScreens` | Tổng số màn hình trong flow | Đây là ví dụ về cách bạn có thể sử dụng các sự kiện analytics để theo dõi: ```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ụ các sự kiện (Nhấn để mở rộng) ```javascript // OnboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // ScreenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // ScreenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // SecondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // UserEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // OnboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: android-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong Android SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng Android của bạn với Adapty SDK." --- Khi người dùng trả lời câu hỏi quiz hoặc nhập dữ liệu vào ô 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 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\} ### Bổ sung 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](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 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 yêu cầu họ nhập email vào trường `email`. Trong code ứng dụng, nó có thể trông như sau: ```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 } } } } } ``` ### 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 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 xây dựng onboarding và gán các ID có ý nghĩa cho các tùy 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à [đặt thuộc tính tùy 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 action của nút đó](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-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-06-24T14:36:38.639Z_ _Successfully processed: 40/40 files_ # API - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-06-24T14:36:38.641Z Total files: 18 --- # File: developer-cli --- --- title: "Developer CLI" description: "Overview of the Adapty Developer CLI." --- The **Adapty Developer CLI** is a command-line tool for managing your Adapty account without opening the Dashboard. It provides the main configuration capabilities, accessible from your terminal or automated environments. **What you can do with the CLI:** - Create and configure iOS and Android apps in your Adapty account - Define access levels — the subscription tiers your app checks at runtime - Set up products and map them to App Store and Google Play store IDs - Create paywalls and assign products to them - Configure placements to fetch paywalls via the SDK :::link Using an AI assistant or MCP client? An [Adapty CLI skill](https://github.com/adaptyteam/adapty-cli/tree/main/skills/adapty-cli) is available to help LLMs work with the 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: "API guides" description: "Learn how to perform specific tasks using the server-side API." --- In this section, you can find guides that cover different use cases and help you perform specific tasks using the server-side API and the Adapty SDK. <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-06-24T14:36:38.654Z_ _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-06-24T14:36:38.655Z Total files: 43 --- # File: capacitor-sdk-overview --- --- title: "Capacitor SDK overview" description: "Learn about Adapty Capacitor SDK and its key features." --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Capacitor.svg?style=flat&logo=capacitor)](https://github.com/adaptyteam/AdaptySDK-Capacitor/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the [Adapty Capacitor SDK](https://github.com/adaptyteam/AdaptySDK-Capacitor/) to take the headache out of in-app purchases so you can focus on what you do best – building amazing apps. Here's what we handle for you: - Handle purchases, receipt validation, and subscription management out of the box - Create and test paywalls without app updates - Get detailed purchase analytics with zero setup - cohorts, LTV, churn, and funnel analysis included - Keep the user subscription status always up to date across app sessions and devices - Integrate your app with marketing attribution and analytics services using just one line of code :::note Before diving into the code, you'll need to integrate Adapty with App Store Connect and Google Play Console, then set up products in the dashboard. Check out our [quickstart guide](quickstart) to get everything configured first. ::: ## 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. Here's what we'll cover in the integration guide: 1. [Install & configure SDK](sdk-installation-capacitor): Add the SDK as a [dependency](https://www.npmjs.com/package/@adapty/capacitor) to your project and activate it in the code. 2. [Enable purchases through paywalls](capacitor-quickstart-paywalls): Set up the purchase flow so users can buy products. 3. [Check the subscription status](capacitor-check-subscription-status): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](capacitor-quickstart-identify): Associate users with their Adapty profiles to ensure their data is stored consistently across devices. ### See it in action Want to see how it all comes together? We've got you covered: **Sample apps**: Check out our complete examples that demonstrate the full setup: - [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) - [Advanced development tools](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/adapty-devtools) ## Main concepts Before diving into the code, let's get familiar with the key concepts that make Adapty work. The beauty of Adapty's approach is that only placements are hardcoded in your app. Everything else – products, paywall designs, pricing, and offers – can be managed flexibly from the Adapty dashboard without app updates: 1. **Product** - Anything available for purchase in your app – subscription, consumable product, or lifetime access. 2. **Paywall** - The only way to retrieve products from Adapty and use it to its full power. We've designed it this way to make it easier to track how different product combinations affect your monetization metrics. A paywall in Adapty serves as both a specific set of your products and the visual configuration that accompanies them. 3. **Placement** - A strategic point in your user journey where you want to show a paywall. Think of placements as the "where" and "when" of your monetization strategy. Common placements include: - `main` - Your primary paywall location - `onboarding` - Shown during the user onboarding flow - `settings` - Accessible from your app's settings Start with the basics like `main` or `onboarding` for your first integration, then think about where else in your app users might be ready to purchase. 4. **Profile** - When users purchase a product, their profile is assigned an **access level** which you use to define access to paid features. --- # 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 vào ứng dụng Capacitor của bạn: - **Core Adapty**: Module này bắt buộc để Adapty hoạt động trong ứng dụng. - **AdaptyUI**: Module này cần thiết nếu bạn dùng [Adapty Paywall Builder](adapty-paywall-builder) — công cụ no-code thân thiện để tạo paywall đa nền tảng. AdaptyUI được kích hoạt tự động cùng với module core. :::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-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/) yêu cầu các 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ợ. :::info Từ SDK v3.17 trở đi, Adapty SDK sử dụng Google Play Billing Library v8.0.0 theo mặc định. ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![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 ``` ## 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. 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 `"YOUR_PUBLIC_SDK_KEY"` trong code. :::important - Đảm bảo bạn dùng **Public SDK key** để khởi tạo Adapty, còn **Secret key** chỉ 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 chắc chắn 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 chờ `activate` hoàn thành trước khi gọi bất kỳ phương thức nào khác của Adapty SDK. Xem [Thứ tự gọi trong Capacitor SDK](capacitor-sdk-call-order) để biết trình tự đầy đủ. ::: :::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). ::: Tiếp theo, thiết lập paywall trong ứng dụng: - 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, 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 dự định 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 làm thêm gì. ## 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ấp độ log có sẵn: | Cấp độ | Mô tả | | ---------- | ------------------------------------------------------------ | | `error` | Chỉ ghi lại lỗi | | `warn` | Ghi lại lỗi và các thông báo từ SDK không gây lỗi nghiêm trọng nhưng đáng chú ý | | `info` | Ghi lại lỗi, cảnh báo và các thông báo thông tin | | `verbose` | Ghi lại mọi thông tin bổ sung có thể hữu ích khi debug, như lệnh gọi hàm, truy vấn API, v.v. | Bạn có thể đặt cấp độ 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 gửi rõ ràng, 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ủ hướng dẫn của cửa hàng hoặc quy định 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 thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Dùng tham số này để tăng cường quyền riêng tư của người dùng, tuân thủ các quy định bảo vệ dữ liệu 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 tính năng dựa trên IP không được yêu cầu 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ẻ ID quảng cáo \{#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 ID quảng cáo. Giá trị mặc định là `false`. Dùng tham số này để tuân thủ chính sách App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc 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ộ đệm media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Theo mặc định, AdaptyUI lưu bộ đệm media (như hình ảnh và video) để cải thiện hiệu suất và giảm sử dụng mạng. Bạn có thể tùy chỉnh cài đặt bộ đệm bằng cách cung cấp cấu hình tùy chỉnh. Dùng `mediaCache` để ghi đè cài đặt bộ đệm 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ố: | Tham số | Bắt buộc | Mô tả | |-----------|----------|-------------| | memoryStorageTotalCostLimit | tùy chọn | Tổng kích thước bộ đệm trong bộ nhớ tính bằng byte. Mặc định theo giá trị của nền tảng. | | memoryStorageCountLimit | tùy chọn | Giới hạn số lượng mục trong bộ nhớ đệm. Mặc định theo giá trị của 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 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 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 từ backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK phát hiện khi ứng dụng được khôi phục từ backup iCloud và xóa toàn bộ dữ liệu SDK được lưu cục bộ, bao gồm thông tin hồ sơ người dùng đã cache, chi tiết sản phẩm và paywall. Sau đó SDK khởi tạo lại với 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. ::: ```swift 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\} #### Xử lý lỗi kích hoạt SDK khi dùng live-reload của Capacitor \{#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, dùng tùy chọn `__ignoreActivationOnFastRefresh` và đặt nó thành biến cờ chế độ phát triển của Capacitor — giá trị này sẽ khác nhau tùy theo bundle bạn đang 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 } ``` ## 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, '14.0' # For core features only # OR +platform :ios, '15.0' # If using paywalls created in the paywall builder ``` #### Quy tắc backup Android (Cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `<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 gốc, chạy `npx cap sync android` để Capacitor cập nhật các tài nguyên đã thay đổi nếu bạn tạo lại nền tảng. ::: #### 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ị xử lý là đã hủy. Để đảm bảo mua hàng 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` của bạn, đả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 Capacitor cho iOS, bạn có thể thấy các lỗi biên dịch Swift 6 trên các pod target của Adapty. Các triệu chứng điển hình bao gồm lỗi không khớp `@Sendable` trong `AdaptyUIBuilderLogic`, thiếu conformance `Sendable` trên các kiểu Adapty, hoặc lỗi cô lập actor. Các pod Adapty khai báo `s.swift_version = '6.0'` và yêu cầu Swift 6 để build. Code ứng dụng của bạn vẫn có thể dùng Swift 5 — chỉ các pod target Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`, `AdaptyLogger`, `AdaptyPlugin`) 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. Để xác minh, 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 bằng cách sử dụng paywall trong Capacitor SDK" description: "Tìm hiểu cách hiển thị paywall trong ứng dụng Capacitor của bạn với Adapty SDK." --- Để 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** – 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** 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 danh sách cung cấp, giá cả và tổ hợp sản phẩm mà không cần chỉnh sửa code ứng dụng. - **Placement** – 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 đó 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. Adapty cung cấp cho bạn ba cách để bật tính năng mua hàng trong ứng dụng. 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 hàng trong trình tạo không cần code](quickstart-paywalls). Adapty tự động hiển thị paywall 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. | | `makePurchase` | 🟡 Trung bình | Bạn tự xây dựng UI paywall trong code ứng dụng, nhưng vẫn lấy đối tượng paywall từ Adapty để duy trì sự linh hoạt trong danh sách sản phẩm. Xem [hướng dẫn](capacitor-quickstart-manual). | | Observer mode | 🔴 Khó | Bạn tự triển khai toàn bộ flow mua hàng. Xem [hướng dẫn](implement-observer-mode-capacitor). | :::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 được tạo thủ công](capacitor-making-purchases). ::: Để hiển thị paywall được tạo trong Adapty paywall builder, trong code ứng dụng, bạn chỉ cần: 1. **Lấy paywall**: Lấy paywall từ Adapty. 2. **Hiển thị paywall và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị container paywall bạn đã lấy trong ứng dụng. 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 paywall với phản hồi của ứng dụng. Ví dụ: mở liên kết hoặc đóng paywall khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. Kết nối ứng dụng của bạn với [App Store](initial_ios) và/hoặc [Google Play](initial-android) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo paywall và thêm sản phẩm vào đó](create-paywall). 4. [Tạo placement và thêm paywall vào đó](create-placement). 5. [Cài đặt và kích hoạt 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 paywall \{#1-get-the-paywall\} Các paywall của bạn được liên kết với các placement được cấu hình trên dashboard. Placement cho phép bạn chạy các paywall khác nhau cho các đối tượng khác nhau hoặc để chạy [A/B test](ab-tests). Để lấy paywall được tạo trong Adapty paywall builder, bạn cần: 1. Lấy đối tượng `paywall` theo ID [placement](placements) bằng phương thức `getPaywall` và kiểm tra xem đây có phải là paywall được tạo trong builder hay không thông qua thuộc tính `hasViewConfiguration`. 2. Tạo view của paywall bằng phương thức `createPaywallView`. View chứa các thành phần UI và kiểu dáng cần thiết để hiển thị paywall. :::important Để lấy cấu hình view, bạn phải bật toggle **Show on device** trong Paywall Builder. Nếu không, bạn sẽ nhận được cấu hình view rỗng và paywall sẽ không được hiển thị. ::: ```typescript showLineNumbers title="Capacitor" try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', }); // the requested paywall } catch (error) { // handle the error } if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { // use your custom logic } ``` ## 2. Hiển thị paywall \{#2-display-the-paywall\} Khi bạn đã có cấu hình paywall, chỉ cần thêm vài dòng code để hiển thị paywall của bạn. Để hiển thị paywall, dùng phương thức `view.present()` trên `view` được tạo bởi phương thức `createPaywallView`. Mỗi `view` chỉ có thể được 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. ```typescript showLineNumbers title="Capacitor" try { await view.present(); } catch (error) { // handle the error } ``` :::tip Để biết thêm chi tiết về cách hiển thị paywall, xem [hướng dẫn](capacitor-present-paywalls) của chúng tôi. ::: ## 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, Capacitor SDK tự động xử lý mua hàng, khôi phục và đóng paywall. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được xác định trước và yêu cầu xử lý hành động trong code của bạn. Hoặc, bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ: bạn có thể muốn giữ paywall mở sau khi người dùng mở một liên kết web. Hãy xem cách bạn có thể xử lý điều này trong triển khai của mình. :::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) nút bấm. ::: ```typescript showLineNumbers title="Capacitor" const unsubscribe = view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); ``` ## Bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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\} Đây là cách tất cả các bước đó có thể được tích hợp cùng nhau trong ứng dụng của bạn. ```typescript showLineNumbers title="Capacitor" export default function PaywallScreen() { const showPaywall = async () => { try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', }); if (!paywall.hasViewConfiguration) { // use your custom logic return; } const view = await createPaywallView(paywall); view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); await view.present(); } catch (error) { // handle any error that may occur during the process console.warn('Error showing paywall:', error); } }; return ( <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}> <button onClick={showPaywall}>Show Paywall</button> </div> ); } ``` --- # 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ơ trong ứng dụng của bạn: 1. Dùng `adapty.addListener('onLatestProfileLoad')` để lắng nghe các thay đổi của 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 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. ```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 của bạn khởi động, cung cấp dữ liệu gói đăng ký đã được lưu 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 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 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 = () => { 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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-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ẽ dẫn bạn qua từng bước tích hợp Adapty vào ứng dụng Capacitor 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 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. ### Cách thiết lập 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ể tự tra cứu các giá trị trên dashboard — bạn sẽ phải cung cấp chúng. 1. **Kết nối cửa hà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 hỗ trợ cả hai nền tảng. Đây là bước bắt buộc để giao dịch mua hàng hoạt động. [Kết nối cửa hàng](integrate-payments) 2. **Sao chép Public SDK key**: 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.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 sản phẩm trực tiếp trong code — Adapty phân phối chú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()`. [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 ứ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 code. :::tip Khi đã có đủ năm yếu tố 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 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\} ### 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 tài liệu Adapty mới nhất. LLM 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. Để cài đặt, chạy: ``` npx ctx7 setup ``` Lệnh này sẽ tự phát hiện editor của bạn và cấu hình Context7 server. Để cài đặt thủ công, xem [kho GitHub của Context7](https://github.com/upstash/context7). Sau khi cấu hình, 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 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ên kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu code, hãy yêu cầu LLM 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 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 nào để xử lý giao dịch 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 công cụ no-code của Adapty, và SDK tự động hiển thị chúng. - [**Paywall tự tạo**](capacitor-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](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 phụ thuộc vào bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-capacitor) Gửi 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ỗi thường gặp:** "Public API key is missing" → kiểm tra lại 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 trong sandbox ngay khi triển khai xong — đừng để đến cuối. Xem [Test giao dịch trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn cài đặt. <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)](capacitor-quickstart-paywalls) - [Lấy Paywall Builder paywall và cấu hình của chúng](capacitor-get-pb-paywalls) - [Hiển thị paywall](capacitor-present-paywalls) - [Xử lý sự kiện paywall](capacitor-handling-events) - [Phản hồi hành động nút bấm](capacitor-handle-paywall-actions) Gửi cho LLM của bạn: ``` 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:** Paywall hiển thị với các sản phẩm đã 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ỗi thường gặp:** Paywall trống hoặc lỗi `getPaywall` → kiểm tra 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="Paywall tự tạo"> **Hướng dẫn:** - [Kích hoạt giao dịch mua trong paywall tùy chỉnh (quickstart)](capacitor-quickstart-manual) - [Lấy 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 giao dịch mua](capacitor-making-purchases) - [Khôi phục giao dịch mua](capacitor-restore-purchase) Gửi cho LLM của bạn: ``` 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] - **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ỗi thường gặp:** Mảng sản phẩm rỗng → kiểm tra 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) 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-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 giao dịch 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ỗi thường gặp:** Không có sự kiện → kiểm tra bạn đã báo cáo giao dịch cho Adapty chưa và server notification đã đượ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 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](capacitor-quickstart-identify) Gửi 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ỗi thường gặp:** Gọi `identify` sau khi kích hoạt nhưng trước khi lấy paywall để tránh attribution cho 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 đ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 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, server notification, 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ỗi thường gặp:** Thiếu server notification → 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 bối 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à [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho website dễ truy cập với LLM. Lưu ý: với một số AI agent (ví dụ: ChatGPT), bạn 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 gộp vào một file. Rất lớn — chỉ dùng khi bạn cần toàn cảnh đầy đủ. - Tập con 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): Tiết kiệm token hơn so với toàn bộ tài liệu. --- # File: capacitor-paywalls --- --- title: "Paywalls" description: "Learn how to work with paywalls in your Capacitor app with Adapty SDK." --- ## Display paywalls ### Adapty Paywall Builder <CustomDocCardList ids={['capacitor-get-pb-paywalls', 'capacitor-present-paywalls', 'capacitor-handling-events', 'capacitor-handle-paywall-actions']} /> :::tip To get started with the Adapty Paywall Builder paywalls quickly, see our [quickstart guide](capacitor-quickstart-paywalls). ::: ### Implement paywalls manually <CustomDocCardList ids={['capacitor-quickstart-manual', 'fetch-paywalls-and-products-capacitor', 'present-remote-config-paywalls-capacitor', 'capacitor-making-purchases']} /> For more guides on implementing paywalls and handling purchases manually, see the [category](capacitor-implement-paywalls-manually). ## Useful features <CustomDocCardList ids={['capacitor-use-fallback-paywalls', 'capacitor-web-paywall']} /> --- # File: capacitor-get-pb-paywalls --- --- title: "Lấy paywall Paywall Builder và cấu hình của chúng trong Capacitor SDK" description: "Tìm hiểu cách lấy PB paywall trong Adapty để kiểm soát gói đăng ký tốt hơn trong ứng dụng Capacitor 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) với 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 quy trình này là lấy paywall liên kết với placement và 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 paywall được tùy chỉnh bằng Paywall Builder. Để biết hướng dẫn về cách lấy Remote Config paywall, hãy tham khảo chủ đề [Lấy paywall và sản phẩm cho remote config paywalls trong ứng dụng di động 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 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-capacitor) 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 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ị bên trong paywall lẫn 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 mới hiển thị trong ứng dụng di độ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 để 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`: ```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 phải là mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân tách bởi dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ phụ 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> | | **params** | tùy chọn | Tham số bổ sung để lấy paywall. | **Đừ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ả chúng mà không cần thay đổi code. Tham số phản hồi: | Tham số | Mô tả | | :-------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | Một đố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 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 — điều này cho biết paywall đượ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ư một Paywall Builder paywall; nếu không, [xử lý nó như một Remote Config paywall](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 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 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 | Một đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **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ể để tạo nội dung cá nhân hóa 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ối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Khi là `true`, AdaptyUI sẽ tự động lấy 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, [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 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 đợ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. Để 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ư đã 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ố nhược điểm đá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 sẽ phải thiết kế paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với paywall không render được. - **Mất khả năng 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 nhược điểm này để 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` như đã 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 } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Capacitor 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 của bạn. | | **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ẻ phụ được phân tách bởi dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ phụ 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](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** | tùy chọn | 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 mục tiêu 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 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 Capacitor 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: ```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 chuyển về giao diện mặc định của nó. ::: --- # File: capacitor-present-paywalls --- --- title: "Hiển thị paywall Paywall Builder trong Capacitor SDK" description: "Hiển thị paywall trong ứng dụng Capacitor bằng Adapty." --- 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ị. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder**. Quy trình hiển thị paywall khác nhau đố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ị 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`](capacitor-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 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 developer định nghĩa \{#use-developer-defined-timer\} Để sử dụng timer do developer định nghĩa trong ứng dụng di động của bạn, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer do developer định nghĩa mà bạn đã thiết lập trong Adapty dashboard. Điều này đảm bảo ứng dụng của bạn cập nhật timer động với giá trị chính xác — như `13d 09h 03m 34s` (được tính bằng thời gian 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). ```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 developer đị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 cập nhật động với giá trị chính xác — như `13d 09h 03m 34s` (được tính bằng thời gian 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). ## 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 thông thường khi một paywall view đang được hiển thị trên Android. Trên Android, các alert thông thường xuất hiện phía sau paywall view, khiến người dùng không 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 trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. ```typescript showLineNumbers await view.present({ iosPresentationStyle: 'page_sheet' }); ``` --- # File: capacitor-handle-paywall-actions --- --- title: "Phản hồi hành động button trong Capacitor SDK" description: "Xử lý hành động button trên paywall trong Capacitor bằng Adapty để tối ưu hóa việc kiếm tiền từ ứng dụng." --- Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập button đúng cách là rất quan trọng: 1. Thêm một [button 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 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. ## Đóng paywall \{#close-paywalls\} Để thêm một button đóng paywall: 1. Trong paywall builder, thêm một button 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. :::info Trong Capacitor SDK, hành động `close` 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. ::: ```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 mua), hãy thêm phần tử **Link** trong paywall builder và xử lý nó theo cách tương tự như button với hành động **Open URL**. ::: Để thêm một button 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 button, 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. :::info Trong Capacitor SDK, hành động `window.open` mặc định sẽ kích hoạt 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 một button cho phép người dùng đăng nhập vào ứng dụng: 1. Trong paywall builder, thêm một button và gán cho nó hành động **Login**. 2. Trong code ứng dụng, triển khai 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 button xử lý các hành động khác: 1. Trong paywall builder, thêm một button, 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ó 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 button để hiển thị paywall khác: ```typescript showLineNumbers const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another paywall console.log('User requested new paywall'); } }, }); ``` --- # File: capacitor-handling-events --- --- title: "Capacitor - Xử lý sự kiện paywall" description: "Xử lý sự kiện gói đăng ký trong Capacitor với SDK của Adapty." --- :::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](capacitor-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, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua trên paywall. Tìm hiểu cách phản hồi những sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các quy trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai 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 mà bạn cần và bỏ qua những cái không cần. Trong trường hợp đó, các event listener không dùng đến sẽ không được tạo. Không có event handler nào là bắt buộc. Các event handler trả về kiểu 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 của view này sẽ bị gỡ bỏ. 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 hiển thị thất bại. - `onUrlPress`: mở URL trên trình duyệt hệ thống và giữ paywall mở. ### Các event handler \{#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** của hệ thống Android. | | **onCloseButtonPress** | Được gọi khi nút đóng hiển thị và người dùng nhấp vào đó. Nên đóng màn hình paywall trong handler 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. Trong trường hợp mua thành công, sự kiện này cung cấp `AdaptyProfile` đã được cập nhật. 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 mua. | | **onPurchaseCancelled** | Được gọi khi người dùng khởi tạo quá trình mua và tự tay 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, xác minh giao dịch thất bại). 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 này 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` cần thiết. Tham khảo 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 paywall view được chọn, cho phép bạn theo dõi những gì người dùng chọn trước khi mua. | | **onAppeared** | Được gọi khi paywall view 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 một paywall và web paywall mở ra trong trình duyệt trong ứng dụng. | | **onDisappeared** | Được gọi khi paywall view biến mất khỏi màn hình. Trên iOS, 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 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 hiển thị view 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ủ. | --- # 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 \{#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). 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 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 }); ``` Tham số: | Tham số | Mô tả | | :------------------- | :------------------------------------------------------- | | **fileLocation** | Đối tượng đại diện cho vị trí của file cấu hình dự phòng. | --- # File: capacitor-localizations-and-locale-codes --- --- title: "Sử dụng localizations và mã locale trong Capacitor SDK" description: "Tìm hiểu cách localize paywall trong ứng dụng Capacitor 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ã locale được dùng đến — 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ã locale khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi dựa vào 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, vì các mã này phức tạp, điều quan trọng là bạn phải hiểu chính xác mình đang gửi gì lên server để nhận đúng localization, 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\} Với mã locale, 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ã gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số ví dụ: `en` (Tiếng Anh), `pt-br` (Tiếng Bồ Đào Nha (Brazil)), `zh` (Tiếng Trung Giản thể), `zh-hant` (Tiếng Trung Phồn thể). ## Khớp mã locale \{#locale-code-matching\} Khi Adapty nhận được yêu cầu từ SDK phía client kèm mã locale và bắt đầu tìm localization tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi locale đầu vào được chuyển về 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 localization có mã locale 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` từ `pt-br`) và tì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ờ vậy, thiết bị iOS gửi `'pt_BR'`, thiết bị Android gửi `pt-BR`, và thiết bị khác gửi `pt-br` đều nhận được cùng một kết quả. ## Cách triển khai localizations được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang tìm hiểu về localizations, có khả năng bạn đã làm việc với các file chuỗi đã localize trong dự án. Nếu vậy, chúng tôi khuyến nghị đặt một cặp key-value chứa mã locale Adapty dự kiến vào mỗi file tương ứng. Sau đó trích xuất giá trị theo key này 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); }; }; ``` 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 trong ứng dụng. ## Cách triển khai localizations: hướng 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 tường minh mã locale cho từng localization. Điều đó 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: ```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 muốn localization được chọn đúng, bạn sẽ phải dựa vào logic của Apple (hoạt động ngay lập tức nếu bạn dùng cách được khuyến nghị với file chuỗi đã localize), hoặc tự tái tạo lại logic đó. 2. Khó đoán chính xác server của Adapty sẽ nhận được gì. Ví dụ, trên iOS có thể lấy được locale như `ar_OM@numbers='latn'` từ thiết bị rồi gửi lên server. Với lời 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ể ngoài ý muốn. Nếu bạn vẫn quyết định dùng cách này — hãy đảm bảo bạn đã xử lý đầy đủ tất cả các trường hợp liên quan. --- # File: capacitor-web-paywall --- --- title: "Implement web paywalls" description: "Tìm hiểu cách triển khai web paywalls 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 paywalls \{#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 paywalls 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 mộ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 rồi 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ờ 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 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à cũng 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 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 trên 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 trên web | Xác minh cấu hình sản phẩm trong Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Không thể mở URL trên 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 paywalls trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} :::important Mở web paywalls 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 paywalls 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 paywalls trong trình duyệt ngay trong ứng dụng. Điều này hiển thị trang mua hàng trên web ngay trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt `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-implement-paywalls-manually --- --- title: "Implement paywalls manually" description: "Learn how to implement paywalls manually in your Capacitor app with Adapty SDK." --- ## Accept purchases If you are working with paywalls you've implemented yourself, you can delegate handling purchases to Adapty, using the `makePurchase` method. This way, we will handle all the user scenarios, and you will only need to handle the purchase results. :::important `makePurchase` works with products created in the Adapty dashboard. Make sure you configure products and ways to retrieve them in the dashboard by following the [quickstart guide](quickstart). ::: <CustomDocCardList ids={['capacitor-quickstart-manual', 'fetch-paywalls-and-products-capacitor', 'present-remote-config-paywalls-capacitor', 'capacitor-making-purchases', 'capacitor-restore-purchase']} /> ## Observer mode If you want to implement your own purchase handling logic from scratch but still want to benefit from the advanced analytics in Adapty, you can use the observer mode. :::important Consider the observer mode limitations [here](observer-vs-full-mode). ::: <CustomDocCardList ids={['implement-observer-mode-capacitor', 'report-transactions-observer-mode-capacitor']} /> --- # File: capacitor-quickstart-manual --- --- title: "Kích hoạt giao dịch mua trong paywall tùy chỉnh của bạn trong Capacitor SDK" description: "Tích hợp Adapty SDK vào các paywall tùy chỉnh trên Capacitor để 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 hoàn toà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 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 giao dịch mua, hãy sử dụng [Adapty Paywall Builder](capacitor-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic giao dịch, và bạn có thể kiểm tra 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 cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn chỉnh sửa sản phẩm, giá cả và ưu đãi mà không cần thay đổi code ứng dụng. - [**Placements**](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 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 nhóm 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 bạn 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 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 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 cách khác nhau. Đọc [hướng dẫn bắt đầu nhanh về nhận dạng người dùng](capacitor-quickstart-identify) để hiểu rõ các đặc điểm và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `paywall` bằng cách truyền [placement](placements) ID vào phương thức `getPaywall`. 2. Lấy mảng sản phẩm cho paywall này bằng phương thức `getPaywallProducts`. ```typescript showLineNumbers async function loadPaywall() { try { const paywall: AdaptyPaywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID' }); const products: AdaptyPaywallProduct[] = await adapty.getPaywallProducts({ paywall }); // Use products to build your custom paywall UI } catch (error) { // Handle the error } } ``` ## 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. Phương thức này sẽ xử lý luồng giao dịch và trả về hồ sơ người dùng đã 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 khôi phục giao dịch mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Phương thức này sẽ đồng bộ lịch sử giao dịch của họ với Adapty và trả về hồ sơ người dùng đã 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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 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 sản xuất, hãy xem [App.tsx](https://github.com/adaptyteam/AdaptySDK-Capacitor/blob/master/examples/adapty-devtools/src/screens/app/App.tsx) trong ứng dụng ví dụ 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 phù hợp, trạng thái tải và tích hợp SDK toàn diện. Tiếp theo, [kiểm tra xem người dùng đã hoàn thành giao dịch mua chưa](capacitor-check-subscription-status) để xác định có nên hiển thị paywall hay cấp quyền truy cập các tính năng trả phí. --- # File: fetch-paywalls-and-products-capacitor --- --- title: "Lấy paywalls và sản phẩm cho remote config paywalls trong Capacitor SDK" description: "Lấy paywalls và sản phẩm trong Adapty Capacitor SDK để tăng cường monetization cho người dùng." --- Trước khi hiển thị remote config và custom paywalls, 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 paywalls. Để biết hướng dẫn lấy paywalls cho Paywall Builder, vui lòng tham khảo [Lấy Paywall Builder paywalls và cấu hình của chúng](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 paywalls và sản phẩm trong ứng dụng (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 paywall](create-paywall) trong Adapty Dashboard. 3. [Tạo placements và thêm paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-capacitor) trong ứng dụ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 paywalls, cho phép bạn hiển thị chúng tại các placement cụ thể trong ứng dụng. Để hiển thị sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placements](placements) của bạn 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 | Đị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 subtag được phân tách bằng 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` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.</p><p></p><p>Xem [Localizations and locale codes](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ừ server 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 người dùng của bạn 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 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 để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.</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`, vì quá trình có thể bao gồm nhiều request khác nhau bên dưới.</p> | **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywalls được cấu hình từ xa, vì vậy số lượng sản phẩm và ư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 paywall hôm nay 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. Tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Đối tượng [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) 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ó: ```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 đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) bao gồm: định danh sản phẩm, tên sản phẩm, giá, tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi tự thiết kế giao diện paywall, bạn 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 dùng phổ biến nhất, nhưng hãy 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 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 locale của thiết bị. | | **Price** | Để hiển thị giá đã được bản địa hóa, dùng `product.price?.localizedString`. Bản địa hóa này dựa trên thông tin locale của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price?.amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price?.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedSubscriptionPeriod`. 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 dạng lập trình, dùng `product.subscription?.subscriptionPeriod`. Từ đó bạn có thể truy cập thuộc tính `unit` để lấy độ dài (tức là 'day', 'week', 'month', 'year', hoặc 'unknown'). Giá trị `numberOfUnits` sẽ cho bạn 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ỉ 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.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ó loại `'free_trial'`.<br/>• `price`: Giá giảm dưới dạng số. Với dùng thử miễn phí, giá trị này là `0`.<br/>• `localizedNumberOfPeriods`: 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 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. 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 locale của người dùng cho phần giảm giá. | ## Tăng tốc lấy paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywalls được lấy gần như ngay lập tức nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywalls, và người dùng có 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 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ể dùng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như được mô tả chi tiết trong phần [Lấy thông tin paywall](fetch-paywalls-and-products-capacitor#fetch-paywall-information) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywalls khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế paywalls hỗ trợ phiên bản hiện tại (legacy) hoặc chấp nhận rằng người dùng phiên bản hiện tại (legacy) có thể gặp vấn đề với paywalls không render được. - **Mất 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 dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để được hưởng lợi từ việc lấy paywall nhanh hơn, hãy 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-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); } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Capacitor 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 subtag được phân tách bằng 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` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.</p><p></p><p>Xem [Localizations and locale codes](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ừ server 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 người dùng của bạn 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 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 để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.</p> | --- # 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 trình bày paywall Remote Config trong Adapty Capacitor 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. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn kiểm soát những gì được hiển thị 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, giúp bạn tự do trình bày 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); } ``` Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang trực quan hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều 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](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 hoàn tất việc hiển thị paywall, 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](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 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 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)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```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). | --- # 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 là bước không thể thiế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 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 đóng vai trò là cổng để người dùng tương tác với paywall và thực hiện 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. Hãy đảm bảo bạn đã [hoàn tất 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 hàng. ## 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. **Muốn có 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ố yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------| | **product** | bắt buộc | Đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **result** | Đối tượng [`AdaptyPurchaseResult`](https://capacitor.adapty.io/types/adaptypurchaseresult) với trường `type` cho biết kết quả mua hàng (`'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 mua hàng \{#change-subscription-when-making-a-purchase\} Khi người dùng chọn một gói đăng ký mới thay vì gia hạn gói hiện tại, cách thức hoạt động phụ thuộc vào cửa hàng: - Với App Store, gói đăng ký được cập nhật tự động 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 đang có gói đăng ký từ nhóm khác, cả hai sẽ cùng hoạt động. - Với Google Play, gói đăng ký không được cập nhật tự động. Bạn cần tự 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 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 | Đối tượng 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: - [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ỉ khả dụng khi 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ỉ 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: { enablePendingPrepaidPlans: true, }, } }); ``` ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <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\} <Callout type="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. </Callout> 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: ```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, 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 bạn nên chuyển hướng người dùng thẳng đế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 mobile 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." --- Khôi phục giao dịch 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í 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 phải thanh toán lại. :::note Với các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), giao dịch 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 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 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** | Đố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 đăng ký của người dùng trong Capacitor 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 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 phù hợp với nhu cầu của bạn, bạn chỉ cần: 1. Bật nó lê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 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ừ 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 chỉ 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ý việc đó. ::: ```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ố: | 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: "Users & access" description: "Learn how to work with users and access levels in your Capacitor app with 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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: 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ý của người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng trong ứng dụng 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 ID sản phẩm vào code của mình. 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, thiết lập [App Store Server Notifications](enable-app-store-server-notifications) - Đối với Android, 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ị 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](capacitor-identifying-users#setting-customer-user-id-on-configuration), sau đó cập nhật 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 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](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ừ 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()`: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); console.log('Profile retrieved successfully'); } catch (error) { console.error('Failed to get profile:', error); } ``` Các 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 hay không. Phương thức `.getProfile` luôn cố gắng truy vấn API để cung cấp kết quả mới nhất. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ server, dữ liệu từ cache sẽ được trả về. Cần lưu ý rằng Adapty SDK thường xuyên cập nhật cache của `AdaptyProfile` để giữ thông tin luôn được cập nhật nhất có thể. | Phương thức `.getProfile()` trả về hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Ví dụ, nếu bạn có ứng dụng báo chí 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 là "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 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\} 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 các 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. ### 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 từ 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 cập dữ liệu trực tiếp từ cache. SDK định kỳ truy vấn server mỗi phút để kiểm tra các 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ữ cache luôn nhất quán với server. --- # 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: "Learn how to work with onboardings in your Capacitor app with Adapty SDK." --- <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." --- Sau khi [bạn đã thiết kế phần hiển thị cho onboarding](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 builder không cần code của chúng tôi, nó được lưu trữ dưới dạng một container với cấu hình mà ứng dụng của bạn cần lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu form nhập vào). 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 để ảnh có đủ thời gian tải về trước khi hiển thị cho người dùng. Để lấy một onboarding, sử 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 instance view. :::warning Kết quả của phương thức `createOnboardingView` chỉ có thể dùng một lần. Nếu bạn cần dùng lại, hãy gọi phương thức `createOnboardingView` một lần nữa. ::: ```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 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 phân cách bằng 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` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.</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> | | **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 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 kết nối internet không ổn định, hãy cân nhắc sử 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 họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ không ổn định đến đâu. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng 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 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> | | **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ị chỉ định trong `loadTimeoutMs`, 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://capacitor.adapty.io/interfaces/adaptyonboarding) bao gồ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 lấy onboarding bằng onboarding dành cho đố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 các 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 vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như được mô tả chi tiết trong phần [Lấy onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, yêu cầu thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không đúng. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ việc nhắm mục tiêu theo quốc gia, attribution, hoặc các thuộc tính tùy chỉnh. Nếu việc lấy nhanh hơn có lợi hơn những hạn chế 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 sử 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); } ``` 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 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 phân cách bằng 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` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.</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> | | **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 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 kết nối internet không ổn định, hãy cân nhắc sử 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 họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ không ổn định đến đâu. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng 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 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> | --- # 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." --- 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 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 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ị 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 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. ::: ```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. ::: Theo mặc định, các liên kết trong onboarding mở trong trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị các trang web ngay trong ứng dụng của bạn, cho phép người dùng xem mà không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết trong trình duyệt ngoài thay thế, 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." --- 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ý. 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 event handler \{#set-up-event-handlers\} Để xử lý sự kiện cho các onboarding, hãy 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ý 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 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**, event handler sẽ được kích hoạt với tham số `actionId` trùng với **Action ID** trong builder. Bạn có thể tự tạo ID, ví dụ "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ự quản 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 onboarding đóng, 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 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ị paywall lên trên onboarding, bạn không thể điều khiển onboarding ở nền bằng lập trình. Việc cố gắng dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn dismiss view onboarding trước khi hiển thị paywall. ```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); }, }); ``` Đối tượng `event` có thể là một trong các loại sau: | Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình 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` | Cho bất kỳ loại sự kiện không xác định nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của flow onboarding | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | <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: capacitor-onboarding-input --- --- title: "Xử lý dữ liệu từ onboardings trong Capacitor SDK" description: "Lưu và sử dụng dữ liệu từ onboardings trong ứng dụng Capacitor 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 `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ụ: ```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\} ### Bổ sung 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 nhập vào với hồ sơ người dùng và tránh hỏi họ lặp lại 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 đó 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 ứng dụng, có thể trông như sau: ```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\} Sử dụng quiz trong onboardings, 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 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 xây dựng onboarding và gán ID có ý nghĩa cho các tùy chọn. 2. Xử lý các câu trả lời quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](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ó nút mở paywall, hãy triển khai code paywall như một [phản hồi cho action của nút đó](capacitor-handling-onboarding-events#opening-a-paywall). --- # File: capacitor-best-practices --- --- title: "Best practices in Capacitor SDK" description: "Reference patterns for integrating Adapty SDK on Capacitor — call order, error handling, and other production-readiness rules." --- <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 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 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()` sẽ thất bại vớ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()` vào 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` resolve. Các lệnh gọi chạy đua với nó sẽ thất bại với [`#3006 profileWasChanged`](capacitor-handle-errors#custom-network-codes), hoặc rơi 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 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ẽ rơi 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 về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Con đường của bạn phụ thuộc vào hai điều: khi nào bạn biết customer user ID và liệu bạn có sử dụng SDK MMP hay analytics 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 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 lúc khởi chạy ứng dụng, hãy truyền trực tiếp vào `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 | Khi nào | 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({ apiKey: '...', params: { customerUserId: '...' } })` | 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({ apiKey: '...' })` 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({ key: '...', value: '...' })` 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 vào đúng 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 con đường 2b với xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong lúc `identify` tạo 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ề cho sai đối tượng. ::: ## Cài đặt 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, `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 trên 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), 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' })` và sau đó là `restorePurchases`. Để biết metadata cần gửi với 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 đá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ề đúng paywall theo đối tượng mục tiêu, và có 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 này giả định `adapty.activate()` và `adapty.identify()` đã được giải quyết xong. Xem [Thứ tự gọi trong Capacitor SDK](capacitor-sdk-call-order). ::: ## Quy tắc và những lỗi cần tránh \{#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ả 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 lúc xử lý. | | Gọi `getPaywall` sau khi attribution đã có cơ hội xử lý — 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 được ghi nhận. Paywall sẽ được xử lý 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ột cách thầm lặng. | | Đặ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 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ẽ tắt app. | Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-capacitor) để 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 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 nhắm mục tiêu AA ngay lần khởi chạy đầu tiên 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 khi attribution được áp dụng trong Capacitor, sử dụng AdaptyProfile.appliedAttributionSources." --- Attribution của Apple Ads (AA) đến bất đồng bộ sau `adapty.activate()`. Ở lần khởi chạy đầu tiên, attribution thường chưa có, nên `getPaywall` sẽ xử lý theo đối tượng mặc định và người dùng Apple Ads bỏ lỡ paywall nhắm mục tiêu AA của bạn. Thay vì trì hoãn việc hiển thị paywall cho đến khi attribution đến, 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ẽ nhận được biến thể nhắm mục tiêu còn những người khác thấy paywall mà không phải 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 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 `adapty.activate()`, SDK 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ẽ 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`. Đ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 xử lý yêu cầu theo đối tượng mặc định, vì vậy người dùng thấy paywall ngay lập tức. 2. Khi `'apple_search_ads'` xuất hiện, gọi `getPaywall` lần nữa. Lúc này Adapty xử lý 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 vắng mặt. Đ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, bước 1 vẫn an toàn — Adapty 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. 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 đã có sẵn `'apple_search_ads'` trong `appliedAttributionSources`, nên `getPaywall` đầu tiên đã trả về paywall nhắm mục tiêu Apple Ads — không có lần fetch thứ hai hay thay đổi hiển thị nào. Flow 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ó đến. 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** với `getPaywall` như thường — đừ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 nó xuất hiện, fetch lại paywall và hiển thị paywall đã 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.getPaywall({ placementId }); // present the targeted paywall 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 bao giờ nhận được attribution Apple Ads, vì vậy hãy xóa listener sau một thời gian thay vì giữ nó mở suốt phiên. 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ụ đầy đủ \{#complete-example\} `onAppleAdsAttribution` resolve khi attribution Apple Ads được áp dụng, hoặc reject sau `timeoutMs`. Cách dùng bên dưới tải paywall ngay lập tức, sau đó fetch 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 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 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 giây lát 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 nâng cấp trước khi paywall được hiển thị. Điều chỉnh `timeoutMs` theo thời gian bạn sẵn lòng tiếp tục lắng nghe — attribution đang đến thường xuất hiện trong vài giây kể từ 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ý](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 đoạn code này thêm listener riêng của nó mà không ảnh hưởng đến những cái khác. --- # File: capacitor-test --- --- title: "Test & release in Capacitor SDK" description: "Learn how to test and release your Capacitor app with Adapty SDK." --- If you've already implemented the Adapty SDK in your Capacitor app, you'll want to test that everything is set up correctly and that purchases work as expected across both iOS and Android platforms. This involves testing both the SDK integration and the actual purchase flow with Apple's sandbox environment and Google Play's testing environment. ## Test your app For comprehensive testing of your in-app purchases, see our platform-specific testing guides: [iOS testing guide](test-purchases-in-sandbox) and [Android testing guide](testing-on-android). ## Prepare for release Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm: - Store connection and server notifications are configured - Purchases complete and are reported to Adapty - Access unlocks and restores correctly - Privacy and review requirements are met --- # 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 làm gì? \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách cẩn thận. ID người dùng theo định dạng `<FirstName.LastName>` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, cũng như việc sử dụng email. Đối với Chế độ Trẻ em, cách tốt nhất là sử dụng các định danh được tạo 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 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, hãy tắt việc 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 từng nền tảng \{#platform-specific-configurations\} #### iOS: Bật Chế độ Trẻ em bằng CocoaPods \{#ios-enable-kids-mode-using-cocoapods\} Nếu bạn đang dùng CocoaPods cho iOS, bạn cũng có thể bật Chế độ Trẻ em ở 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 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 ``` #### Android: Bật Chế độ Trẻ em bằng Gradle \{#android-enable-kids-mode-using-gradle\} Đối với Android, bạn cũng có thể bật Chế độ Trẻ em ở cấp độ native bằng cách thêm nội dung sau vào `build.gradle` của ứng dụng: ```groovy showLineNumbers title="android/app/build.gradle" android { defaultConfig { // ... existing config ... // Enable Kids Mode buildConfigField "boolean", "ADAPTY_KIDS_MODE", "true" } } ``` ## 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: "Reference documentation for Adapty Capacitor SDK." --- This page contains reference documentation for Adapty Capacitor SDK. Choose the topic you need: - **[SDK models](https://capacitor.adapty.io/)** - Data models and structures used by the SDK - **[Handle errors](capacitor-handle-errors)** - Error handling and troubleshooting --- # 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 bọc một lỗi StoreKit, Play Billing, mạng hoặc backend bên trong. Khi bật verbose logs (`adapty.setLogLevel({ logLevel: 'verbose' })` — xem [Logging](sdk-installation-capacitor#logging)), lỗi bên trong đó sẽ được in ra console, giúp bạn xác định nguyên nhân thực sự. Thuộc tính `detail` trên `AdaptyError` luôn được điền dữ liệu bất kể mức log — verbose logs chỉ giúp hiển thị nó trên 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]) { } ``` ## 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 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 fetchPaywall(placementId: string) { try { const paywall = await adapty.getPaywall({ placementId }); return paywall; } 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; } } ``` ##  System StoreKit codes | Error | Code | Description | |-----|----|-----------| | unknown | 0 | This error indicates that an unknown or unexpected error occurred. | | clientInvalid | 1 | This error code indicates that the client is not allowed to perform the attempted action. | | paymentCancelled | 2 | <p>This error code indicates that the user canceled a payment request.</p><p>No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.</p> | | paymentInvalid | 3 | This error indicates that one of the payment parameters was not recognized by the store. | | paymentNotAllowed | 4 | <p>This error code indicates that the user is not allowed to authorize payments. Possible reasons:</p><p></p><p>- Payments are not supported in the user's country.</p><p>- The user is a minor.</p> | | storeProductNotAvailable | 5 | This error code indicates that the requested product is absent from the App Store. Make sure the product is available for the used country. | | cloudServicePermissionDenied | 6 | This error code indicates that the user has not allowed access to Cloud service information. | | cloudServiceNetworkConnectionFailed | 7 | This error code indicates that the device could not connect to the network. | | cloudServiceRevoked | 8 | This error code indicates that the user has revoked permission to use this cloud service. | | privacyAcknowledgementRequired | 9 | This error code indicates that the user has not yet acknowledged the store privacy policy. | | unauthorizedRequestData | 10 | This error code indicates that the request is built incorrectly. | | invalidOfferIdentifier | 11 | <p>The offer identifier is not valid. Possible reasons:</p><p></p><p>- You have not set up an offer with that identifier in the App Store.</p><p>- You have revoked the offer.</p><p>- You misprinted the offer ID.</p> | | invalidSignature | 12 | This error code indicates that the signature in a payment discount is not valid. Make sure you've filled out the **In-app purchase Key ID** field and uploaded the **In-App Purchase Private Key** file. Refer to the [Configure App Store integration](app-store-connection-configuration) topic for details. | | missingOfferParams | 13 | <p>This error indicates issues with Adapty integration or with offers.</p><p>Refer to the [Configure App Store integration](app-store-connection-configuration) and to [Offers](offers) for details on how to set them up.</p> | | invalidOfferPrice | 14 | This error code indicates that the price you specified in the store is no longer valid. Offers must always represent a discounted price. | ## Custom Android codes | Error | Code | Description | |-----|----|-----------| | adaptyNotInitialized | 20 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native]( sdk-installation-reactnative). | | productNotFound | 22 | This error indicates that the product requested for purchase is not available in the store. | | invalidJson | 23 | The paywall JSON is not valid. Fix it in the Adapty Dashboard. Refer to the [Customize paywall with remote config](customize-paywall-with-remote-config) topic for details on how to fix it. | | currentSubscriptionToUpdateNotFoundInHistory | 24 | The original subscription that needs to be renewed is not found. | | pendingPurchase | 25 | This error indicates that the purchase state is pending rather than purchased. Refer to the [Handling pending transactions](https://developer.android.com/google/play/billing/integrate#pending) page in the Android Developer docs for details. | | billingServiceTimeout | 97 | This error indicates that the request has reached the maximum timeout before Google Play can respond. This could be caused, for example, by a delay in the execution of the action requested by the Play Billing Library call. | | featureNotSupported | 98 | The requested feature is not supported by the Play Store on the current device. | | billingServiceDisconnected | 99 | This fatal error indicates that the client app’s connection to the Google Play Store service via the `BillingClient` has been severed. | | billingServiceUnavailable | 102 | This transient error indicates the Google Play Billing service is currently unavailable. In most cases, this means there is a network connection issue anywhere between the client device and Google Play Billing services. | | billingUnavailable | 103 | <p>This error indicates that a user billing error occurred during the purchase process. Examples of when this can occur include:</p><p></p><p>1\. The Play Store app on the user's device is out of date.</p><p>2. The user is in an unsupported country.</p><p>3. The user is an enterprise user, and their enterprise admin has disabled users from making purchases.</p><p>4. Google Play is unable to charge the user’s payment method. For example, the user's credit card might have expired.</p><p>5. The user is not logged into the Play Store app.</p> | | developerError | 105 | This is a fatal error that indicates you're improperly using an API. | | billingError | 106 | This is a fatal error that indicates an internal problem with Google Play itself. | | itemAlreadyOwned | 107 | The consumable product has already been purchased. | | itemNotOwned | 108 | This error indicates that the requested action on the item failed sin | ## Custom StoreKit codes | Error | Code | Description | |-----|----|-----------| | noProductIDsFound | 1000 | <p>This error indicates that none of the products in the paywall is available in the store.</p><p>If you are encountering this error, please follow the steps below to resolve it:</p><p></p><p>1. Check if all the products have been added to Adapty Dashboard.</p><p>2. Ensure that the Bundle ID of your app matches the one from the Apple Connect.</p><p>3. Verify that the product identifiers from the app stores match with the ones you have added to the Dashboard. Please note that the identifiers should not contain Bundle ID, unless it is already included in the store.</p><p>4. Confirm that the app paid status is active in your Apple tax settings. Ensure that your tax information is up-to-date and your certificates are valid.</p><p>5. Check if a bank account is attached to the app, so it can be eligible for monetization.</p><p>6. Check if the products are available in all regions.Also, ensure that your products are in **“Ready to Submit”** state.</p> | | productRequestFailed | 1002 | <p>Unable to fetch available products at the moment. Possible reason:</p><p></p><p>- No cache was yet created and no internet connection at the same time.</p> | | cantMakePayments | 1003 | In-App purchases are not allowed on this device. | | noPurchasesToRestore | 1004 | This error indicates that Google Play did not find the purchase to restore. | | cantReadReceipt | 1005 | <p>There is no valid receipt available on the device. This can be an issue during sandbox testing.</p><p>No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.</p> | | productPurchaseFailed | 1006 | Product purchase failed. This wraps an underlying StoreKit error — read the wrapped error (or enable verbose logs to see it in the console) for the actual reason. The wrapped error is typically one of the StoreKit codes 0–14 in the table above — most commonly `paymentCancelled`, `paymentInvalid`, `paymentNotAllowed`, or `invalidOfferPrice`. If you can't identify a specific reason, try a new [sandbox profile](test-purchases-in-sandbox); if it still fails, contact Apple support. | | refreshReceiptFailed | 1010 | This error indicates that the receipt was not received. Applicable to StoreKit 1 only. | | receiveRestoredTransactionsFailed | 1011 | Purchase restoration failed. | ## Custom network codes | Error | Code | Description | | :------------------- | :--- | :----------------------------------------------------------- | | notActivated | 2002 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native](sdk-installation-reactnative). | | badRequest | 2003 | Bad request. | | serverError | 2004 | Server error. | | networkFailed | 2005 | The network request failed. | | decodingFailed | 2006 | This error indicates that response decoding failed. | | encodingFailed | 2009 | This error indicates that request encoding failed. | | analyticsDisabled | 3000 | We can't handle analytics events, since you've opted it out. Refer to the [Analytics integration](analytics-integration) topic for details. | | wrongParam | 3001 | This error indicates that some of your parameters are not correct: blank when it cannot be blank or wrong type, etc. | | activateOnceError | 3005 | It is not possible to call `.activate` method more than once. | | profileWasChanged | 3006 | The user profile was changed during the operation. | | fetchTimeoutError | 3101 | This error means that the paywall could not be fetched within the set limit. To avoid this situation, [set up local fallbacks](fetch-paywalls-and-products). | | operationInterrupted | 9000 | This operation was interrupted by the system. | --- # File: capacitor-sdk-migration-guides --- --- title: "Capacitor SDK Migration Guides" description: "Migration guides for Adapty Capacitor SDK versions." --- This page contains all migration guides for Adapty Capacitor SDK. Choose the version you want to migrate to for detailed instructions: - [**Migrate to v3.16**](migration-to-capacitor-316) --- # 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-06-24T14:36:38.682Z_ _Successfully processed: 43/43 files_ # FLUTTER - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-06-24T14:36:38.685Z Total files: 41 --- # File: sdk-installation-flutter --- --- title: "Cài đặt & cấu hình Flutter SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên Flutter cho các ứng dụng dựa trên gói đăng ký." --- Adapty SDK gồm hai module chính để tích hợp liền mạch vào ứng dụng Flutter của bạn: - **Core Adapty**: SDK cốt lõi, bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder) — công cụ no-code thân thiện để tạo paywall đa nền tảng một cách dễ dàng. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example) của chúng tôi, minh họa toàn bộ quá trình cài đặt, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Yêu cầu \{#requirements\} Adapty SDK hỗ trợ iOS 13.0+, nhưng cần iOS 15.0+ để hoạt động đúng với các paywall được tạo trong Paywall Builder. :::info Adapty tương thích với Google Play Billing Library đến phiên bản 8.x. Mặc định, Adapty hoạt động với Google Play Billing Library v7.0.0, nhưng nếu bạn muốn sử dụng phiên bản mới hơn, bạn có thể thêm thủ công [dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Flutter.svg?style=flat&logo=flutter)](https://github.com/adaptyteam/AdaptySDK-Flutter/releases) 1. Thêm Adapty vào file `pubspec.yaml` của bạn: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: ^<the latest SDK version> ``` 2. Chạy lệnh sau để cài đặt các dependency: ```bash showLineNumbers title="Terminal" flutter pub get ``` 3. Import Adapty SDK vào ứng dụng của bạn: ```dart showLineNumbers title="main.dart" import 'package:adapty_flutter/adapty_flutter.dart'; ``` ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Kích hoạt Adapty SDK trong code ứng dụng của bạn. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng. ::: Để lấy **Public SDK Key**: 1. 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 `"YOUR_PUBLIC_SDK_KEY"` trong code. :::important - Đảm bảo bạn dùng **Public SDK key** để khởi tạo Adapty, còn **Secret key** chỉ 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 chắc chắn 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 dùng [Adapty Paywall Builder](adapty-paywall-builder), trước tiên hãy [kích hoạt module AdaptyUI](#activate-adaptyui-module-of-adapty-sdk) bên dưới, sau đó làm theo [hướng dẫn nhanh Paywall Builder](flutter-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, hãy xem [hướng dẫn nhanh cho paywall tùy chỉnh](flutter-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn dự định sử dụng [Paywall Builder](adapty-paywall-builder) và đã [cài đặt module AdaptyUI](sdk-installation-flutter#install-adapty-sdk), bạn cũng cần kích hoạt AdaptyUI: :::note Các dependency liên quan đến AdaptyUI được liên kết vào ứng dụng bất kể AdaptyUI có được kích hoạt hay không. ::: :::important Trong code của bạn, phải kích hoạt module Adapty cốt lõi trước khi kích hoạt AdaptyUI. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withActivateUI(true), // This automatically activates AdaptyUI ); ``` ## Thiết lập tùy chọn \{#optional-setup\} ### Ghi log \{#logging\} #### Thiết lập hệ thống ghi log \{#set-up-the-logging-system\} Adapty ghi lại các lỗi và thông tin quan trọng để giúp bạn hiểu những gì đang xảy ra. Các mức log có sẵn như sau: | Mức | Mô tả | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `AdaptyLogLevel.none` | Không ghi log gì cả. Giá trị mặc định | | `AdaptyLogLevel.error` | Chỉ ghi log các lỗi | | `AdaptyLogLevel.warn` | Ghi log các lỗi và thông báo từ SDK không gây ra lỗi nghiêm trọng nhưng đáng chú ý. | | `AdaptyLogLevel.info` | Ghi log các lỗi, cảnh báo và các thông báo thông tin khác nhau. | | `AdaptyLogLevel.verbose` | Ghi log mọi thông tin bổ sung có thể hữu ích trong quá trình debug, như lời gọi hàm, truy vấn API, v.v. | Bạn có thể đặt mức log trong ứng dụng trước khi cấu hình Adapty: ```dart showLineNumbers title="main.dart" // Set log level before activation. // 'verbose' is recommended for development and the first production release await Adapty().setLogLevel(AdaptyLogLevel.verbose); // Or set it during configuration await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withLogLevel(AdaptyLogLevel.verbose), ); ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn gửi dữ liệu đó một cách tường minh, nhưng bạn có thể áp dụng thêm các chính sách bảo mật dữ liệu để tuân thủ quy định của cửa hàng hoặc quốc gia. #### Tắt thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt việc thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không được yêu cầu cho ứng dụng của bạn. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withIpAddressCollectionDisabled(true), ); ``` #### Tắt thu thập và chia sẻ ID quảng cáo \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `appleIdfaCollectionDisabled` (iOS) hoặc `googleAdvertisingIdCollectionDisabled` (Android) thành `true` để tắt việc thu thập các identifier quảng cáo. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc khi ứng dụng của bạn không cần attribution quảng cáo hay analytics dựa trên ID quảng cáo. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleIdfaCollectionDisabled(true) // iOS ..withGoogleAdvertisingIdCollectionDisabled(true), // Android ); ``` #### Thiết lập cấu hình cache media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Module được kích hoạt tự động cùng với Adapty SDK. Nếu bạn không sử dụng Paywall Builder và muốn tắt module AdaptyUI, hãy truyền `withActivateUI(false)` trong quá trình kích hoạt. Mặc định, AdaptyUI lưu cache media (như hình ảnh và video) để cải thiện hiệu suất và giảm sử dụng mạng. Bạn có thể tùy chỉnh cài đặt cache bằng cách cung cấp cấu hình riêng. Sử dụng `withMediaCacheConfiguration` để ghi đè kích thước cache và thời gian hiệu lực mặc định. Đây là tùy chọn — nếu bạn không gọi phương thức này, các giá trị mặc định sẽ được dùng (100MB dung lượng đĩa, không giới hạn số lượng trong bộ nhớ). Tuy nhiên, nếu bạn sử dụng cấu hình, tất cả các tham số phải được bao gồm. ```dart showLineNumbers title="main.dart" final mediaCacheConfig = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 200 * 1024 * 1024, // 200 MB memoryStorageCountLimit: 2147483647, // max int value diskStorageSizeLimit: 200 * 1024 * 1024, // 200 MB ); await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withMediaCacheConfiguration(mediaCacheConfig), ); ``` **Tham số:** | Tham số | Bắt buộc | Mô tả | |-------------------------|----------|-----------------------------------------------------------------------------| | memoryStorageTotalCostLimit | tùy chọn | Tổng kích thước cache trong bộ nhớ tính bằng byte. Mặc định là 100 MB. | | memoryStorageCountLimit | tùy chọn | Giới hạn số lượng item trong bộ nhớ. Mặc định là giá trị int tối đa. | | diskStorageSizeLimit | tùy chọn | Giới hạn kích thước file trên đĩa tính bằng byte. Mặc định là 100 MB. | ### Bật mức độ truy cập cục bộ (Android) \{#enable-local-access-levels-android\} Mặc định, [mức độ truy cập cục bộ](local-access-levels) được bật trên iOS và tắt trên Android. Để bật trên Android, đặt `withGoogleLocalAccessLevelAllowed` thành `true`: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleLocalAccessLevelAllowed(true), ); ``` ### Xóa dữ liệu khi khôi phục backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ backup iCloud và xóa toàn bộ dữ liệu SDK được lưu cục bộ, bao gồm thông tin hồ sơ người dùng được cache, chi tiết sản phẩm và paywall. SDK sau đó sẽ khởi tạo lại với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ có cache SDK cục bộ bị xóa. Lịch sử giao dịch với Apple và dữ liệu người dùng trên máy chủ Adapty vẫn không thay đổi. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withClearDataOnBackup(true) // default – false ); ``` ## Xử lý sự cố \{#troubleshooting\} #### Quy tắc backup Android (cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `<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"/> #### Mua hàng thất bại sau khi quay lại từ ứng dụng khác trên Android \{#purchases-fail-after-returning-from-another-app-in-android\} Nếu Activity khởi động flow mua hàng sử dụng `launchMode` không phải mặc định, Android có thể tạo lại hoặc tái sử dụng Activity đó không đúng cách khi người dùng quay lại từ Google Play, ứng dụng ngân hàng hoặc trình duyệt. Điều này có thể khiến kết quả mua hàng bị mất hoặc bị coi là đã hủy. Để đảm bảo mua hàng hoạt động đúng, chỉ sử dụng launch mode `standard` hoặc `singleTop` cho Activity khởi động flow mua hàng, và tránh các mode khác. Trong `AndroidManifest.xml`, hãy đảm bảo Activity khởi động flow mua hàng được đặt thành `standard` hoặc `singleTop`: ```xml <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ể thấy lỗi biên dịch Swift 6 trên các pod target của Adapty. Các triệu chứng thường gặp bao gồm lỗi `@Sendable` trong `AdaptyUIBuilderLogic`, thiếu conformance `Sendable` trên các kiểu Adapty, hoặc lỗi actor isolation. Các pod Adapty khai báo `s.swift_version = '6.0'` và yêu cầu Swift 6 để build. Code ứng dụng của bạn có thể tiếp tục dùng Swift 5 — chỉ các pod target của Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`, `AdaptyLogger`, `AdaptyPlugin`) cần được build với Swift 6. Nguyên nhân phổ biến nhất là hook `post_install` trong `ios/Podfile` ghi đè `SWIFT_VERSION` cho mọi pod target: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` **Cách khắc phục**: Loại trừ các pod target của Adapty khỏi việc ghi đè: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| next if %w[Adapty AdaptyUI AdaptyUIBuilder AdaptyLogger AdaptyPlugin].include?(target.name) target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` Sau đó chạy `pod install` từ thư mục `ios/` và build lại. Để kiểm tra, mở `ios/Pods/Pods.xcodeproj`, chọn pod target `Adapty` → **Build Settings** → **Swift Language Version**. Giá trị phải là **Swift 6**. --- # File: flutter-quickstart-paywalls --- --- title: "Kích hoạt mua hàng bằng cách sử dụng paywall trong Flutter SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký trong ứng dụng." --- Để kích hoạt in-app purchase, bạn cần nắm vững ba khái niệm cốt lõi: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywall**](paywalls) là các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi ưu đãi, giá cả và tổ hợp sản phẩm mà không cần chỉnh sửa code của ứng dụng. - [**Placement**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trên dashboard, sau đó yêu cầu chúng bằng placement ID trong code. Điều này giúp bạn dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho các nhóm người dùng khác nhau. Adapty cung cấp ba cách để kích hoạt mua hàng trong ứng dụng của bạn. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Cách triển khai | Độ phức tạp | Khi nào dùng | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Dễ | Bạn [tạo một paywall hoàn chỉnh, sẵn sàng để mua hàng trong no-code builder](quickstart-paywalls). Adapty tự động render và xử lý toàn bộ quy trình mua hàng phức tạp, xác thực biên lai và quản lý gói đăng ký ở phía sau. | | Paywall tạo thủ công | 🟡 Trung bình | Bạn tự triển khai giao diện paywall trong code ứng dụng, nhưng vẫn lấy đối tượng paywall từ Adapty để duy trì sự linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](flutter-quickstart-manual). | | Chế độ Observer | 🔴 Khó | Bạn đã có cơ sở hạ tầng xử lý mua hàng riêng và muốn tiếp tục sử dụng nó. Lưu ý rằng chế độ observer có một số hạn chế trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai paywall được tạo trong Adapty paywall builder.** Nếu bạn không muốn sử dụng paywall builder, hãy xem [hướng dẫn xử lý mua hàng trong paywall tạo thủ công](flutter-making-purchases). ::: Để hiển thị paywall được tạo trong Adapty paywall builder, trong code ứng dụng, bạn chỉ cần: 1. **Lấy paywall**: Lấy paywall từ Adapty. 2. **Hiển thị paywall và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị container paywall mà bạn đã lấy trong ứng dụng. 3. **Xử lý hành động nút**: Liên kết tương tác của người dùng với paywall với phản hồi của ứng dụng. Ví dụ: mở liên kết hoặc đóng paywall khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. Kết nối ứng dụng của bạn với [App Store](initial_ios) và/hoặc [Google Play](initial-android) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo paywall và thêm sản phẩm vào đó](create-paywall). 4. [Tạo placement và thêm paywall vào đó](create-placement). 5. [Cài đặt và kích hoạt SDK](sdk-installation-flutter) trong code ứng dụng của bạn. :::tip Cách nhanh nhất để hoàn thành các bước này là làm theo [hướng dẫn nhanh](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy paywall \{#1-get-the-paywall\} Các paywall của bạn được liên kết với các placement được cấu hình trên dashboard. Placement cho phép bạn chạy các paywall khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy paywall được tạo trong Adapty paywall builder, bạn cần: 1. Lấy đối tượng `paywall` theo [placement](placements) ID bằng phương thức `getPaywall` và kiểm tra xem đó có phải là paywall được tạo trong builder hay không thông qua thuộc tính `hasViewConfiguration`. 2. Tạo paywall view bằng phương thức `createPaywallView`. View chứa các phần tử giao diện và kiểu dáng cần thiết để hiển thị paywall. :::important Để lấy cấu hình view, bạn phải bật toggle **Show on device** trong Paywall Builder. Nếu không, bạn sẽ nhận được cấu hình view rỗng và paywall sẽ không được hiển thị. ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## 2. Hiển thị paywall \{#2-display-the-paywall\} Khi bạn đã có cấu hình paywall, chỉ cần thêm vài dòng code là đủ để hiển thị paywall của mình. Để hiển thị paywall, sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức `createPaywallView`. Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo một instance `view` mới. ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip Để biết thêm chi tiết về cách hiển thị paywall, hãy xem [hướng dẫn](flutter-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút \{#3-handle-button-actions\} Khi người dùng nhấn các nút trong paywall, Flutter SDK tự động xử lý việc mua hàng và khôi phục. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được định nghĩa sẵn và yêu cầu xử lý hành động trong code của bạn. Để kiểm soát hoặc theo dõi các tiến trình trên màn hình paywall, hãy triển khai các phương thức `AdaptyUIPaywallsEventsObserver` và đặt observer trước khi hiển thị bất kỳ màn hình nào. Nếu người dùng đã thực hiện một hành động nào đó, `paywallViewDidPerformAction` sẽ được gọi và ứng dụng của bạn cần phản hồi tùy theo action ID. Ví dụ: paywall của bạn có thể có nút đóng và các URL cần mở (ví dụ: điều khoản sử dụng và chính sách bảo mật). Vì vậy, bạn cần phản hồi các hành động với ID `Close` và `OpenUrl`. :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [hành động](flutter-handle-paywall-actions) và [sự kiện](flutter-handling-events) nút. ::: ```dart showLineNumbers title="Flutter" class _PaywallScreenState extends State<PaywallScreen> implements AdaptyUIPaywallsEventsObserver { @override void initState() { super.initState(); // Register this class as the paywalls event observer AdaptyUI().setPaywallsEventsObserver(this); } // This method is called when user performs an action on the paywall UI @override void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL using url_launcher package _launchUrl(url); break; } } // Helper method to launch URLs Future<void> _launchUrl(String url) async { try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { // Handle case where URL cannot be launched print('Could not launch $url'); } } catch (e) { // Handle any errors print('Error launching URL: $e'); } } } ``` ## Bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra mua hàng trong [sandbox App Store](test-purchases-in-sandbox) hoặc [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một lần mua thử từ paywall. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](flutter-check-subscription-status) để đảm bảo bạn hiển thị paywall hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Đây là cách tất cả các bước đó có thể được tích hợp cùng nhau trong ứng dụng của bạn. ```dart void main() async { runApp(MaterialApp(home: PaywallScreen())); } class PaywallScreen extends StatefulWidget { @override State<PaywallScreen> createState() => _PaywallScreenState(); } class _PaywallScreenState extends State<PaywallScreen> implements AdaptyUIPaywallsEventsObserver { @override void initState() { super.initState(); // Register this class as the paywalls event observer AdaptyUI().setPaywallsEventsObserver(this); _showPaywallIfNeeded(); } Future<void> _showPaywallIfNeeded() async { try { final paywall = await Adapty().getPaywall( placementId: 'YOUR_PLACEMENT_ID', ); if (!paywall.hasViewConfiguration) return; final view = await AdaptyUI().createPaywallView(paywall: paywall); await view.present(); } catch (_) { // Handle any errors (network, SDK issues, etc.) } } // This method is called when user performs an action on the paywall UI @override void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL using url_launcher package _launchUrl(url); break; } } // Helper method to launch URLs Future<void> _launchUrl(String url) async { try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { // Handle case where URL cannot be launched print('Could not launch $url'); } } catch (e) { // Handle any errors print('Error launching URL: $e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Adapty Paywall Example')), body: Center( // Add a button to re-trigger the paywall for testing purposes child: ElevatedButton( onPressed: _showPaywallIfNeeded, child: Text('Show Paywall'), ), ), ); } } ``` --- # File: flutter-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong Flutter SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Flutter của bạn với Adapty." --- Để quyết định xem người dùng có thể truy cập nội dung trả phí hay cần xem paywall, bạn cần kiểm tra [mức độ truy cập](access-level) của họ trong hồ sơ người dùng. Bài viết này hướng dẫn bạn cách truy cập trạng thái hồ sơ người dùng để quyết định nên hiển thị gì cho họ — paywall hay nội dung trả phí. ## Lấy trạng thái gói đăng ký \{#get-subscription-status\} Khi quyết định hiển thị paywall hay nội dung trả phí cho người dùng, bạn kiểm tra [mức độ truy cập](access-level) trong hồ sơ của họ. Có hai lựa chọn: - Gọi `getProfile` khi cần dữ liệu hồ sơ mới nhất ngay lập tức (ví dụ: khi khởi động ứng dụng) hoặc muốn buộc cập nhật. - Thiết lập **cập nhật hồ sơ tự động** để giữ một bản sao cục bộ được tự động làm mới mỗi khi trạng thái gói đăng ký thay đổi. ### Lấy hồ sơ người dùng \{#get-profile\} Cách đơn giản nhất để lấy trạng thái gói đăng ký là dùng phương thức `getProfile`: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Lắng nghe cập nhật gói đăng ký \{#listen-to-subscription-updates\} Để tự động nhận các cập nhật hồ sơ trong ứng dụng: 1. Dùng `Adapty().didUpdateProfileStream.listen()` để lắng nghe các thay đổi hồ sơ — Adapty sẽ tự động gọi phương thức này mỗi khi trạng thái gói đăng ký của người dùng thay đổi. 2. Lưu dữ liệu hồ sơ được cập nhật khi phương thức này được gọi, để bạn có thể sử dụng xuyên suốt ứng dụng mà không cần thực hiện thêm request mạng. ```dart class SubscriptionManager { AdaptyProfile? _currentProfile; SubscriptionManager() { // Listen for profile updates Adapty().didUpdateProfileStream.listen((profile) { _currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() bool hasAccess() { return _currentProfile?.accessLevels['premium']?.isActive ?? false; } } ``` :::note Adapty tự động gọi stream listener cập nhật hồ sơ khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký đã lưu cache ngay cả khi thiết bị không có kết nối mạng. ::: ## Kết nối hồ sơ với logic paywall \{#connect-profile-with-paywall-logic\} Khi cần đưa ra quyết định ngay lập tức về việc hiển thị paywall hay cấp quyền truy cập nội dung trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách này hữu ích trong các tình huống như khi khởi động ứng dụng, khi vào các mục premium, hoặc trước khi hiển thị nội dung cụ thể. ```dart Future<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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-flutter --- --- title: "Tích hợp Adapty vào ứng dụng Flutter của bạn với sự hỗ trợ của AI" description: "Hướng dẫn từng bước tích hợp Adapty vào ứng dụng Flutter của bạn bằng Cursor, Context7, ChatGPT, Claude hoặc các công cụ AI khác." --- Hướng dẫn này sẽ giúp bạn tích hợp Adapty vào ứng dụng Flutter từng bước với công cụ lập trình AI — bạn cung cấp cho nó đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ code SDK nào. Bạn có thể thực hiện điều này bằng một LLM skill tương tác, hoặc thủ công thông qua Dashboard. ### Cách dùng Skill (khuyến nghị) \{#skill-approach-recommended\} Adapty CLI skill cho phép LLM của bạn thiết lập app, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối các cửa hàng](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm skill, chạy `/adapty-cli` trong agent của bạn. Nó sẽ hướng dẫn bạn qua từng bước — bao gồm cả lúc cần mở Dashboard để kết nối các cửa hàng. ### Cách thiết lập thủ công trên Dashboard \{#dashboard-approach\} Nếu bạn muốn tự cấu hình mọi thứ, đây là những gì cần có trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard — bạn sẽ cần tự cung cấp chúng. 1. **Kết nối các cửa hàng ứng dụng**: Trong Adapty Dashboard, vào **App settings → General**. Kết nối cả App Store và Google Play nếu ứng dụng Flutter của bạn hỗ trợ cả hai nền tảng. Đây là điều bắt buộc để các giao dịch mua hoạt động. [Kết nối cửa hàng ứng dụng](integrate-payments) 2. **Sao chép Public SDK key**: Trong Adapty Dashboard, vào **App settings → General**, sau đó tìm phần **API keys**. Trong code, đây là chuỗi bạn truyền vào cấu hình Adapty. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu sản phẩm trực tiếp trong code — Adapty cung cấp chúng thông qua paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo một paywall và một placement**: Trong Adapty Dashboard, tạo paywall trên trang **Paywalls**, sau đó gán nó vào một placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty().getPaywall()`. [Tạo paywall](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình cho từng sản phẩm trên trang **Products**. Trong code, chuỗi được kiểm tra trong `profile.accessLevels['premium']?.isActive`. Mức độ truy cập `premium` mặc định phù hợp với hầu hết các ứng dụng. Nếu người dùng trả phí có quyền truy cập vào các tính năng khác nhau tùy theo sản phẩm (ví dụ: gói `basic` so với gói `pro`), hãy [tạo thêm mức độ truy cập](assigning-access-level-to-a-product) trước khi bắt đầu viết code. :::tip Khi đã có đủ năm điều trên, bạn đã sẵn sàng để viết code. Hãy nói với LLM của bạn: "Public SDK key của tôi là X, placement ID của tôi là Y" để nó có thể tạo code khởi tạo và lấy paywall chính xác. ::: ### Thiết lập khi sẵn sàng \{#set-up-when-ready\} Những mục này không bắt buộc để bắt đầu viết code, nhưng bạn sẽ cần chúng khi tích hợp trưởng thành hơn: - **A/B test**: Cấu hình trên trang **Placements**. Không cần thay đổi code. [A/B test](ab-tests) - **Thêm paywall và placement**: Thêm các lời gọi `getPaywall` với các placement ID khác nhau. - **Tích hợp analytics**: Cấu hình trên trang **Integrations**. Cách thiết lập khác nhau tùy theo tích hợp. Xem [tích hợp analytics](analytics-integration) và [tích hợp attribution](attribution-integration). ## Cung cấp tài liệu Adapty cho LLM của bạn \{#feed-adapty-docs-to-your-llm\} ### Dùng Context7 (khuyến nghị) \{#use-context7-recommended\} [Context7](https://context7.com) là một MCP server cung cấp cho LLM của bạn quyền truy cập trực tiếp vào tài liệu Adapty luôn cập nhật. LLM của bạn tự động lấy đúng tài liệu dựa trên những gì bạn hỏi — không cần dán URL thủ công. Context7 hoạt động với **Cursor**, **Claude Code**, **Windsurf** và các công cụ tương thích MCP khác. Để thiết lập, chạy: ``` npx ctx7 setup ``` Lệnh này phát hiện trình soạn thảo của bạn và cấu hình Context7 server. Để thiết lập thủ công, xem [kho GitHub Context7](https://github.com/upstash/context7). Sau khi cấu hình, tham chiếu thư viện Adapty trong các prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the Flutter SDK ``` :::warning Dù Context7 không còn cần dán link tài liệu thủ công, thứ tự triển khai vẫn quan trọng. Hãy làm theo [hướng dẫn triển khai](#implementation-walkthrough) bên dưới từng bước để đảm bảo mọi thứ hoạt động đúng. ::: ### Dùng tài liệu dạng plain text \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng Markdown thuần túy. Thêm `.md` vào cuối URL, hoặc nhấn **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor-flutter.md](https://adapty.io/docs/vi/adapty-cursor-flutter.md). Mỗi bước trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới đều có khối "Gửi cho LLM của bạn" với các link `.md` để dán vào. Để xem thêm tài liệu cùng lúc, xem [các file index và tập hợp theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này sẽ đi qua việc tích hợp Adapty theo thứ tự triển khai. Mỗi bước bao gồm tài liệu cần gửi cho LLM, những gì bạn cần thấy khi hoàn thành và các vấn đề thường gặp. ### Lên kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết code, hãy yêu cầu LLM phân tích dự án của bạn và tạo kế hoạch triển khai. Nếu công cụ AI của bạn hỗ trợ chế độ lập kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy sử dụng nó để LLM có thể đọc cả cấu trúc dự án lẫn tài liệu Adapty trước khi viết bất kỳ code nào. Hãy cho LLM biết cách bạn xử lý giao dịch mua — điều này ảnh hưởng đến các hướng dẫn mà nó cần theo dõi: - [**Adapty Paywall Builder**](adapty-paywall-builder): Bạn tạo paywall trong trình xây dựng không cần code của Adapty, và SDK tự động hiển thị chúng. - [**Paywall tự tạo**](flutter-making-purchases): Bạn tự xây dựng giao diện paywall trong code nhưng vẫn dùng Adapty để lấy sản phẩm và xử lý giao dịch mua. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên hạ tầng mua hàng hiện có và chỉ dùng Adapty cho analytics và tích hợp. Chưa biết chọn cái nào? Đọc [bảng so sánh trong quickstart](flutter-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm dependency Adapty SDK bằng `flutter pub add` và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — không có gì khác hoạt động được nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-flutter) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-flutter.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Ứng dụng build và chạy được trên cả iOS và Android. Console debug hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra xem bạn đã thay thế placeholder bằng key thực của mình từ App settings chưa. ::: ### Hiển thị paywall và xử lý giao dịch mua \{#show-paywalls-and-handle-purchases\} Lấy paywall theo placement ID, hiển thị nó và xử lý các sự kiện mua hàng. Các hướng dẫn bạn cần phụ thuộc vào cách bạn xử lý giao dịch mua. Kiểm thử từng giao dịch mua trong sandbox khi bạn làm — đừng đợi đến cuối. Xem [Kiểm thử giao dịch mua trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. <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 paywall và cấu hình của chúng bằng Paywall Builder trong Flutter SDK" description: "Tìm hiểu cách lấy paywall PB trong Adapty để kiểm soát gói đăng ký tốt hơn trong Flutter." --- Sau khi [bạn đã thiết kế phần giao diện cho paywall](adapty-paywall-builder) bằng Paywall Builder mới trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động. Bước đầu tiên là lấy paywall gắn với placement và cấu hình view của nó như mô tả bên dưới. :::warning Paywall Builder mới yêu cầu Flutter SDK phiên bản 3.3.0 trở lên. ::: Lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang tự triển khai paywall theo cách thủ công, hãy tham khảo chủ đề [Lấy paywall và sản phẩm cho paywall remote config trong ứng dụng di động](fetch-paywalls-and-products-flutter). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu hiển thị paywall trong ứng dụng di động (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-flutter) 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 code ứng dụng để hiển thị cho người dùng. Paywall như vậy chứa cả nội dung hiển thị lẫn cách thức hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view của nó, rồi hiển thị trong ứng dụng di động. Để đảm bảo hiệu suất tối ưu, hãy lấy paywall và [cấu hình view](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy paywall, dùng phương thức `getPaywall`: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <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 là mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân cách bằng dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ thứ hai là vùng.</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](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ị 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ó kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối có kém đến đâu. Cache được cập nhật thường xuyên nên an toàn khi dùng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.</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áy chủ dự phòng độc lập trong trường hợp CDN không tiếp cận được. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</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 `loadTimeout` đã chỉ định, vì thao tác có thể bao gồm nhiều yêu cầu 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` đế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`.</p> | Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) với danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình view của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bật nút **Show on device** trong paywall builder. Nếu tùy chọn này chưa được bật, cấu hình view sẽ không thể lấy được. ::: Sau khi lấy paywall, hãy kiểm tra xem nó có chứa `ViewConfiguration` hay không — điều này cho biết paywall được tạo bằng Paywall Builder. Thông tin này sẽ hướng dẫn bạn cách hiển thị paywall. Nếu có `ViewConfiguration`, xử lý nó như paywall Paywall Builder; nếu không, [xử lý nó như paywall remote config](present-remote-config-paywalls-flutter). ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Sau khi có view, [hiển thị paywall](flutter-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được lấy gần như ngay lập tức nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng có kết nối internet yếu, việc lấy paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào. Để giải quyết điều này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như đã mô tả trong phần [Lấy thông tin paywall](flutter-get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng dùng phiên bản hiện tại (cũ) có thể gặp sự cố với paywall không render được. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng targeting cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để được hưởng lợi từ việc lấy paywall nhanh hơn, dùng phương thức `getPaywallForDefaultAudience` như sau. Ngược lại, hãy dùng `getPaywall` như mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder). ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note Phương thức `getPaywallForDefaultAudience` có sẵn từ Flutter SDK phiên bản 3.2.0 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <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ẻ phụ được phân cách bằng dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ thứ hai là vùng.</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](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ó kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối có kém đến đâu. Cache được cập nhật thường xuyên nên an toàn khi dùng trong phiên để tránh các yêu cầu mạng.</p><p></p><p>Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall, hãy triển khai custom assets. Hình ảnh hero và video có ID được xác định trước: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị hình ảnh preview cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh preview trước khi chạy video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty Flutter SDK lên phiên bản 3.8.0 trở lên. ::: Đây là ví dụ về cách cung cấp custom assets thông qua một dictionary đơn giản: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customAssets: <CUSTOM_ASSETS>, preloadProducts: preloadProducts, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note Nếu không tìm thấy asset, paywall sẽ hiển thị theo giao diện mặc định. ::: ## Thiết lập timer do nhà phát triển định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng custom timer trong ứng dụng di động, hãy tạo một đối tượng tuân theo giao thức `AdaptyTimerResolver`. Đối tượng này định nghĩa cách mỗi custom timer sẽ được render. Nếu muốn, bạn có thể dùng trực tiếp dictionary `[String: Date]` vì nó đã tuân thủ giao thức này. Đây là ví dụ: ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2025, 1, 1), // New Year 2025 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là **Timer ID** của các timer do nhà phát triển định nghĩa mà bạn đã thiết lập trong Adapty Dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động mỗi timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như Năm mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng thời gian 6 giờ bắt đầu khi người dùng mở paywall. --- # File: flutter-present-paywalls --- --- title: "Flutter - Present new Paywall Builder paywalls" description: "Hiển thị paywall trong ứng dụng Flutter bằng các tính năng kiếm tiền của Adapty." --- Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần phải lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung cần hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** — yêu cầu SDK v3.2.0 trở lên. Cách hiển thị paywall khác nhau tùy theo phiên bản Paywall Builder được dùng để thiết kế và loại paywall Remote Config. - Để hiển thị **paywall Remote Config**, xem [Render paywall được thiết kế bằng remote config](present-remote-config-paywalls). ::: Adapty Flutter SDK cung cấp hai cách để hiển thị paywall: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị paywall dưới dạng màn hình độc lập, dùng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createPaywallView`](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể dùng một lần. Nếu cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo `view` mới. :::warning Tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Đóng paywall \{#dismiss-the-paywall\} Khi cần đóng paywall theo chương trình, dùng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Hiển thị hộp thoại \{#show-dialog\} Dùng phương thức này thay cho hộp thoại alert thông thường khi một paywall view đang được hiển thị trên Android. Trên Android, các alert thông thường xuất hiện phía sau paywall view, khiến người dùng không thể nhìn thấy chúng. Phương thức này đảm bảo hộp thoại hiển thị đúng vị trí phía trên paywall trên tất cả các nền tảng. ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng paywall vào cây widget hiện có, dùng widget `AdaptyUIPaywallPlatformView` trực tiếp trong cây widget Flutter của bạn. ```dart showLineNumbers title="Flutter" AdaptyUIPaywallPlatformView( paywall: paywall, // The paywall object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidFailRendering: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note Để platform view trên Android hoạt động, hãy đảm bảo `MainActivity` của bạn kế thừa `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: --- # File: flutter-handle-paywall-actions --- --- title: "Xử lý hành động nút trong Flutter SDK" description: "Xử lý hành động nút paywall trong Flutter bằng Adapty để tối ưu hóa việc kiếm tiền từ ứng dụng." --- Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm [nút trong Paywall Builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một ID hành động tùy chỉnh. 2. Viết code trong ứng dụng của bạn để xử lý từng hành động đã gán. Hướng dẫn này hướng dẫn cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn. :::warning **Chỉ có giao dịch mua và khôi phục được xử lý tự động.** Tất cả các hành động nút khác, chẳng hạn như đóng paywall hoặc mở liên kết, đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code ứng dụng, triển khai trình xử lý cho các hành động `CloseAction` và `AndroidSystemBackAction`. :::info Trong Flutter SDK, các hành động `CloseAction` và `AndroidSystemBackAction` mặc định sẽ kích hoạt việc đóng paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một paywall có thể kích hoạt mở paywall khác. ::: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; default: break; } } ``` ## Mở URL từ paywall \{#open-urls-from-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch mua), hãy thêm phần tử **Link** trong Paywall Builder và xử lý nó giống như nút với hành động **Open URL**. ::: Để thêm nút mở liên kết từ paywall (ví dụ: **Điều khoản sử dụng** hoặc **Chính sách bảo mật**): 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng, triển khai trình xử lý cho hành động `openUrl` để mở URL nhận được trong trình duyệt. ```dart // You have to install url_launcher plugin in order to handle urls: // https://pub.dev/packages/url_launcher void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { switch (action) { case OpenUrlAction(url: final url): final Uri uri = Uri.parse(url); launchUrl(uri, mode: LaunchMode.inAppBrowserView); break; default: break; } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút đăng nhập người dùng vào ứng dụng: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Login**. 2. Trong code ứng dụng, triển khai trình xử lý cho hành động `login` để xác định người dùng. ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Handle login action Navigator.of(context).push(MaterialPageRoute(builder: (context) => LoginScreen())); break; default: break; } } ``` ## Xử lý hành động tùy chỉnh \{#handle-custom-actions\} Để thêm nút xử lý các hành động khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và đặt cho nó một ID. 2. Trong code ứng dụng, triển khai trình xử lý cho ID hành động bạn đã tạo. Ví dụ, nếu bạn có một bộ ưu đãi gói đăng ký khác hoặc sản phẩm mua một lần, bạn có thể thêm nút để hiển thị một paywall khác: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another paywall break; default: break; } } ``` --- # File: flutter-handling-events --- --- title: "Flutter - Xử lý sự kiện paywall" description: "Khám phá cách xử lý các sự kiện liên quan đến gói đăng ký trong Flutter bằng Adapty để theo dõi tương tác người dùng hiệu quả." --- :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút bấm (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](flutter-handle-paywall-actions) để biết thêm chi tiết. ::: Các paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm thao tác nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua trên paywall. Tìm hiểu cách phản hồi các sự kiện này bên dưới. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** yêu cầu Adapty SDK v3.0 trở lên. ::: Để kiểm soát hoặc theo dõi các tiến trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai các phương thức `AdaptyUIPaywallsEventsObserver` và thiết lập observer trước khi hiển thị bất kỳ màn hình nào: ```javascript showLineNumbers title="Flutter" AdaptyUI().setPaywallsEventsObserver(this); ``` :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### Paywall xuất hiện \{#paywall-appeared\} Phương thức này được gọi khi màn hình paywall được hiển thị trên màn hình. :::note Trên iOS, cũng được gọi khi người dùng nhấn vào [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong paywall và một web paywall mở ra trong trình duyệt trong ứng dụng. ::: ```javascript showLineNumbers title="Flutter" void paywallViewDidAppear(AdaptyUIPaywallView view) { } ``` #### Paywall biến mất \{#paywall-disappeared\} Phương thức này được gọi khi màn hình paywall bị đóng khỏi màn hình. :::note Trên iOS, cũng được gọi khi một [web paywall](web-paywall#step-2a-add-a-web-purchase-button) được mở từ paywall trong trình duyệt trong ứng dụng biến mất khỏi màn hình. ::: ```javascript showLineNumbers title="Flutter" void paywallViewDidDisappear(AdaptyUIPaywallView view) { } ``` #### Chọn sản phẩm \{#product-selection\} Nếu một sản phẩm được chọn để mua (bởi người dùng hoặc hệ thống), phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { } ``` <Details> <summary>Ví dụ sự kiện (Nhấn để mở rộng)</summary> ```javascript { "productId": "premium_monthly" } ``` </Details> #### Bắt đầu mua \{#started-purchase\} Nếu người dùng khởi tạo quá trình mua, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { } ``` <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> #### Hoàn thành mua \{#finished-purchase\} Phương thức này được gọi khi giao dịch mua thành công, người dùng hủy giao dịch mua, hoặc giao dịch mua đang ở trạng thái chờ xử lý: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ``` <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": "AdaptyPurchaseResultSuccess", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Pending purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultPending" } } // User cancelled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultUserCancelled" } } ``` </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 hành động nút bấm](flutter-handle-paywall-actions) để biết chi tiết về cách đóng màn hình paywall. #### Hoàn thành điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi thử mở một [web paywall](web-paywall) cho một sản phẩm cụ thể. Điều này bao gồm cả các lần điều hướng thành công và thất bại: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **Tham số:** | Tham số | Mô tả | |:------------|:---------------------------------------------------------------------------------------------------------------| | **product** | Một `AdaptyPaywallProduct` mà web paywall được mở cho. Có thể là `null`. | | **error** | Một đối tượng `AdaptyError` nếu điều hướng web paywall thất bại; `null` nếu điều hướng thành công. | <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": "web_navigation_failed", "message": "Failed to open web paywall", "details": { "underlyingError": "Browser unavailable" } } } ``` </Details> #### Mua thất bại \{#failed-purchase\} Phương thức này được gọi khi giao dịch mua thất bại (ví dụ: do lỗi thanh toán hoặc lỗi mạng). Phương thức này **không** kích hoạt khi người dùng chủ động hủy hoặc giao dịch đang chờ xử lý — những trường hợp đó được xử lý bởi `paywallViewDidFinishPurchase`: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } ``` <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 tạo quá trình khôi phục, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartRestore(AdaptyUIPaywallView view) { } ``` #### Khôi phục thành công \{#successful-restore\} Nếu khôi phục giao dịch mua thành công, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { } ``` <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ý](flutter-listen-subscription-changes) để tìm hiểu cách kiểm tra và chủ đề [Phản hồi hành động nút bấm](flutter-handle-paywall-actions) để tìm hiểu cách đóng màn hình paywall. #### Khôi phục thất bại \{#failed-restore\} Nếu khôi phục giao dịch mua thất bại, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { } ``` <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> ### Tải dữ liệu và hiển thị \{#data-fetching-and-rendering\} #### Lỗi tải sản phẩm \{#product-loading-errors\} Nếu bạn không truyền mảng sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ máy chủ. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức này: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { } ``` <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 xảy ra lỗi trong quá trình hiển thị giao diện, lỗi đó sẽ được báo cáo bằng cách gọi phương thức này. Theo mặc định (kể từ v3.15.2), paywall sẽ tự động bị đóng khi xảy ra lỗi hiển thị, nhưng bạn có thể ghi đè hành vi này nếu cần. ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // Default behavior: view.dismiss() // Override with custom logic if needed, for example: // - Log the error // - Show an error message to the user } ``` <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, hãy cho chúng tôi biết. --- # File: flutter-use-fallback-paywalls --- --- title: "Flutter - Sử dụng paywall dự phòng" description: "Xử lý các trường hợp người dùng ngoại tuyến hoặc máy chủ Adapty không khả dụng" --- :::warning Paywall dự phòng được hỗ trợ từ Flutter SDK v2.11 trở lên. ::: Để duy trì trải nghiệm người dùng mượt mà, điều quan trọng là phải thiết lập [paywall dự phòng](/fallback-paywalls) cho các flow, [paywall](paywalls) và [onboarding](onboardings) của bạn. Biện pháp phòng ngừa này giúp mở rộng khả năng của ứng dụng trong trường hợp mất kết nối internet một phần hoặc hoàn toàn. * **Nếu ứng dụng không thể kết nối đến máy chủ Adapty:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng, và truy cập cấu hình onboarding đã lưu cục bộ. * **Nếu ứng dụng không thể kết nối internet:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng. Onboarding chứa nội dung từ xa và cần có kết nối internet để hoạt động. :::important Trước khi thực hiện các bước trong hướng dẫn này, hãy [tải xuống](/local-fallback-paywalls) các file cấu hình dự phòng từ Adapty. ::: ## Cấu hình \{#configuration\} 1. Thêm các tệp cấu hình dự phòng vào thư mục `assets` của ứng dụng ở thư mục gốc của dự án. 2. Gọi phương thức `.setFallback` **trước khi** bạn tải paywall hoặc onboarding mục tiêu. ```javascript showLineNumbers title="javascript" final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { await Adapty.setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số: | Tham số | Mô tả | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **assetId** | Đường dẫn đến tệp cấu hình dự phòng. | :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: --- # File: flutter-localizations-and-locale-codes --- --- title: "Sử dụng localizations và mã locale trong Flutter SDK" description: "Quản lý localizations và mã locale của ứng dụng để tiếp cận người dùng toàn cầu." --- ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một số tình huống mà mã locale phát huy tác dụng — ví dụ, khi bạn cần lấy paywall phù hợp với localization hiện tại của ứng dụng. Vì mã locale khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì độ phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng localization — và điều gì xảy ra tiếp theo — để luôn nhận được kết quả như mong đợi. ## Tiêu chuẩn mã locale tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã locale, Adapty sử dụng phiên bản được điều chỉnh nhẹ của [tiêu chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi mã gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Khớp mã locale \{#locale-code-matching\} Khi Adapty nhận được yêu cầu từ SDK phía client kèm mã locale và bắt đầu tìm localization tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi locale đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay bằng dấu gạch ngang (`-`) 2. Hệ thống tìm kiếm localization có mã locale khớp hoàn toàn 3. Nếu không tìm thấy, hệ thống lấy phần trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tiếp tục tìm kiếm 4. Nếu vẫn không tìm thấy, hệ thống trả về localization mặc định `en` Nhờ vậy, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` đều nhận được kết quả giống nhau. ## Triển khai localizations: cách khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang quan tâm đến localizations, nhiều khả năng bạn đã làm việc với các file chuỗi đã được bản địa hóa trong dự án. Trong trường hợp đó, chúng tôi khuyến nghị thêm một cặp key-value chứa mã locale Adapty tương ứng vào mỗi file localization. Sau đó, lấy giá trị của key đó khi gọi SDK, như sau: ```dart showLineNumbers // 1. Modify your app_en.arb, app_es.arb, app_pt_br.arb files /* app_en.arb */ "adapty_paywalls_locale": "en", /* app_es.arb */ "adapty_paywalls_locale": "es", /* app_pt_br.arb */ "adapty_paywalls_locale": "pt-br", // 2. Extract and use the locale code final locale = AppLocalizations.of(context)!.adapty_paywalls_locale; // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Cách này giúp bạn kiểm soát hoàn toàn localization nào sẽ được lấy về cho từng người dùng của ứng dụng. ## Triển khai localizations: cách khác \{#implementing-localizations-the-other-way\} Bạn cũng có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống) mà không cần định nghĩa mã locale một cách tường minh cho từng localization. Điều đó có nghĩa là lấy mã locale từ các đối tượng khác mà nền tảng cung cấp, như sau: ```dart showLineNumbers final locale = Localizations.localeOf(context).languageCode; // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Lưu ý rằng chúng tôi không khuyến nghị cách này vì một số lý do: 1. Trên iOS, ngôn ngữ ưu tiên và locale hiện tại không giống nhau. Nếu muốn localization được chọn đúng, bạn phải dựa vào logic của Apple — vốn hoạt động tốt nếu bạn dùng cách khuyến nghị với file chuỗi đã được bản địa hóa — hoặc tự tái tạo logic đó. 2. Rất khó dự đoán chính xác server của Adapty sẽ nhận được gì. Ví dụ, trên iOS, có thể lấy được một locale như `ar_OM@numbers='latn'` từ thiết bị và gửi lên server. Khi đó bạn sẽ không nhận được localization `ar-om` như mong đợi, mà thay vào đó là `ar` — điều có thể gây bất ngờ. Nếu bạn vẫn quyết định dùng cách này, hãy đảm bảo đã xử lý tất cả các trường hợp liên quan. --- # File: flutter-web-paywall --- --- title: "Triển khai web paywall trong Flutter SDK" description: "Thiết lập web paywall để nhận thanh toán mà không cần qua phí và kiểm duyệt của App Store." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trên dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywall bằng phương thức SDK. Phương thức `.openWebPaywall`: 1. Tạo một URL duy nhất cho phép Adapty liên kết paywall cụ thể được hiển thị cho người dùng với trang web mà họ được chuyển hướng đến. 2. Theo dõi khi người dùng quay lại ứng dụng, sau đó gọi `.getProfile` theo các khoảng thời gian ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không. Nhờ đó, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ kích hoạt trong ứng dụng gần như ngay lập tức. ```dart showLineNumbers title="Flutter" try { await Adapty().openWebPaywall(product: <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. Sử dụng khi các sản phẩm trong Adapty paywall của bạn khác với những sản phẩm trong web paywall. ::: #### Xử lý lỗi \{#handle-errors\} | Lỗi | Mô tả | Hành động khuyến nghị | |-----------------------------------------|------------------------------------------------------------|------------------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | Paywall chưa được cấu hình URL mua hàng qua web | Kiểm tra xem paywall đã được cấu hình đúng trong Adapty Dashboard chưa | | AdaptyError.productWithoutPurchaseUrl | Sản phẩm chưa có URL mua hàng qua web | Xác minh cấu hình sản phẩm trong Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Không thể mở URL trong trình duyệt | Kiểm tra cài đặt thiết bị hoặc cung cấp phương thức mua hàng thay thế | | AdaptyError.failedDecodingWebPaywallUrl | Không thể mã hóa đúng các tham số trong URL | Xác minh các tham số URL hợp lệ và được định dạng đúng | ## Mở web paywall trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} :::important Mở web paywall trong trình duyệt trong ứng dụng được hỗ trợ từ Adapty SDK v3.15 trở lên. ::: Theo mặc định, web paywall mở trong trình duyệt bên ngoài. Để mang lại trải nghiệm liền mạch cho người dùng, bạn có thể mở web paywall trong trình duyệt trong ứng dụng. Cách này hiển thị trang mua hàng web ngay trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt tham số `in` thành `.inAppBrowser`: ```dart showLineNumbers try { await Adapty().openWebPaywall( product: <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 xử lý các sự cố thường gặp khi sử dụng paywall được thiết kế trong Adapty Paywall Builder với Flutter SDK. ## Lấy cấu hình paywall thất bại \{#getting-a-paywall-configuration-fails\} **Vấn đề**: Phương thức `createPaywallView` không thể truy xuất cấu hình paywall. **Nguyên nhân**: Paywall chưa được bật hiển thị trên thiết bị trong Paywall Builder. **Giải pháp**: Bật toggle **Show on device** trong Paywall Builder. <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 bị tính gấp đôi \{#the-paywall-view-number-is-too-big\} **Vấn đề**: Số lượt xem paywall hiển thị gấp đôi so với dự kiến. **Nguyên nhân**: Bạn có thể đang gọi `logShowPaywall` trong code, khiến số lượt xem bị tính trùng khi sử dụng Paywall Builder. Với các paywall được thiết kế bằng Paywall Builder, analytics được theo dõi tự động, vì vậy bạn không cần dùng phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowPaywall` trong code nếu đang sử dụng Paywall Builder. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các sự cố liên quan đến Paywall Builder chưa được đề cập ở trên. **Giải pháp**: Nếu cần, hãy migrate SDK lên phiên bản mới nhất theo [hướng dẫn migration](flutter-sdk-migration-guides). Nhiều sự cố đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: flutter-quickstart-manual --- --- title: "Bật tính năng mua hàng trong paywall tùy chỉnh với Flutter SDK" description: "Tích hợp Adapty SDK vào các paywall Flutter tùy chỉnh để bật tính năng in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Giữ toàn quyền kiểm soát việc triển khai paywall, trong khi Adapty SDK tự động lấy sản phẩm, xử lý giao dịch mua mới và khôi phục các giao dịch trước đó. :::important **Hướng dẫn này dành cho các nhà phát triển đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để bật tính năng mua hàng, hãy sử dụng [Adapty Paywall Builder](flutter-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic mua hàng, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để bật tính năng in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi sản phẩm, giá cả và ưu đãi mà không cần chỉnh sửa code ứng dụng. - [**Placements**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (như `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho placement trong dashboard, sau đó yêu cầu chúng theo placement ID trong code. Cách này giúp bạn dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho từng nhóm người dùng. Hãy đảm bảo bạn hiểu những khái niệm này ngay cả khi làm việc với paywall tùy chỉnh. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng của mình. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy sản phẩm của mình. Để hiểu những gì cần làm trên dashboard, hãy theo dõi hướng dẫn bắt đầu nhanh [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc với hoặc không cần xác thực phía backend. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và đã xác định theo cách khác nhau. Đọc [hướng dẫn bắt đầu nhanh về xác định người dùng](flutter-quickstart-identify) để hiểu rõ đặc điểm và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `paywall` bằng cách truyền [placement](placements) ID vào phương thức `getPaywall`. 2. Lấy mảng sản phẩm cho paywall này bằng phương thức `getPaywallProducts`. ```dart showLineNumbers Future<void> loadPaywall() async { try { final paywall = await Adapty().getPaywall(placementId: 'YOUR_PLACEMENT_ID'); final products = await Adapty().getPaywallProducts(paywall: paywall); // Use products to build your custom paywall UI } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 2. Chấp nhận thanh toán \{#step-2-accept-purchases\} Khi người dùng nhấn vào một sản phẩm trong paywall tùy chỉnh của bạn, hãy gọi phương thức `makePurchase` với sản phẩm đã chọn. Phương thức này sẽ xử lý luồng mua hàng và trả về hồ sơ người dùng đã được cập nhật. ```dart showLineNumbers Future<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. Phương thức này sẽ đồng bộ lịch sử mua hàng của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```dart showLineNumbers Future<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 } } ``` ## Các bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Kiểm tra giao dịch mua trong [sandbox của App Store](test-purchases-in-sandbox) hoặc trên [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một giao dịch mua thử từ paywall. Để xem cách hoạt động trong một triển khai sẵn sàng cho môi trường production, hãy xem [PurchasesObserver](https://github.com/adaptyteam/AdaptySDK-Flutter/blob/master/example/lib/purchase_observer.dart) trong ứng dụng mẫu của chúng tôi, nơi minh họa cách xử lý giao dịch mua với error handling phù hợp, UI observers và tích hợp SDK toàn diện. Tiếp theo, [kiểm tra xem người dùng đã hoàn thành giao dịch mua chưa](flutter-check-subscription-status) để quyết định có nên hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí. --- # File: fetch-paywalls-and-products-flutter --- --- title: "Lấy paywalls và sản phẩm cho Remote Config paywalls trong Flutter SDK" description: "Lấy paywalls và sản phẩm trong Adapty Flutter SDK để tăng cường khả năng kiếm tiền từ người dùng." --- Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và các paywall tùy chỉnh. Để biết hướng dẫn lấy paywalls cho Paywall Builder, vui lòng tham khảo [Lấy Paywall Builder paywalls và cấu hình của chúng](flutter-get-pb-paywalls). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: <details> <summary>Trước khi bắt đầu lấy paywalls và sản phẩm trong ứng dụng của bạn (click để mở rộng)</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 paywall](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-flutter) trong ứng dụng của bạn. </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 paywalls, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng. Để hiển thị sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của bạn bằng phương thức `getPaywall`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywalls được cấu hình từ xa, vì vậy số lượng sản phẩm và các ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt — nếu hôm nay một paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <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 được kỳ vọng là một mã ngôn ngữ gồm một hoặc nhiều subtag được phân cách bằng dấu trừ (**-**). Subtag đầu tiên dành cho ngôn ngữ, subtag thứ hai dành cho vùng.</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><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 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 có kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ có trải nghiệm tải nhanh hơn, dù kết nối internet của họ có chập chờn thế nào. Cache được cập nhật thường xuyên, nên sử dụng trong phiên để tránh các yêu cầu mạng là hoàn toàn an toàn.</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 xóa thủ công.</p><p></p><p>Adapty SDK lưu trữ paywalls ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](flutter-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để lấy paywalls nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywalls, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối internet yếu.</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> | Đừng hardcode ID sản phẩm! Vì paywalls được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được các tình huống này. Ví dụ, nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị 2 sản phẩm đó. Tuy nhiên, nếu sau này bạn lấy được 3 sản phẩm, ứng dụng nên hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn phải hardcode là placement ID. Tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) gồm: danh sách ID sản phẩm, định danh paywall, Remote Config, và một số thuộc tính khác. | ## Lấy sản phẩm \{#fetch-products\} Sau khi có paywall, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số trả về: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) gồm: định danh sản phẩm, tên sản phẩm, giá, tiền tệ, thời hạn gói đăng ký, và một số thuộc tính khác. | Khi tự thiết kế giao diện paywall, bạn thường cần truy cập các thuộc tính này từ đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html). Dưới đây là các thuộc tính được sử dụng phổ biến nhất, nhưng hãy tham khảo tài liệu được liên kết để biết đầy đủ thông tin về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | Để hiển thị tiêu đề sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, chứ không phải locale của thiết bị. | | **Price** | Để hiển thị giá đã được bản địa hóa, dùng `product.price.localizedString`. Bản địa hóa này dựa trên thông tin locale của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price.amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedPeriod`. Bản địa hóa này dựa trên locale của thiết bị. Để lấy chu kỳ gói đăng ký theo chương trình, dùng `product.subscription?.period`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là ngày, tuần, tháng, năm, hoặc không xác định). Giá trị `numberOfUnits` sẽ cho bạn biết số đơn vị chu kỳ. Ví dụ, đối với gói đăng ký theo quý, bạn sẽ thấy `AdaptyPeriodUnit.month` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. | | **Introductory Offer** | Để hiển thị huy hiệu hoặc chỉ báo khác rằng gói đăng ký có ưu đãi giới thiệu, hãy xem thuộc tính `product.subscription?.offer?.phases`. Đây là một danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:<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ẽ thuộc loại `AdaptyPaymentMode.freeTrial`.<br/>• `price`: Giá giảm dưới dạng số. Đối với dùng thử miễn phí, tìm giá trị `0` ở đây.<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. 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ỳ gói đăng ký được định dạng theo locale của người dùng cho phần giảm giá. | ## Tăng tốc lấy paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywalls được lấy gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywalls, và người dùng của bạn có kết nối internet yếu, việc lấy paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như được mô tả chi tiết trong phần [Lấy thông tin paywall](fetch-paywalls-and-products-flutter#fetch-paywall-information) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Có thể gặp vấn đề tương thích ngược**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế paywalls hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywalls không được render. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để hưởng lợi từ việc lấy paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như mô tả dưới đây. Nếu không, hãy tiếp tục dùng `getPaywall` như đã mô tả [ở trên](fetch-paywalls-and-products-flutter#fetch-paywall-information). ::: :::note Phương thức `getPaywallForDefaultAudience` chưa được hỗ trợ trong Flutter SDK, nhưng sẽ sớm được thêm vào. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <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 được kỳ vọng là một mã ngôn ngữ gồm một hoặc nhiều subtag được phân cách bằng dấu trừ (**-**). Subtag đầu tiên dành cho ngôn ngữ, subtag thứ hai dành cho vùng.</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><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 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 có kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ có trải nghiệm tải nhanh hơn, dù kết nối internet của họ có chập chờn thế nào. Cache được cập nhật thường xuyên, nên sử dụng trong phiên để tránh các yêu cầu mạng là hoàn toàn an toàn.</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 xóa thủ công.</p> | --- # File: present-remote-config-paywalls-flutter --- --- title: "Render paywall designed by remote config in Flutter SDK" description: "Khám phá cách hiển thị paywall Remote Config trong Adapty Flutter SDK để cá nhân hóa trải nghiệm người dùng." --- Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần render trong code của ứng dụng để hiển thị nó cho người dùng. Vì Remote Config linh hoạt theo nhu cầu của bạn, bạn hoàn toàn chủ động quyết định nội dung và giao diện của paywall. Chúng tôi cung cấp một phương thức để lấy cấu hình remote, giúp bạn tự do hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## Lấy remote config của paywall và hiển thị nó \{#get-paywall-remote-config-and-present-it\} Để lấy remote config của một paywall, hãy truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID"); final String? headerText = paywall.remoteConfig?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang trực quan hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng xoay khác nhau, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy nhớ [ghi lại sự kiện xem paywall](present-remote-config-paywalls-flutter#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập dữ liệu cho funnel và A/B test. ::: Sau khi hiển thị paywall xong, tiếp tục thiết lập flow thanh toán. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Xem chi tiết về phương thức `.makePurchase()` tại [Thực hiện mua hàng](flutter-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng gọi là fallback paywall](flutter-use-fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu suất của các paywall. Trong khi dữ liệu mua hàng được thu thập tự động, việc ghi lại lượt xem paywall cần có sự tham gia của bạn vì chỉ bạn mới biết khi nào khách hàng nhìn thấy một paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```dart showLineNumbers try { final result = await Adapty().logShowPaywall(paywall: paywall); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:----------------------------------------------------------------------| | **paywall** | bắt buộc | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). | --- # File: flutter-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng di động với Flutter SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng di động là bước thiết yếu để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ cần hiển thị paywall là đủ để hỗ trợ mua hàng nếu bạn sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall. Nếu bạn không sử dụng Paywall Builder, bạn cần dùng một phương thức riêng là `.makePurchase()` để hoàn tất giao dịch mua và mở khóa nội dung mong muốn. Phương thức này là cổng để người dùng tương tác với paywall và tiến hành các giao dịch họ muốn. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm mà người dùng đang cố mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng. :::warning Lưu ý rằng ưu đãi giới thiệu sẽ chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh điều kiện nhận ưu đãi giới thiệu của người dùng trên iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Bỏ qua bước này có thể khiến ứng dụng bị từ chối khi phát hành. Hơn nữa, điều này có thể dẫn đến việc tính giá đầy đủ cho những người dùng đủ điều kiện nhận ưu đãi giới thiệu. ::: Hãy đảm bảo bạn đã [hoàn thành cấu hình ban đầu](quickstart) mà không bỏ qua bất kỳ bước nào. Nếu không, chúng tôi không thể xác thực các giao dịch mua. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang dùng [Paywall Builder](adapty-paywall-builder)?** Các giao dịch mua được xử lý tự động — bạn có thể bỏ qua bước này. **Muốn có hướng dẫn từng bước?** Xem [hướng dẫn quickstart](flutter-implement-paywalls-manually) để biết hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```dart showLineNumbers try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): if (profile.accessLevels['premium']?.isActive ?? false) { // Grant access to the paid features } break; case AdaptyPurchaseResultPending(): break; case AdaptyPurchaseResultUserCancelled(): break; default: break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | bắt buộc | Đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) được lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** | <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 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 phiên bản StoreKit của Apple thấp hơn v2.0 và phiên bản Adapty SDK thấp hơn v2.9.0, bạn cần cung cấp [Apple App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple deprecated. ::: ## Thay đổi gói đăng ký khi mua hàng \{#change-subscription-when-making-a-purchase\} Khi người dùng chọn một gói đăng ký mới thay vì gia hạn gói hiện tại, cách thức hoạt động phụ thuộc vào cửa hàng: - Đối với App Store, gói đăng ký được cập nhật tự động trong nhóm đăng ký. Nếu người dùng mua một gói đăng ký từ nhóm này trong khi đã có gói từ nhóm khác, cả hai gói đăng ký sẽ hoạt động đồng thời. - Đối với Google Play, gói đăng ký không được cập nhật tự động. Bạn cần quản lý việc chuyển đổi trong mã nguồn ứng dụng di động như mô tả bên dưới. Để thay thế gói đăng ký bằng một gói khác trên Android, hãy gọi phương thức `.makePurchase()` với tham số bổ sung: ```dart showLineNumbers try { final result = await adapty.makePurchase( product: product, subscriptionUpdateParams: subscriptionUpdateParams, ); // successful cross-grade } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Tham số yêu cầu bổ sung: | Tham số | Bắt buộc | Mô tả | | :--------------------------- | :------- |:--------------------------------------------------------------------------------------------------------| | **subscriptionUpdateParams** | bắt buộc | Đối tượng [`AdaptyAndroidSubscriptionUpdateParameters`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyAndroidSubscriptionUpdateParameters-class.html). | Bạn có thể đọc thêm về gói đăng ký và các chế độ thay thế trong tài liệu Google Developer: - [Về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Khuyến nghị từ Google về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Chế độ thay thế [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Lưu ý: phương thức này chỉ khả dụng cho việc nâng cấp gói đăng ký. Hạ cấp không được hỗ trợ. - Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Thay đổi gói đăng ký thực sự sẽ chỉ xảy ra khi chu kỳ thanh toán của gói đăng ký hiện tại kết thúc. ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <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\} <Callout type="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. </Callout> 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: ```dart showLineNumbers try { await Adapty().presentCodeRedemptionSheet(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` :::danger Dựa trên quan sát của chúng tôi, trang Offer Code Redemption trong một số ứng dụng có thể không hoạt động ổn định. Chúng tôi khuyến nghị chuyển hướng người dùng trực tiếp đến App Store. Để thực hiện điều này, bạn cần mở URL theo định dạng sau: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ### Quản lý gói trả trước (Android) \{#manage-prepaid-plans-android\} Nếu người dùng ứng dụng của bạn có thể mua [gói trả trước](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (ví dụ: mua gói đăng ký không tự gia hạn trong vài tháng), bạn có thể bật [giao dịch chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleEnablePendingPrepaidPlans(true), ); ``` --- # File: flutter-restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng di động với Flutter SDK" description: "Tìm hiểu cách khôi phục giao dịch mua trong Adapty để đảm bảo trải nghiệm người dùng liền mạch." --- Khôi phục giao dịch mua trên cả iOS và Android là tính năng cho phép người dùng lấy lại quyền truy cập vào nội dung đã mua trước đó — chẳng hạn như gói đăng ký hoặc in-app purchase — mà không bị tính phí thêm. Tính năng này đặc biệt hữu ích cho những người đã gỡ và cài lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn tiếp tục sử dụng nội dung đã mua mà không cần thanh toán lại. :::note Trong các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), giao dịch mua sẽ được khôi phục tự động mà không cần thêm code từ phía bạn. Nếu đó là trường hợp của bạn — bạn có thể bỏ qua bước này. ::: Để khôi phục giao dịch mua khi bạn không sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`: ```javascript showLineNumbers try { final profile = await Adapty().restorePurchases(); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful access restore } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Các tham số trả về: | Tham số | Mô tả | |---------|-----------| | **Profile** | <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 chế độ Observer trong Flutter SDK" description: "Triển khai chế độ observer trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong Flutter SDK." --- Nếu bạn đã có hệ thống xử lý mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể khám phá [chế độ Observer](observer-vs-full-mode). Ở dạng cơ bản, chế độ Observer cung cấp tính năng phân tích nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics. Nếu đây là những gì bạn cần, bạn chỉ cần: 1. Bật chế độ này khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. Làm theo hướng dẫn cài đặt cho [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk). 2. [Báo cáo giao dịch](report-transactions-observer-mode-flutter) từ hệ thống mua hàng hiện có của bạn lên Adapty. ## Thiết lập chế độ Observer \{#observer-mode-setup\} Bật chế độ Observer nếu bạn tự xử lý việc mua hàng và trạng thái gói đăng ký, đồng thời sử dụng Adapty để gửi các sự kiện gói đăng ký và analytics. :::important Khi chạy ở chế độ Observer, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều đó. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withObserverMode(true) // Enable observer mode ..withLogLevel(AdaptyLogLevel.verbose), ); ``` Tham số: | Tham số | Mô tả | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean kiểm soát [chế độ Observer](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong chế độ Observer \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thêm một số bước thiết lập trong chế độ Observer. Đây là những gì bạn cần làm ngoài các bước đã nêu ở trên: 1. Hiển thị paywall như bình thường đối với [paywall dùng Remote Config](present-remote-config-paywalls-flutter). 3. [Liên kết paywall](report-transactions-observer-mode-flutter) với các giao dịch mua hàng. --- # File: report-transactions-observer-mode-flutter --- --- title: "Báo cáo giao dịch trong Observer Mode với Flutter SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu với Flutter SDK." --- <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 mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng của mình. Điều quan trọng là phải thiết lập điều này **trước** khi phát hành ứng dụng để tránh sai sót trong analytics. Sử dụng `reportTransaction` để báo cáo rõ ràng từng giao dịch để Adapty nhận biết. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận ra giao dịch, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy bao gồm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```javascript showLineNumbers try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | <ul><li> Cho iOS: Mã định danh của giao dịch.</li><li> Cho Android: Mã định danh dạng chuỗi `purchase.getOrderId` của giao dịch mua hàng, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.</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://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ự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng hoặc khôi phục chúng. Điều quan trọng là phải thiết lập điều này **trước** khi phát hành ứng dụng để tránh sai sót trong analytics. Sử dụng `reportTransaction` trên cả hai nền tảng để báo cáo rõ ràng từng giao dịch, và sử dụng `restorePurchases` trên Android như một bước bổ sung để đảm bảo Adapty nhận biết giao dịch đó. :::warning **Đừng bỏ qua việc báo cáo giao dịch và khôi phục giao dịch mua hàng!** Nếu bạn không gọi các phương thức này, Adapty sẽ không nhận ra giao dịch, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy bao gồm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```javascript showLineNumbers // every time when calling transaction.finish() if (Platform.isAndroid) { try { await Adapty().restorePurchases(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc | <ul><li> Cho iOS, StoreKit 1: một đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).</li><li> Cho iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).</li><li> Cho Android: Mã định danh dạng chuỗi (purchase.getOrderId của giao dịch mua hàng, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.</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://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 trong App Store, do đó không cần báo cáo thủ công. - Phiên bản 3.2 không hỗ trợ Observer Mode. </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 Mobile Code](flutter-restore-purchase). :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `restorePurchases`, Adapty sẽ không nhận ra giao dịch, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: </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 định sử dụng paywall và/hoặc A/B test trong Observer Mode, bạn cần liên kết giao dịch đến từ cửa hàng ứng dụng của mình với paywall tương ứng trong code ứng dụng của bạn. Điều quan trọng là phải làm đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến sai sót trong analytics. ```javascript final transactionId = transaction.transactionIdentifier final variationId = paywall.variationId try { await Adapty().setVariationId('transactionId', variationId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` </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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: flutter-setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong Flutter SDK" description: "Tìm hiểu cách thiết lập thuộc tính người dùng trong Adapty để phân khúc đối tượng tốt hơn." --- Bạn có thể thiết lập các thuộc tính tùy chọn như email, số điện thoại, v.v. cho người dùng ứng dụng. Sau đó, bạn có thể dùng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM. ### Thiết lập thuộc tính người dùng \{#setting-user-attributes\} Để thiết lập thuộc tính người dùng, gọi phương thức `.updateProfile()`: ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setEmail("email@email.com") ..setPhoneNumber("+18888888888") ..setFirstName('John') ..setLastName('Appleseed') ..setGender(AdaptyProfileGender.other) ..setBirthday(DateTime(1970, 1, 3)); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Lưu ý rằng các thuộc tính bạn đã thiết lập trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các key được phép \{#the-allowed-keys-list\} Các key `<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 tùy chỉnh của người dùng \{#custom-user-attributes\} Bạn có thể thiết lập các thuộc tính tùy chỉnh của riêng mình. Những thuộc tính này thường liên quan đến cách người dùng sử dụng ứng dụng. Ví dụ, với ứng dụng thể dục, đó có thể là số buổi tập mỗi tuần; với ứng dụng học ngoại ngữ, đó là trình độ của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi có mục tiêu, đồng thời dùng trong phân tích để xác định chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```javascript showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..setCustomStringAttribute('value1', 'key1') ..setCustomDoubleAttribute(1.0, 'key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Để xóa một key hiện có, dùng phương thức `.withRemoved(customAttributeForKey:)`: ```javascript showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..removeCustomAttribute('key1') ..removeCustomAttribute('key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Đôi khi bạn cần biết những thuộc tính tùy chỉnh nào đã được thiết lập trước đó. Để làm điều này, hãy dùng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không cập nhật kịp thời, vì thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau bất kỳ lúc nào, do đó các thuộc tính trên server có thể đã thay đổi kể từ lần đồng bộ cuối cùng. ::: ### Giới hạn \{#limits\} - Tối đa 30 thuộc tính tùy chỉnh mỗi người dùng - Tên key tối đa 30 ký tự. Tên key có thể bao gồm các ký tự chữ và số, cùng với các ký tự sau: `_` `-` `.` - Giá trị có thể là chuỗi hoặc số thực (float) với tối đa 50 ký tự. --- # File: flutter-listen-subscription-changes --- --- title: "Kiểm tra trạng thái đăng ký trong Flutter SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký của người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng trong ứng dụng Flutter của bạn." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên dễ dàng hơn bao giờ hết. Bạn không cần phải chèn thủ công ID sản phẩm vào code. Thay vào đó, bạn có thể dễ dàng xác nhận trạng thái gói đăng ký của người dùng bằng cách kiểm tra [mức độ truy cập](access-level) đang hoạt động. <details> <summary>Trước khi bắt đầu kiểm tra trạng thá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à thuộc tính của đối tượng [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html). Chúng tôi khuyến nghị bạn lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](flutter-identifying-users#setting-customer-user-id-on-configuration), rồi cập nhật lại mỗi khi có thay đổi. Nhờ vậy, bạn có thể dùng đối tượng hồ sơ người dùng mà không cần gọi lại nhiều lần. Để nhận thông báo khi hồ sơ người dùng thay đổi, hãy lắng nghe sự kiện cập nhật hồ sơ như mô tả trong phần [Lắng nghe cập nhật hồ sơ, bao gồm mức độ truy cập](flutter-listen-subscription-changes) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ server \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ server, dùng phương thức `.getProfile()`: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số trả về: | Tham số | Mô tả | | --------- | ------------------------------------------------------------ | | Profile | <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 sẽ trả về kết quả mới nhất. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ server, dữ liệu từ cache sẽ được trả về thay thế. Ngoài ra, Adapty SDK cũng thường xuyên cập nhật cache của `AdaptyProfile` để đảm bảo thông tin luôn ở trạng thái mới nhất.</p> | Phương thức `.getProfile()` cung cấp hồ sơ người dùng, từ đó bạn có thể kiểm tra trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Chẳng hạn, nếu bạn có ứng dụng đọc báo và bán gói đăng ký theo từng chủ đề riêng biệt, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết trường hợp, bạn chỉ cần một mức độ truy cập duy nhất — khi đó, hãy dùng mức độ truy cập mặc định "premium". Dưới đây là ví dụ kiểm tra mức độ truy cập mặc định "premium": ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); if (profile?.accessLevels['premium']?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Lắng nghe cập nhật trạng thái đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận thông báo từ Adapty, bạn cần thực hiện thêm một số cấu hình: ```javascript showLineNumbers Adapty().didUpdateProfileStream.listen((profile) { // handle any changes to subscription state }); ``` Adapty cũng kích hoạt một sự kiện khi ứng dụng khởi động. Trong trường hợp này, trạng thái gói đăng ký được lưu trong cache sẽ được truyền vào. ### Cache trạng thái đăng ký \{#subscription-status-cache\} Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi server không khả dụng, dữ liệu cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ người dùng. Tuy nhiên, cần lưu ý rằng không thể yêu cầu dữ liệu trực tiếp từ cache. SDK định kỳ truy vấn server mỗi phút để kiểm tra có cập nhật hay thay đổi nào liên quan đến hồ sơ người dùng không. Nếu có bất kỳ thay đổi nào — chẳng hạn như giao dịch mới hoặc các cập nhật khác — chúng sẽ được đồng bộ vào dữ liệu cache để giữ cho cache luôn nhất quán với server. --- # File: flutter-deal-with-att --- --- title: "Xử lý ATT trong Flutter SDK" description: "Bắt đầu với Adapty trên Flutter để đơn giản hóa việc thiết lập và quản lý gói đăng ký." --- Nếu ứng dụng của bạn sử dụng framework AppTrackingTransparency và hiển thị yêu cầu ủy quyền theo dõi ứng dụng cho người dùng, bạn cần gửi [trạng thái ủy quyền](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) đó đến Adapty. ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setAppTrackingTransparencyStatus(AdaptyIOSAppTrackingTransparencyStatus.authorized); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::warning Chúng tôi khuyến nghị bạn gửi giá trị này càng sớm càng tốt khi nó thay đổi. Chỉ như vậy dữ liệu mới được gửi kịp thời đến các tích hợp bạn đã cấu hình. ::: --- # File: kids-mode-flutter --- --- title: "Chế độ Trẻ em trong Flutter SDK" description: "Dễ dàng bật Chế độ Trẻ em để tuân thủ chính sách của Apple và Google. Không thu thập IDFA, GAID hay dữ liệu quảng cáo trong Flutter SDK." --- Nếu ứng dụng Flutter của bạn dành cho trẻ em, bạn phải tuân thủ chính sách của [Apple](https://developer.apple.com/kids/) và [Google](https://support.google.com/googleplay/android-developer/answer/9893335). Nếu đang sử dụng Adapty SDK, bạn chỉ cần thực hiện vài bước đơn giản để cấu hình SDK đáp ứng các chính sách này và vượt qua quá trình xét duyệt của cửa hàng ứng dụng. ## Cần làm gì? \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, hãy cẩn thận khi sử dụng customer user ID. ID người dùng có định dạng `<TênĐệm.Họ>` sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như sử dụng email. Với Chế độ Trẻ em, cách làm tốt nhất là dùng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã băm hoặc UUID do thiết bị tạo) để đảm bảo tuân thủ. ## Bật Chế độ Trẻ em \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\} Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để thực hiện, vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** trong phần **Collect users' IP address**. ### Cập nhật trong code ứng dụng di động \{#updates-in-your-mobile-app-code\} Để tuân thủ chính sách, hãy tắt việc thu thập IDFA của người dùng (cho iOS), GAID/AAID (cho Android) và địa chỉ IP. **Android: Cập nhật cấu hình SDK** ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') // highlight-start ..withGoogleAdvertisingIdCollectionDisabled(true), // set to `true` ..withIpAddressCollectionDisabled(true), // set to `true` // highlight-end ); } catch (e) { // handle the error } ``` **iOS: Bật Chế độ Trẻ em bằng CocoaPods** 1. Cập nhật Podfile của bạn: - Nếu bạn **chưa có** phần `post_install`, hãy thêm toàn bộ khối code bên dưới. - Nếu bạn **đã có** phần `post_install`, hãy hợp nhất các dòng được đánh dấu vào đó. ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. Áp dụng các thay đổi bằng cách chạy ```sh showLineNumbers title="Shell" pod install ``` --- # File: flutter-get-onboardings --- --- title: "Lấy onboarding trong Flutter SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho Flutter." --- Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng builder trên Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Flutter của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement cùng cấu hình hiển thị như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Flutter SDK](sdk-installation-flutter) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Lấy onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng no-code builder của chúng tôi, nó được lưu dưới dạng một container chứa cấu hình mà ứng dụng của bạn cần lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập form). Container cũng tự động theo dõi các sự kiện analytics, nên bạn không cần tự triển khai việc theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy onboarding, sử dụng phương thức `getOnboarding`: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboarding(placementId: "YOUR_PLACEMENT_ID"); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` Tiếp theo, gọi phương thức `createOnboardingView` để lấy view bạn sẽ hiển thị. :::warning Kết quả của phương thức `createOnboardingView` chỉ có thể dùng một lần. Nếu bạn cần dùng lại, hãy gọi lại phương thức `createOnboardingView`. Gọi nó hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers try { final onboardingView = await Adapty().createOnboardingView(onboarding: onboarding); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <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 là một mã ngôn ngữ gồm một hoặc hai thẻ con phân tách bằng dấu gạch ngang (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</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 khi gặp lỗi. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</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 họ sẽ tải nhanh hơn dù kết nối có chập chờn. Cache được cập nhật thường xuyên, nên sử dụng trong phiên làm việc để tránh các yêu cầu mạng là an toàn.</p><p></p><p>Lưu ý rằng cache vẫn giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt hoặc xóa thủ công.</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 dùng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</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 `loadTimeout` đã chỉ định, vì thao tác có thể bao gồm nhiều yêu cầu bên dưới.</p> | Tham số phản hồi: | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyOnboarding-class.html) bao gồm: định danh và cấu hình của onboarding, Remote Config, và một số thuộc tính khác. | ## Tăng tốc lấy onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được lấy gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong các trường hợp có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào. Để giải quyết điều này, bạn có thể dùng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị vẫn là lấy onboarding bằng phương thức `getOnboarding`, như mô tả chi tiết trong phần [Lấy onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc dùng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, yêu cầu thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không đúng. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ việc nhắm mục tiêu theo quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ lấy nhanh hơn vượt trội hơn những hạn chế này trong trường hợp sử dụng của bạn, hãy dùng `getOnboardingForDefaultAudience` như bên dưới. Nếu không, hãy dùng `getOnboarding` như mô tả [ở trên](#fetch-onboarding). ::: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboardingForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |-----------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <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 là một mã ngôn ngữ gồm một hoặc hai thẻ con phân tách bằng dấu gạch ngang (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.</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 khi gặp lỗi. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.</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 họ sẽ tải nhanh hơn dù kết nối có chập chờn. Cache được cập nhật thường xuyên, nên sử dụng trong phiên làm việc để tránh các yêu cầu mạng là an toàn.</p><p></p><p>Lưu ý rằng cache vẫn giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt hoặc xóa thủ công.</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 dùng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</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." --- Nếu bạn đã tùy chỉnh một onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code Flutter của mình để hiển thị cho người dùng. Onboarding đó đã bao gồm cả nội dung cần hiển thị lẫn cách thức hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Flutter SDK](sdk-installation-flutter) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Adapty Flutter SDK cung cấp hai cách để hiển thị onboarding: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị onboarding dưới dạng màn hình độc lập, sử dụng phương thức `onboardingView.present()` trên `onboardingView` được tạo bởi phương thức `createOnboardingView`. Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại onboarding, hãy gọi `createOnboardingView` thêm một lần nữa để tạo một instance `onboardingView` mới. :::warning Tái sử dụng cùng một `onboardingView` mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```javascript showLineNumbers title="Flutter" try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Đóng onboarding \{#dismiss-the-onboarding\} Khi bạn cần đóng onboarding theo cách lập trình, hãy sử dụng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await onboardingView.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\} Cấu hình cách onboarding được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await onboardingView.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng onboarding vào cây widget hiện có, sử dụng widget `AdaptyUIOnboardingPlatformView` trực tiếp trong cây widget Flutter của bạn. ```javascript showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, // The onboarding object you fetched onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` :::note Để platform view trên Android hoạt động, hãy đảm bảo `MainActivity` của bạn kế thừa `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: ## Loader trong quá trình tải onboarding \{#loader-during-onboarding\} Khi hiển thị onboarding, bạn có thể nhận thấy một màn hình tải ngắn giữa splash screen và onboarding trong khi view bên dưới đang được khởi tạo. Bạn có thể xử lý điều này theo nhiều cách khác nhau tùy theo nhu cầu. #### Kiểm soát splash screen bằng onDidFinishLoading \{#control-splash-screen-using-ondidfinishloading\} :::note Cách này chỉ khả dụng khi nhúng onboarding dưới dạng widget. Không áp dụng cho màn hình hiển thị độc lập. ::: Cách tiếp cận đa nền tảng được khuyến nghị là giữ splash screen hoặc overlay tùy chỉnh hiển thị cho đến khi onboarding được tải đầy đủ, sau đó ẩn nó thủ công. Khi sử dụng widget nhúng, hãy đặt widget của bạn phủ lên trên và ẩn overlay khi `onDidFinishLoading` được kích hoạt: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Hide your custom splash screen or overlay here }, // ... other callbacks ) ``` ### Tùy chỉnh loader mặc định \{#customize-native-loader\} :::important Cách này phụ thuộc vào từng nền tảng và yêu cầu duy trì code UI native. Không được khuyến nghị trừ khi bạn đã duy trì các native layer riêng biệt trong ứng dụng của mình. ::: Nếu bạn cần tùy chỉnh loader mặc định, bạn có thể thay thế nó bằng các layout theo từng nền tảng. Cách này yêu cầu triển khai riêng cho Android và iOS: - **iOS**: Thêm `AdaptyOnboardingPlaceholderView.xib` vào dự án Xcode của bạn - **Android**: Tạo `adapty_onboarding_placeholder_view.xml` trong `res/layout` và định nghĩa một placeholder ở đó ## Tùy chỉnh cách mở liên kết trong onboarding \{#customize-how-links-open-in-onboardings\} :::important Tùy chỉnh cách mở liên kết trong onboarding được hỗ trợ từ Adapty SDK v3.15.1 trở lên. ::: Theo mặc định, các liên kết trong onboarding mở trong trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch bằng cách hiển thị trang web ngay trong ứng dụng của bạn, cho phép người dùng xem mà không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết trong trình duyệt bên ngoài thay thế, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `AdaptyWebPresentation.externalBrowser`: <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 padding (Android) \{#disable-safe-area-paddings-android\} Theo mặc định, trên thiết bị Android, view onboarding tự động áp dụng safe area padding để tránh các thành phần UI hệ thống như thanh trạng thái và thanh điều hướng. Tuy nhiên, nếu bạn muốn tắt hành vi này và kiểm soát toàn bộ layout, bạn có thể thực hiện bằng cách thêm một boolean resource vào ứng dụng: 1. Vào `android/app/src/main/res/values`. Nếu chưa có file `bools.xml`, hãy tạo mới. 2. Thêm resource sau: ```xml <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ả onboarding trong ứng dụng của bạn. --- # File: flutter-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong Flutter SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong Flutter sử dụng Adapty." --- Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể xử lý. Cách xử lý các sự kiện này phụ thuộc vào phương thức hiển thị bạn đang sử dụng: - **Hiển thị toàn màn hình**: Yêu cầu thiết lập một global event observer để xử lý sự kiện cho tất cả các onboarding view - **Widget nhúng**: Xử lý sự kiện thông qua các tham số callback inline trực tiếp trong widget Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Flutter SDK](sdk-installation-flutter) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Sự kiện hiển thị toàn màn hình \{#full-screen-presentation-events\} ### Thiết lập event observer \{#set-up-event-observer\} Để xử lý sự kiện cho các onboarding toàn màn hình, hãy triển khai `AdaptyUIOnboardingsEventsObserver` và thiết lập nó trước khi hiển thị: ```javascript showLineNumbers title="Flutter" AdaptyUI().setOnboardingsEventsObserver(this); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Xử lý sự kiện \{#handle-events\} Triển khai các phương thức sau trong observer của bạn: ```javascript showLineNumbers title="Flutter" void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { // Onboarding finished loading } void onboardingViewDidFailWithError( AdaptyUIOnboardingView view, AdaptyError error, ) { // Handle loading errors } void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle close action view.dismiss(); } void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle custom actions } void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle user input updates } void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { // Track analytics events } ``` ## Sự kiện widget nhúng \{#embedded-widget-events\} Khi sử dụng `AdaptyUIOnboardingPlatformView`, bạn có thể xử lý sự kiện thông qua các tham số callback inline trực tiếp trong widget. Lưu ý rằng sự kiện sẽ được gửi đến cả callback của widget và global observer (nếu đã thiết lập), nhưng global observer là tùy chọn: ```javascript showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Onboarding finished loading }, onDidFailWithError: (error) { // Handle loading errors }, onCloseAction: (meta, actionId) { // Handle close action }, onPaywallAction: (meta, actionId) { _openPaywall(actionId); }, onCustomAction: (meta, actionId) { // Handle custom actions }, onStateUpdatedAction: (meta, elementId, params) { // Handle user input updates }, onAnalyticsEvent: (meta, event) { // Track analytics events }, ) ``` ## Các loại sự kiện \{#event-types\} Các phần sau mô tả các loại sự kiện khác nhau mà bạn có thể xử lý, bất kể phương thức hiển thị nào bạn đang sử dụng. ### Xử lý custom action \{#handle-custom-actions\} Trong builder, bạn có thể thêm hành động **custom** vào một nút và gán ID cho nó. <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 như **Login** hoặc **Allow notifications**, phương thức delegate `onboardingController` sẽ được kích hoạt với case `.custom(id:)` và tham số `actionId` chính là **Action ID** từ builder. Bạn có thể tạo ID riêng của mình, như "allowNotifications". ```javascript // Full-screen presentation void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { switch (actionId) { case 'login': _login(); break; case 'allow_notifications': _allowNotifications(); break; } } // Embedded widget onCustomAction: (meta, actionId) { _handleCustomAction(actionId); } ``` <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 thành tải onboarding \{#finishing-loading-onboarding\} Khi onboarding hoàn tất việc tải, sự kiện này sẽ được kích hoạt: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { print('Onboarding loaded: ${meta.onboardingId}'); } // Embedded widget onDidFinishLoading: (meta) { print('Onboarding loaded: ${meta.onboardingId}'); } ``` <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ự quản lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ: bạn cần dừng hiển thị chính onboarding đó. ::: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { await view.dismiss(); } // Embedded widget onCloseAction: (meta, actionId) { Navigator.of(context).pop(); } ``` <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 onboarding đóng, có một cách đơn giản hơn — xử lý sự kiện đóng và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall: Lưu ý rằng, đối với iOS, chỉ một view (paywall hoặc onboarding) có thể được hiển thị trên màn hình cùng một lúc. Nếu bạn hiển thị paywall lên trên onboarding, bạn không thể điều khiển onboarding ở nền theo cách lập trình. Việc cố gắng dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn còn hiển thị. Để tránh điều này, hãy luôn dismiss onboarding view trước khi hiển thị paywall. ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } Future<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 analytics event khi có các sự kiện liên quan đến điều hướng xảy ra trong flow onboarding: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { trackEvent(event.type, meta.onboardingId); } // Embedded widget onAnalyticsEvent: (meta, event) { trackEvent(event.type, meta.onboardingId); } ``` Đối tượng `event` có thể là một trong các loại sau: | Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình được hoàn thành. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn thành) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `secondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy [gán ID `final` cho màn hình cuối](design-onboarding). | | `unknown` | Dành cho bất kỳ loại sự kiện không được nhận diện nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của flow onboarding | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | <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: flutter-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong Flutter SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng Flutter của bạn với Adapty SDK." --- Khi người dùng trả lời câu hỏi trong bài kiểm tra hoặc nhập dữ liệu vào một trường nhập liệu, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Ví dụ: ```dart // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Process data } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Process data } ``` Xem định dạng action [tại đây](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyUIOnboardingPlatformView/onStateUpdatedAction.html). <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\} ### Bổ sung dữ liệu vào hồ sơ người dùng \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu nhập vào với hồ sơ người dùng và tránh hỏi họ hai lần cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](flutter-setting-user-attributes) với dữ liệu đầu vào khi xử lý action. Ví dụ: bạn yêu cầu người dùng nhập tên vào trường văn bản có ID là `name` và muốn đặt giá trị của trường đó làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code ứng dụng, nó có thể trông như thế này: ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Sử dụng các bài kiểm tra trong onboarding, bạn cũng có thể tùy chỉnh paywall hiển thị cho người dùng sau khi họ hoàn thành onboarding. Ví dụ: bạn có thể hỏi người dùng về kinh nghiệm tập thể thao của họ và hiển thị các CTA và sản phẩm khác nhau cho các nhóm người dùng khác nhau. 1. [Thêm bài kiểm tra](onboarding-quizzes) trong trình xây dựng onboarding và gán ID có ý nghĩa cho các lựa chọn. 2. Xử lý các câu trả lời bài kiểm tra dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](flutter-setting-user-attributes) cho người dùng. ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho mỗi phân khúc bạn đã tạo. 5. [Hiển thị paywall](flutter-paywalls) cho placement trong code ứng dụng của bạn. Nếu onboarding của bạn có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho action của nút đó](flutter-handling-onboarding-events#opening-a-paywall). --- # File: flutter-sdk-call-order --- --- title: "Thứ tự gọi hàm trong Flutter SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution và lỗi #2002 không liên tục bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự." --- `Adapty().activate()` phải hoàn thành trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi hàm này trả về kết quả, SDK chưa có trạng thái. Mọi lệnh gọi trước hoặc song song với `activate()` sẽ thất bại với lỗi [`#2002 notActivated`](error-handling-on-flutter-react-native-unity#custom-network-codes). Nếu ứng dụng của bạn xác thực người dùng và bạn thu thập customer user ID sau khi khởi chạy, hãy gọi `Adapty().identify()` tại thời điểm đó. Đừng gọi các phương thức liên quan đến hành động người dùng cho đến khi `identify` trả về kết quả. Các lệnh gọi chạy đua với nó sẽ thất bại với [`#3006 profileWasChanged`](error-handling-on-flutter-react-native-unity#custom-network-codes), hoặc rơi vào hồ sơ người dùng ẩn danh được tạo lúc kích hoạt. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và thông tin sở hữu cài đặt không phải lúc nào cũng được chuyển sang hồ sơ đã xác định. Nếu ứng dụng không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) áp dụng quy tắc tương tự. Hãy khởi tạo chúng trước và chờ callback UID của chúng trước khi gọi `Adapty().activate`. Nếu không, MMP ID sẽ gắn vào một hồ sơ ẩn danh tạm thời và không phải lúc nào cũng được chuyển sang hồ sơ đã xác định. Về các lưu ý riêng của AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Hướng đi của bạn phụ thuộc vào hai điều: thời điểm bạn biết customer user ID và việc bạn có sử dụng SDK MMP hoặc analytics hay không. - **Bước 2 và 5**: Bắt buộc với mọi ứng dụng. Kích hoạt SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ bắt buộc nếu bạn tích hợp SDK MMP hoặc analytics (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ bắt buộc nếu ứng dụng xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn đã có customer user ID lúc khởi chạy ứng dụng, hãy truyền trực tiếp vào `activate()` (bước 2a). Hướng này không bao giờ tạo hồ sơ ẩn danh, vì vậy bước 4 là không cần thiết. | Bước | Lệnh gọi | Thời điểm | Ghi chú | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi chạy ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty().activate(configuration: ...)` với `withCustomerUserId` được thiết lập trong configuration | Khởi chạy ứng dụng, sau bước 1, nếu bạn đã có customer user ID | Được khuyến nghị. Không bao giờ tạo hồ sơ ẩn danh. | | 2b | `Adapty().activate(configuration: ...)` không có `withCustomerUserId` | Khởi chạy ứng dụng, sau bước 1, nếu bạn chưa có customer user ID (hoặc không thu thập) | Adapty tạo một hồ sơ ẩn danh. | | 3 | `Adapty().setIntegrationIdentifier(key: ..., value: ...)` cho từng MMP | Sau bước 2, trước mọi lệnh gọi liên quan đến hành động người dùng | Bắt buộc để MMP ID gắn vào đúng hồ sơ. | | 4 | `await Adapty().identify(customerUserId)` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên hướng 2b có xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong lúc `identify` sẽ tạo ra `#3006 profileWasChanged`. | | 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ ổn định. | :::important Bỏ qua các bước này sẽ gây ra mất quyền truy cập premium cho người dùng cũ, thiếu `appsflyer_id` trên hồ sơ, và paywall được trả về cho sai đối tượng. ::: ## Cài đặt web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng qua web checkout (Stripe, Paddle) và sau đó cài đặt ứng dụng native, lần `activate()` đầu tiên trên thiết bị sẽ tạo một hồ sơ ẩn danh mới. Hồ sơ này không được liên kết với hồ sơ web. Nếu bạn có thể xác định customer user ID trước khi khởi chạy ứng dụng (từ luồng xác thực hoặc install referrer), hãy truyền trực tiếp vào `activate()`. Nếu không, giao dịch mua trên web sẽ không hiển thị trên thiết bị cho đến khi bạn gọi `identify("YOUR_USER_ID")` và sau đó gọi `restorePurchases`. Về metadata cần gửi kèm mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: flutter-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong Flutter SDK" description: "Tải paywall Adapty một cách đáng tin cậy: thời điểm, bộ nhớ cache và các mẫu fallback cho Flutter." --- Một lần tải paywall đáng tin cậy trên Flutter cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng, và fallback một cách linh hoạt khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ cache và các mẫu fallback để đạt được điều đó. :::tip Các quy tắc này giả định rằng `Adapty().activate()` và `Adapty().identify()` đã hoàn tất. Xem [Thứ tự gọi trong Flutter SDK](flutter-sdk-call-order). ::: ## Quy tắc và những lỗi thường gặp \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement bạn sắp hiển thị. | Tải trước tất cả các placement đồng thời khi khởi động. | Tải trước hàng loạt sẽ chặn luồng chính và gây màn hình đen trong thời gian đó. | | Gọi `getPaywall` sau khi attribution đã có cơ hội xử lý xong — ví dụ, 1–2 giây sau khi `activate` hoặc sau khi `didUpdateProfileStream` kích hoạt. | Gọi `getPaywall` trong `main()` trước `runApp`. | Attribution chưa được ghi nhận. Paywall sẽ được xác định theo đối tượng mặc định và âm thầm bỏ qua các phân khúc cũng như cá nhân hóa ASA. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mọi placement. | Chờ `getPaywall` vô thời hạn. | Nếu không có timeout, người dùng kết nối kém sẽ thấy màn hình trắng cho đến khi mạng phục hồi — hoặc họ sẽ thoát ứng dụng. | Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-flutter) để tham khảo các tham số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn đúng placement. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối liên tục kém (khu vực nông thôn, phương tiện công cộng, vùng bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy: AdaptyPaywallFetchPolicy.returnCacheDataElseLoad` cho mọi lần tải, ngoại trừ lần đầu tiên. - Cấu hình [paywall dự phòng](fallback-paywalls) cho mọi placement trong Adapty Dashboard. - Đặt `loadTimeout` từ 3–5 giây và chấp nhận paywall dự phòng khi timeout xảy ra. - Không chặn việc hiển thị paywall bởi `getProfile()`. Gọi `getPaywall` độc lập để một profile phản hồi chậm không làm tắc nghẽn giao diện. --- # File: flutter-test --- --- title: "Test & release in Flutter SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Flutter của bạn với Adapty." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng Flutter của mình, bước tiếp theo là kiểm tra xem mọi thứ đã được thiết lập đúng chưa và các giao dịch mua có hoạt động như mong đợi trên cả hai nền tảng iOS và Android không. Quá trình này bao gồm việc kiểm tra cả tích hợp SDK lẫn luồng mua hàng thực tế với môi trường sandbox của Apple và môi trường thử nghiệm của Google Play. ## Kiểm thử ứng dụng \{#test-your-app\} Để kiểm thử in-app purchase một cách toàn diện, hãy xem các hướng dẫn kiểm thử theo nền tảng: [Hướng dẫn kiểm thử iOS](test-purchases-in-sandbox) và [Hướng dẫn kiểm thử Android](testing-on-android). ## Chuẩn bị phát hành \{#prepare-for-release\} Trước khi nộp ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối cửa hàng và thông báo từ máy chủ đã được cấu hình - Giao dịch mua hoàn tất và được báo cáo về Adapty - Mức độ truy cập được mở khóa và khôi phục đúng cách - Các yêu cầu về quyền riêng tư và xét duyệt đã được đáp ứng --- # File: InvalidProductIdentifiers-flutter --- --- title: "Sửa lỗi Code-1000 noProductIDsFound trong Flutter SDK" description: "Khắc phục lỗi mã sản phẩm không hợp lệ khi quản lý gói đăng ký trong Adapty." --- Lỗi mã 1000, `noProductIDsFound`, cho biết không có sản phẩm nào bạn yêu cầu trên paywall có thể mua được trong App Store, mặc dù chúng đã được đăng ký ở đó. Đôi khi lỗi này đi kèm với cảnh báo `InvalidProductIdentifiers`. Nếu cảnh báo xuất hiện mà không có lỗi, bạn có thể bỏ qua an toàn. Nếu bạn gặp lỗi `noProductIDsFound`, hãy làm theo các bước sau để khắc phục: ## Bước 1. Kiểm tra bundle ID \{#step-2-check-bundle-id\} --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <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: 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-06-24T14:36:38.710Z_ _Successfully processed: 41/41 files_ # IOS - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-06-24T14:36:38.713Z Total files: 44 --- # File: sdk-installation-ios --- --- title: "Cài đặt & cấu hình iOS SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên iOS cho các ứng dụng dựa trên gói đăng ký." --- Adapty SDK bao gồm hai module chính để tích hợp vào ứng dụng của bạn: - **Core Adapty**: Module SDK cốt lõi, bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng. - **AdaptyUI**: Module tùy chọn, cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder) — công cụ no-code thân thiện giúp tạo paywall đa nền tảng dễ dàng. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples) của chúng tôi, minh họa toàn bộ quá trình thiết lập bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: Để xem hướng dẫn triển khai đầy đủ, bạn cũng có thể xem các video sau: <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. ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-iOS.svg?style=flat&logo=apple)](https://github.com/adaptyteam/AdaptySDK-iOS/releases) Adapty SDK được cài đặt qua Swift Package Manager. Trong Xcode, vào **File** -> **Add Package Dependency...**. Lưu ý rằng các bước thêm package dependency có thể khác nhau tùy phiên bản Xcode, vì vậy hãy tham khảo tài liệu Xcode nếu cần. 1. Nhập URL repository: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Chọn phiên bản (khuyến nghị chọn phiên bản ổn định mới nhất) và nhấn **Add Package**. 3. Trong cửa sổ **Choose Package Products**, chọn các module bạn cần: - **Adapty** (module cốt lõi) - **AdaptyUI** (tùy chọn - chỉ khi bạn dự định sử dụng Paywall Builder) :::note Lưu ý: - Để bật [chế độ Kids](kids-mode), hãy chọn **Adapty_KidsMode** thay vì **Adapty**. - Không chọn các package khác trong danh sách – bạn sẽ không cần chúng. ::: 4. Nhấn **Add Package** để hoàn tất cài đặt. 5. **Kiểm tra cài đặt:** Trong project navigator, bạn sẽ thấy "Adapty" (và "AdaptyUI" nếu đã chọn) dưới mục **Package Dependencies**. :::important Adapty iOS SDK 4.0 là phiên bản pre-release. Swift Package Manager không tự động resolve các phiên bản beta qua quy tắc **Up to Next Major Version** (`from:`), vì vậy bạn phải chỉ định chính xác phiên bản. Trong Xcode, đặt **Dependency Rule** thành **Exact Version** và nhập `4.0.0-beta.1`. Trong `Package.swift`, dùng `.exact("4.0.0-beta.1")`. Xem [Migrate Adapty iOS SDK lên v4](migration-to-ios-sdk-v4). ::: ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Kích hoạt Adapty SDK trong code của ứng dụng. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: Để lấy **Public SDK Key**: 1. 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 `"YOUR_PUBLIC_SDK_KEY"` trong code. :::important - Đảm bảo bạn dùng **Public SDK key** để khởi tạo Adapty, còn **Secret key** chỉ 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 chắc chắn 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 Hãy chờ `activate` hoàn thành trước khi gọi bất kỳ phương thức nào khác của Adapty SDK. Xem [Thứ tự gọi trong iOS SDK](ios-sdk-call-order) để biết trình tự đầy đủ. ::: Tiếp theo, hãy thiết lập paywall trong ứng dụng: - Nếu bạn dùng [Adapty Paywall Builder](adapty-paywall-builder), trước tiên hãy [kích hoạt module AdaptyUI](#activate-adaptyui-module-of-adapty-sdk) bên dưới, sau đó làm theo [hướng dẫn nhanh Paywall Builder](ios-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, xem [hướng dẫn nhanh cho paywall tùy chỉnh](ios-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn dự định dùng [Paywall Builder](adapty-paywall-builder) và đã [cài đặt module AdaptyUI](sdk-installation-ios#install-adapty-sdk), bạn cũng cần kích hoạt AdaptyUI. :::important Trong code, bạn phải kích hoạt module Adapty cốt lõi trước khi kích hoạt AdaptyUI. ::: <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 cache mặc định cho paywall](#set-up-media-cache-configuration-for-adaptyui). ::: ## Cài đặt tùy chọn \{#optional-setup\} ### Logging \{#logging\} #### Thiết lập hệ thống logging \{#set-up-the-logging-system\} Adapty ghi log các lỗi và thông tin quan trọng khác để giúp bạn hiểu những gì đang xảy ra. Các cấp độ log có sẵn: | Cấp độ | Mô tả | | ---------- | ------------------------------------------------------------ | | `error` | Chỉ ghi log các lỗi | | `warn` | Ghi log các lỗi và thông báo từ SDK không gây ra lỗi nghiêm trọng nhưng đáng chú ý | | `info` | Ghi log các lỗi, cảnh báo và các thông báo thông tin khác nhau | | `verbose` | Ghi log mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như lời gọi hàm, API query, v.v. | ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(logLevel: .verbose) // recommended for development ``` #### Chuyển hướng thông báo từ hệ thống logging \{#redirect-the-logging-system-messages\} Nếu bạn cần gửi log của Adapty đến hệ thống của mình hoặc lưu vào file, hãy dùng phương thức `setLogHandler` và triển khai logic logging tùy chỉnh bên trong. Handler này nhận các bản ghi log chứa nội dung thông báo và cấp độ nghiêm trọng. ```swift showLineNumbers title="Swift" Adapty.setLogHandler { record in writeToLocalFile("Adapty \(record.level): \(record.message)") } ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi, nhưng bạn có thể áp dụng thêm các chính sách bảo mật dữ liệu để tuân thủ hướng dẫn của cửa hàng hoặc quy định của từng quốc gia. #### Tắt thu thập và chia sẻ IDFA \{#disable-idfa-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `idfaCollectionDisabled` thành `true` để tắt thu thập và chia sẻ IDFA. Dùng tham số này để tuân thủ App Store Review Guidelines hoặc tránh kích hoạt lời nhắc App Tracking Transparency khi IDFA không cần thiết cho ứng dụng của bạn. Giá trị mặc định là `false`. Để biết thêm chi tiết về thu thập IDFA, tham khảo phần [Tích hợp Analytics](analytics-integration#disable-collection-of-advertising-identifiers). ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(idfaCollectionDisabled: true) ``` #### Tắt thu thập và chia sẻ địa chỉ IP \{#disable-ip-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Dùng tham số này để tăng cường quyền riêng tư của người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không bắt buộc trong ứng dụng của bạn. ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(ipAddressCollectionDisabled: true) ``` #### Cấu hình media cache cho paywall trong AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Lưu ý rằng cấu hình AdaptyUI là tùy chọn. Bạn có thể kích hoạt module AdaptyUI mà không cần config. Tuy nhiên, nếu bạn dùng config, tất cả các tham số đều bắt buộc. ```swift showLineNumbers title="Swift" // Configure AdaptyUI let adaptyUIConfiguration = AdaptyUI.Configuration( mediaCacheConfiguration: .init( memoryStorageTotalCostLimit: 100 * 1024 * 1024, memoryStorageCountLimit: .max, diskStorageSizeLimit: 100 * 1024 * 1024 ) ) // Activate AdaptyUI AdaptyUI.activate(configuration: adaptyUIConfiguration) ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :-------------------------- | :------- | :----------------------------------------------------------- | | memoryStorageTotalCostLimit | bắt buộc | Tổng giới hạn chi phí lưu trữ tính bằng byte. | | memoryStorageCountLimit | bắt buộc | Giới hạn số lượng item của bộ nhớ lưu trữ. | | diskStorageSizeLimit | bắt buộc | Giới hạn kích thước file trên đĩa của bộ nhớ lưu trữ tính bằng byte. Giá trị 0 nghĩa là không giới hạn. | ### Hành vi hoàn tất giao dịch \{#transaction-finishing-behavior\} :::info Tính năng này có sẵn từ SDK phiên bản 3.12.0 trở lên. ::: Mặc định, Adapty tự động hoàn tất giao dịch sau khi xác thực thành công. Tuy nhiên, nếu bạn cần xác thực giao dịch nâng cao (như xác thực receipt phía server, phát hiện gian lận hoặc logic nghiệp vụ tùy chỉnh), bạn có thể cấu hình SDK để hoàn tất giao dịch thủ công. ```swift showLineNumbers title="Swift" let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(transactionsFinishBehavior: .manual) // .auto is the default ``` Xem thêm chi tiết về cách hoàn tất giao dịch trong [hướng dẫn](ios-transaction-management). ### Xóa dữ liệu khi khôi phục từ backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ backup iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng được cache, chi tiết sản phẩm và paywall. Sau đó SDK sẽ khởi tạo lại với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ xóa cache SDK cục bộ. Lịch sử giao dịch với Apple và dữ liệu người dùng trên server Adapty vẫn được giữ nguyên. ::: ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(clearDataOnBackup: true) // default – false ``` ## Xử lý sự cố \{#troubleshooting\} #### Lỗi Swift 6 concurrency với Tuist \{#swift-6-concurrency-error-with-tuist\} Khi build với [Tuist](https://tuist.dev/), bạn có thể gặp lỗi biên dịch Swift 6 strict concurrency. Triệu chứng điển hình bao gồm lỗi không khớp thuộc tính `@Sendable` trong `AdaptyUIBuilderLogic` hoặc các lỗi Sendability cross-module tương tự. Nguyên nhân là Tuist tạo project Xcode từ các SPM package nhưng không giữ lại cài đặt `swift-tools-version: 6.0`. Kết quả là một số target Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`) biên dịch với quy tắc Swift 5 trong khi các target khác dùng Swift 6, tạo ra lỗi `@Sendable` cross-module. **Cách khắc phục**: Nâng cấp lên Adapty SDK **3.15.5** trở lên, phiên bản này giải quyết vấn đề bất kể phiên bản ngôn ngữ Swift hỗn hợp. **Cách tạm thời**: Nếu bạn không thể nâng cấp, hãy đặt rõ Swift 6 cho cả ba target Adapty trong cấu hình Tuist: ```swift showLineNumbers targetSettings: [ "Adapty": .init().swiftVersion("6"), "AdaptyUI": .init().swiftVersion("6"), "AdaptyUIBuilder": .init().swiftVersion("6"), ] ``` --- # File: ios-quickstart-paywalls --- --- title: "Bật tính năng mua hàng với Flow Builder trong iOS SDK" description: "Hướng dẫn nhanh để bật in-app purchase với Adapty Flow Builder." --- Để bật tính năng in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flow**](adapty-flow-builder) – chuỗi màn hình giới thiệu sản phẩm đến người dùng, được xây dựng trong Flow Builder no-code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện trong code, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](ios-quickstart-manual). - [**Placement**](placements) – vị trí và thời điểm bạn hiển thị flow trong app (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trong dashboard, sau đó lấy chúng theo placement ID trong code. Cách này giúp dễ dàng chạy A/B test và hiển thị các flow khác nhau cho từng nhóm người dùng. Adapty cung cấp ba cách để bật tính năng mua hàng trong app. Chọn một trong số đó tùy theo yêu cầu của app bạn: | Cách triển khai | Độ phức tạp | Khi nào sử dụng | |---|---|---| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng thanh toán trong no-code builder](quickstart-paywalls). Adapty tự động render và xử lý toàn bộ quy trình mua hàng, xác thực receipt, và quản lý gói đăng ký ở phía sau. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự triển khai giao diện paywall trong code app, nhưng vẫn lấy đối tượng flow từ Adapty để linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](ios-quickstart-manual). | | Observer mode | 🔴 Khó | Bạn đã có sẵn hệ thống xử lý mua hàng riêng và muốn tiếp tục sử dụng nó. Lưu ý rằng observer mode có những hạn chế nhất định trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, xem [Triển khai paywall thủ công](ios-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code app của bạn, bạn chỉ cần: 1. **Lấy flow**: Lấy từ Adapty. 2. **Hiển thị và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị view trong app. 3. **Xử lý hành động nút bấm**: Liên kết tương tác của người dùng với phản hồi tương ứng trong app. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. [Kết nối app của bạn với App Store](initial_ios) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo flow và thêm sản phẩm vào đó](create-paywall). 4. [Tạo placement và thêm flow vào đó](create-placement). 5. [Cài đặt và kích hoạt Adapty SDK](sdk-installation-ios) trong code app của bạn. Hướng dẫn này sử dụng API của Adapty iOS SDK v4 (beta). ## 1. Lấy flow \{#1-get-the-flow\} Các flow của bạn được liên kết với các placement được cấu hình trong dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc để chạy [A/B test](ab-tests). Để lấy một flow được tạo trong Adapty Flow Builder, bạn cần: 1. Lấy đối tượng `flow` theo [placement](placements) ID bằng phương thức `getFlow` và kiểm tra xem nó có cấu hình view hay không. 2. Lấy cấu hình view bằng phương thức `getFlowConfiguration`. Nó chứa các phần tử giao diện và style cần thiết để hiển thị flow. ```swift func loadFlow() async { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") guard flow.hasViewConfiguration else { print("Flow doesn't have a view configuration") return } flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow) } ``` ## 2. Hiển thị flow \{#2-display-the-flow\} Sau khi có cấu hình flow, bạn chỉ cần thêm vài dòng code để hiển thị flow. <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. `didFailPurchase`, `didFinishRestore`, `didFailRestore`, và `didReceiveError` là bắt buộc. Khi kiểm thử, bạn có thể chỉ cần sao chép code từ đoạn snippet dưới đây để ghi log các sự kiện này. :::tip Xử lý `didFinishPurchase` không bắt buộc, nhưng hữu ích khi bạn muốn thực hiện hành động sau khi mua hàng thành công. Nếu bạn không triển khai callback đó, flow sẽ tự động đóng lại. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didFailPurchase: { product, error in print("Purchase failed: \(error)") }, didFinishRestore: { profile in print("Restore finished successfully") }, didFailRestore: { error in print("Restore failed: \(error)") }, didReceiveError: { error in flowPresented = false print("Flow error: \(error)") } ) ``` </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) } ``` Triển khai `AdaptyFlowControllerDelegate` để xử lý các sự kiện. Tối thiểu, hãy triển khai các error handler bắt buộc (ba phương thức không có implementation mặc định): ```swift extension YourViewController: AdaptyFlowControllerDelegate { func flowController(_ controller: AdaptyFlowController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError) { print("Purchase failed: \(error)") } func flowController(_ controller: AdaptyFlowController, didFinishRestoreWith profile: AdaptyProfile) { print("Restore finished successfully") } func flowController(_ controller: AdaptyFlowController, didFailRestoreWith error: AdaptyError) { print("Restore failed: \(error)") } } ``` </TabItem> </Tabs> :::info Để biết thêm chi tiết về cách hiển thị flow, xem [hướng dẫn](ios-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút bấm \{#3-handle-button-actions\} Khi người dùng nhấn nút, iOS SDK tự động xử lý việc mua hàng, khôi phục, đóng flow và mở liên kết. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được định nghĩa trước và yêu cầu xử lý hành động trong code của bạn. Hoặc bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ, đây là cách xử lý nút đóng. Trong UIKit, SDK tự động đóng controller khi `.close` được kích hoạt — chỉ ghi đè nếu bạn muốn hành vi tùy chỉnh. Trong SwiftUI, bạn phải tự đặt binding `isPresented` thành `false`. :::tip Đọc các hướng dẫn của chúng tôi về cách xử lý [hành động](handle-paywall-actions) và [sự kiện](ios-handling-events) của nút bấm. ::: <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 } }, 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> Flow của bạn đã sẵn sàng để hiển thị trong app. [Kiểm thử mua hàng trong chế độ sandbox](test-purchases-in-sandbox) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](ios-check-subscription-status) để đảm bảo bạn hiển thị flow hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Đây là cách tất cả các bước trong hướng dẫn này có thể được tích hợp vào app của bạn cùng nhau. <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 } }, 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, 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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor --- --- title: "Tích hợp Adapty vào ứng dụng iOS với sự hỗ trợ của AI" description: "Hướng dẫn từng bước tích hợp Adapty vào ứng dụng iOS sử dụng Cursor, Context7, ChatGPT, Claude hoặc các công cụ AI khác." --- Hướng dẫn này sẽ đưa bạn qua từng bước tích hợp Adapty vào ứng dụng iOS với sự hỗ trợ của công cụ lập trình AI — bạn cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ code SDK nào. Bạn có thể thực hiện điều này với kỹ năng LLM tương tác, hoặc thủ công thông qua Dashboard. ### Cách tiếp cận với Skill (khuyến nghị) \{#skill-approach-recommended\} Kỹ năng Adapty CLI cho phép LLM của bạn thiết lập ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng của mình](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm kỹ năng, chạy `/adapty-cli` trong agent của bạn. Nó sẽ hướng dẫn bạn từng bước — bao gồm cả khi nào cần mở Dashboard để kết nối cửa hàng. ### Cách tiếp cận thủ công qua Dashboard \{#dashboard-approach\} Nếu bạn muốn cấu hình mọi thứ thủ công, đây là những gì bạn cần trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard thay cho bạn — bạn cần tự cung cấp chúng. 1. **Kết nối app store của bạn**: Trong Adapty Dashboard, vào **App settings → General**. Đây là yêu cầu bắt buộc để mua hàng hoạt động. [Kết nối App Store](integrate-payments) 2. **Sao chép Public SDK key của bạn**: Trong Adapty Dashboard, vào **App settings → General**, sau đó tìm phần **API keys**. Trong code, đây là chuỗi bạn truyền vào `Adapty.activate("YOUR_PUBLIC_SDK_KEY")`. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu trực tiếp đến sản phẩm trong code — Adapty phân phối chúng thông qua flow hoặc paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo một flow hoặc paywall và một placement**: Trong Adapty Dashboard, tạo một flow (hoặc paywall nếu bạn tự xây dựng giao diện), sau đó gán nó cho một placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty.getFlow("YOUR_PLACEMENT_ID")`. [Tạo flow](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình cho từng sản phẩm trên trang **Products**. Trong code, chuỗi được kiểm tra trong `profile.accessLevels["premium"]`. Mức độ truy cập `premium` mặc định phù hợp với hầu hết ứng dụng. Nếu người dùng trả phí được truy cập vào các tính năng khác nhau tùy theo sản phẩm (ví dụ, gói `basic` so với gói `pro`), hãy [tạo các mức độ truy cập bổ sung](assigning-access-level-to-a-product) trước khi bắt đầu lập trình. :::tip Khi bạn có đủ năm mục này, bạn đã sẵn sàng viết code. Hãy nói với LLM của bạn: "Public SDK key của tôi là X, placement ID của tôi là Y" để nó có thể tạo code khởi tạo và lấy paywall chính xác. ::: ### Thiết lập khi sẵn sàng \{#set-up-when-ready\} Những mục này không bắt buộc để bắt đầu lập trình, nhưng bạn sẽ cần chúng khi tích hợp trưởng thành hơn: - **A/B test**: Cấu hình trên trang **Placements**. Không cần thay đổi code. [A/B test](ab-tests) - **Thêm flow và placement**: Thêm nhiều lệnh gọi `getFlow` với các placement ID khác nhau. - **Tích hợp analytics**: Cấu hình trên trang **Integrations**. Thiết lập khác nhau tùy theo tích hợp. Xem [tích hợp analytics](analytics-integration) và [tích hợp attribution](attribution-integration). ## Cung cấp tài liệu Adapty cho LLM của bạn \{#feed-adapty-docs-to-your-llm\} ### Sử dụng Context7 (khuyến nghị) \{#use-context7-recommended\} [Context7](https://context7.com) là một máy chủ MCP cung cấp cho LLM của bạn quyền truy cập trực tiếp vào tài liệu Adapty cập nhật. LLM của bạn tự động lấy đúng tài liệu dựa trên những gì bạn hỏi — không cần dán URL thủ công. Context7 hoạt động với **Cursor**, **Claude Code**, **Windsurf** và các công cụ tương thích MCP khác. Để thiết lập, chạy: ``` npx ctx7 setup ``` Lệnh này phát hiện trình soạn thảo của bạn và cấu hình máy chủ Context7. Để thiết lập thủ công, xem [kho lưu trữ GitHub của Context7](https://github.com/upstash/context7). Sau khi cấu hình, hãy tham chiếu thư viện Adapty trong các prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the iOS SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn quan trọng. Hãy làm theo [hướng dẫn triển khai](#implementation-walkthrough) bên dưới từng bước để đảm bảo mọi thứ hoạt động đúng. ::: ### Sử dụng tài liệu dạng văn bản thuần \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng văn bản thuần Markdown. Thêm `.md` vào cuối URL của nó, hoặc nhấp vào **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor.md](https://adapty.io/docs/vi/adapty-cursor.md). Mỗi giai đoạn trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới bao gồm một khối "Gửi đến LLM của bạn" với các link `.md` để dán. Để có thêm tài liệu cùng một lúc, xem [tệp chỉ mục và tập hợp con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này đi qua việc tích hợp Adapty theo thứ tự triển khai. Mỗi giai đoạn bao gồm các tài liệu cần gửi cho LLM, những gì bạn sẽ thấy khi hoàn thành và các vấn đề thường gặp. ### Lên kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết code, hãy yêu cầu LLM phân tích dự án của bạn và tạo kế hoạch triển khai. Nếu công cụ AI của bạn hỗ trợ chế độ lập kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy sử dụng nó để LLM có thể đọc cả cấu trúc dự án lẫn tài liệu Adapty trước khi viết code. Hãy cho LLM biết bạn sử dụng cách tiếp cận nào cho việc mua hàng — điều này ảnh hưởng đến các hướng dẫn nó nên tuân theo: - [**Adapty Flow Builder**](adapty-flow-builder): Bạn tạo flow trong trình xây dựng no-code của Adapty, và SDK hiển thị chúng tự động. - [**Paywall tự tạo thủ công**](ios-quickstart-manual): Bạn tự xây dựng giao diện paywall trong code nhưng vẫn dùng Adapty để lấy sản phẩm và xử lý mua hàng. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên cơ sở hạ tầng mua hàng hiện có và chỉ dùng Adapty cho analytics và tích hợp. Chưa biết chọn cái nào? Đọc [bảng so sánh trong quickstart](ios-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Cài đặt gói Adapty SDK qua Swift Package Manager trong Xcode và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — không có gì hoạt động được nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-ios) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-ios.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Ứng dụng build và chạy được. Console Xcode hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra xem bạn đã thay placeholder bằng key thực từ App settings chưa. ::: ### Hiển thị flow hoặc paywall và xử lý mua hàng \{#show-flows-or-paywalls-and-handle-purchases\} Lấy một flow hoặc paywall theo placement ID, hiển thị nó và xử lý các sự kiện mua hàng. Các hướng dẫn bạn cần phụ thuộc vào cách bạn xử lý mua hàng. Hãy kiểm tra từng giao dịch trong sandbox khi bạn tiến hành — đừng đợi đến cuối. Xem [Kiểm tra mua hàng trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. <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ạng modal view \{#present-as-a-modal-view\} Để hiển thị flow hoặc paywall trên màn hình thiết bị dưới dạng modal view, dùng modifier `.flow` trong SwiftUI. Cách gọi tối thiểu cần `isPresented`, `flowConfiguration`, và bốn callback bắt buộc: ```swift showLineNumbers title="SwiftUI" .flow( isPresented: $flowPresented, flowConfiguration: <AdaptyUI.FlowConfiguration>, didFailPurchase: { _, _ in /* handle the error */ }, didFinishRestore: { _ in /* check access level and dismiss */ }, didFailRestore: { _ in /* handle the error */ }, didReceiveError: { _ in flowPresented = false } ) ``` Để kiểm soát nhiều hơn, thêm các callback tùy chọn như `didPerformAction` để xử lý sự kiện nhấn nút và `didFinishPurchase` để phản hồi khi mua hàng thành công: ```swift showLineNumbers title="SwiftUI" @State var flowPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the flow or paywall var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: <AdaptyUI.FlowConfiguration>, didPerformAction: { action in switch action { case .close: flowPresented = false default: // Handle other actions break } }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) } ``` Các tham số: | Tham số | Bắt buộc | Mô tả | |:-----------------------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc hiển thị màn hình flow hoặc paywall. | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Dùng phương thức `AdaptyUI.getFlowConfiguration(forFlow:)`. Xem [Lấy flows và paywalls](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn thành thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didReceiveError** | bắt buộc | Được gọi khi có lỗi render hoặc lỗi runtime từ flow script (ví dụ: exception JavaScript, mã `AdaptyUIError` `4105`). Với lỗi render, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định flow hoặc paywall hiển thị toàn màn hình hay dạng sheet. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của flow hoặc paywall được hiển thị. | | **didDisappear** | tùy chọn | Được gọi khi view của flow hoặc paywall bị đóng. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Có hai action ID được định nghĩa sẵn: `close` và `openURL`; các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi `Adapty.makePurchase()` hoàn thành thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web kết thúc. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi có lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm chỉ được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các alert item phía trên flow hoặc paywall. | | **showAlertBuilder** | tùy chọn | Hàm để render alert view. | | **placeholderBuilder** | tùy chọn | Hàm để render view placeholder trong khi flow hoặc paywall đang tải. Mặc định là `ProgressView`. | Xem chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết thêm chi tiết về các tham số. ### Hiển thị dạng non-modal view \{#present-as-a-non-modal-view\} Bạn cũng có thể hiển thị flows và paywalls dưới dạng navigation destination hoặc inline view trong navigation flow của ứng dụng. Dùng `AdaptyFlowView` trực tiếp trong các SwiftUI view của bạn: ```swift showLineNumbers title="SwiftUI" AdaptyFlowView( flowConfiguration: <AdaptyUI.FlowConfiguration>, didFailPurchase: { product, error in // Handle purchase failure }, didFinishRestore: { profile in // Handle successful restore }, didFailRestore: { error in // Handle restore failure }, didReceiveError: { error in // Handle the error (rendering or JS exception from the flow script). } ) ``` ## Hiển thị flows và paywalls trong UIKit \{#present-flows-and-paywalls-in-uikit\} Để hiển thị flow hoặc paywall trên màn hình thiết bị, thực hiện các bước sau: 1. Khởi tạo visual flow bạn muốn hiển thị bằng phương thức `AdaptyUI.flowController(with:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualFlow = try AdaptyUI.flowController( with: <AdaptyUI.FlowConfiguration>, delegate: <AdaptyFlowControllerDelegate> ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :---------- | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Dùng phương thức `AdaptyUI.getFlowConfiguration(forFlow:)`. Xem chủ đề [Lấy flows và paywalls](get-pb-paywalls) để biết thêm chi tiết. | | **delegate** | bắt buộc | Một `AdaptyFlowControllerDelegate` để lắng nghe các sự kiện của flow và paywall. Xem chủ đề [Xử lý sự kiện flow & paywall](ios-handling-events) để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :------------------------------------------------------- | | **AdaptyFlowController** | Đối tượng đại diện cho màn hình flow hoặc paywall được yêu cầu. | 2. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó trên màn hình thiết bị: ```swift showLineNumbers title="Swift" present(visualFlow, animated: true) ``` :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywalls, thực hiện mua hàng và các chức năng cơ bản khác. ::: </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 để hiển thị cho người dùng. Paywall đó đã chứa cả nội dung cần hiển thị lẫn cách hiển thị. Để lấy đối tượng `AdaptyUI.PaywallConfiguration` dùng bên dưới, xem [Lấy paywalls Paywall Builder và cấu hình của chúng](get-pb-paywalls). ## Hiển thị paywalls trong SwiftUI \{#present-paywalls-in-swiftui\} ### Hiển thị dạng modal view \{#present-as-a-modal-view\} Để hiển thị visual paywall trên màn hình thiết bị dưới dạng modal view, dùng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the paywall var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration: <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 } ) } ``` Các tham số: | Tham số | Bắt buộc | Mô tả | |:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc hiển thị màn hình paywall. | | **paywallConfiguration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Dùng phương thức `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)`. Xem chủ đề [Lấy paywalls Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn thành thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didFailRendering** | bắt buộc | Được gọi nếu có lỗi xảy ra khi render giao diện. Trong trường hợp này, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định paywall hiển thị toàn màn hình hay dạng modal. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của paywall được hiển thị. | | **didDisappear** | tùy chọn | Được gọi khi view của paywall bị đóng. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Các nút khác nhau có action ID khác nhau. Có hai action ID được định nghĩa sẵn: `close` và `openURL`; các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi `Adapty.makePurchase()` hoàn thành thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web kết thúc. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi có lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm chỉ được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các alert item phía trên paywall. | | **showAlertBuilder** | tùy chọn | Hàm để render alert view. | | **placeholderBuilder** | tùy chọn | Hàm để render view placeholder trong khi paywall đang tải. | Xem chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết thêm chi tiết về các tham số. ### Hiển thị dạng non-modal view \{#present-as-a-non-modal-view\} Bạn cũng có thể hiển thị paywalls dưới dạng navigation destination hoặc inline view trong navigation flow của ứng dụng. Dùng `AdaptyPaywallView` trực tiếp trong các SwiftUI view của bạn: ```swift showLineNumbers title="SwiftUI" AdaptyPaywallView( paywallConfiguration: <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ị paywalls trong UIKit \{#present-paywalls-in-uikit\} Để hiển thị visual paywall trên màn hình thiết bị, thực hiện các bước sau: 1. Khởi tạo visual paywall bạn muốn hiển thị bằng phương thức `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: <paywall configuration object>, delegate: <AdaptyPaywallControllerDelegate> ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :---------- | | **paywall configuration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Dùng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Xem chủ đề [Lấy paywalls Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **delegate** | bắt buộc | Một `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện paywall. Xem chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | **AdaptyPaywallController** | Đối tượng đại diện cho màn hình paywall được yêu cầu | 2. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó trên màn hình thiết bị: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywalls, thực hiện mua hàng và các chức năng cơ bản khác. ::: </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 } }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in flowPresented = false } ) ``` ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục mua hàng), hãy thêm phần tử **Link** trong builder và xử lý nó giống như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết từ flow hoặc paywall (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng, triển khai một handler cho hành động `openURL` để mở URL nhận được trong trình duyệt. :::info Trong iOS SDK, hành động `openURL` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case let .openURL(url): UIApplication.shared.open(url, options: [:]) // default behavior default: break } }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in flowPresented = false } ) ``` ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong builder, thêm một nút, gán cho nó hành động **Custom**, và gán cho nó một ID. 2. Trong code ứng dụng của bạn, implement một handler cho action ID bạn đã tạo. Ví dụ: nếu bạn có thêm một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút để hiển thị một flow hoặc paywall khác: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case let .custom(id): if id == "openNewPaywall" { // Display another flow or paywall } default: break } }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in flowPresented = false } ) ``` </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 */ }, didFailPurchase: { product, error in /* handle the error */ }, didStartRestore: { /* Handle the event */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false }, didFailLoadingProducts: { error in // Return `true` to retry loading return false } ) } ``` Bạn chỉ cần đăng ký những tham số closure cần dùng và bỏ qua những tham số không cần thiết. | Tham số | Bắt buộc | Mô tả | |:-----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Binding kiểm soát việc hiển thị màn hình flow hoặc paywall. | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Xem [Lấy flow và paywall](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn tất thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didReceiveError** | bắt buộc | Được gọi khi flow gặp lỗi hiển thị hoặc lỗi runtime từ script của flow (ví dụ: ngoại lệ JavaScript, mã `AdaptyUIError` `4105`). Trong trường hợp lỗi hiển thị, hãy [liên hệ bộ phận hỗ trợ Adapty](mailto:support@adapty.io). | | **placeholderBuilder** | tùy chọn | Hàm dùng để hiển thị view placeholder trong khi flow hoặc paywall đang tải. Mặc định là `ProgressView`. | | **fullScreen** | tùy chọn | Xác định flow hoặc paywall hiển thị ở chế độ toàn màn hình hay dạng sheet. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của flow hoặc paywall xuất hiện trên màn hình. | | **didDisappear** | tùy chọn | Được gọi khi view của flow hoặc paywall bị đóng lại. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Có hai action ID được định nghĩa sẵn: `close` và `openURL`; các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi `Adapty.makePurchase()` hoàn tất thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web hoàn tất. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi xảy ra lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm được tải một phần. | | **showAlertItem** | tùy chọn | Binding kiểm soát việc hiển thị các mục cảnh báo phía trên flow hoặc paywall. | | **showAlertBuilder** | tùy chọn | Hàm dùng để hiển thị view cảnh báo. | ## Xử lý sự kiện trong UIKit \{#handling-events-in-uikit\} Đối với các ứng dụng UIKit, sự kiện được xử lý thông qua giao thức `AdaptyFlowControllerDelegate`. Xem [Hiển thị flow & paywall - iOS](ios-present-paywalls) để biết cách thiết lập `AdaptyFlowController` với `AdaptyFlowControllerDelegate`. Giao thức khai báo 13 phương thức. Ba trong số đó không có triển khai mặc định và bắt buộc phải được implement khi tuân thủ: `didFailPurchase`, `didFinishRestoreWith`, và `didFailRestoreWith`. Các phương thức còn lại cung cấp triển khai mặc định không làm gì (no-op) và có thể được ghi đè khi bạn muốn hành vi tùy chỉnh. Các phương thức được nhóm theo mục đích bên dưới. ### Vòng đời \{#lifecycle\} ```swift showLineNumbers title="Swift" func flowControllerDidAppear(_ controller: AdaptyFlowController) { } func flowControllerDidDisappear(_ controller: AdaptyFlowController) { } ``` Các hàm này được gọi khi flow hoặc màn hình paywall được hiển thị và bị đóng lại. ### Hành động của người dùng \{#user-actions\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didPerform action: AdaptyUI.Action ) { } ``` Các trường hợp của `AdaptyUI.Action`: - `.close` — hành vi mặc định là đóng controller. Ghi đè nếu bạn muốn giữ controller trên màn hình hoặc thực hiện thêm thao tác dọn dẹp. - `.openURL(url:)` — hành vi mặc định là mở URL bằng `UIApplication.shared.open(...)`. - `.custom(id:)` — được kích hoạt cho các nút có ID hành động tùy chỉnh được thiết lập trong builder. ### Chọn sản phẩm \{#product-selection\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didSelectProduct product: AdaptyPaywallProduct ) { } ``` Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. Sản phẩm mang đầy đủ thông tin ưu đãi (tính đủ điều kiện được xác định tự động trong v4 — không còn kiểu `AdaptyPaywallProductWithoutDeterminingOffer` riêng biệt nữa). ### Sự kiện mua hàng \{#purchase-events\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didStartPurchase product: AdaptyPaywallProduct ) { } func flowController( _ controller: AdaptyFlowController, didFinishPurchase product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { // Default: dismiss the controller unless the purchase was cancelled. } func flowController( _ controller: AdaptyFlowController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError ) { } ``` `didFailPurchase` là sự kiện mua hàng duy nhất không có implementation mặc định. `didFinishPurchase` mặc định sẽ đóng controller khi mua thành công; chỉ cần override nếu bạn cần logic tùy chỉnh sau khi mua. ### Sự kiện khôi phục \{#restore-events\} ```swift showLineNumbers title="Swift" func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { } func flowController( _ controller: AdaptyFlowController, didFinishRestoreWith profile: AdaptyProfile ) { } func flowController( _ controller: AdaptyFlowController, didFailRestoreWith error: AdaptyError ) { } ``` `didFinishRestoreWith` và `didFailRestoreWith` không có implementation mặc định. Hãy kiểm tra xem `AdaptyProfile` trả về có chứa mức độ truy cập mong muốn hay không trước khi dismiss controller. ### Lỗi flow và lỗi tải sản phẩm \{#flow-errors-and-product-loading-errors\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didReceiveError error: AdaptyUIError ) { } func flowController( _ controller: AdaptyFlowController, didFailLoadingProductsWith error: AdaptyError ) -> Bool { // Return `true` to retry product loading; default returns `false`. return false } func flowController( _ controller: AdaptyFlowController, didPartiallyLoadProducts failedIds: [String] ) { } ``` `didReceiveError` kích hoạt khi có lỗi render và lỗi runtime từ script của flow (ngoại lệ JavaScript, mã `AdaptyUIError` `4105`). Với lỗi render, hãy [liên hệ Adapty Support](mailto:support@adapty.io). Với lỗi tải, trả về `true` từ `didFailLoadingProductsWith` để thử lại — hữu ích khi gặp sự cố mạng tạm thời. ### Điều hướng thanh toán web \{#web-payment-navigation\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didFinishWebPaymentNavigation product: AdaptyPaywallProduct?, error: AdaptyError? ) { } ``` Được gọi sau khi điều hướng thanh toán web kết thúc, dù thành công hay thất bại. </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 xảy ra trên màn hình paywall trong ứng dụng của bạn, hãy implement các phương thức `AdaptyPaywallControllerDelegate`. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### Chọn sản phẩm \{#product-selection\} Khi người dùng chọn một sản phẩm để mua, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer ) { } ``` <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 phải lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung lẫn cách thức hiển thị. :::warning Phần này chỉ áp dụng cho [Observer mode](observer-vs-full-mode). Nếu bạn không làm việc trong Observer mode, hãy tham khảo [iOS - Hiển thị paywall Paywall Builder](ios-present-paywalls). ::: <SDKv4> <details> <summary>Trước khi bắt đầu hiển thị flow (Click để 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 cho chúng. 5. [Tạo placement và gán flow hoặc paywall vào đó](create-placement). 6. [Lấy flow và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng. </details> <p> </p> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> 1. Implement đối tượng `AdaptyObserverModeResolver`. Protocol này giống như trong SDK v3 — observer mode không thay đổi giữa việc render flow và paywall: ```swift showLineNumbers title="Swift" func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // call onStartPurchase / onFinishPurchase to notify AdaptyUI about the purchase progress } func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, onFinishRestore: @escaping () -> Void) { // call onStartRestore / onFinishRestore to notify AdaptyUI about the restore progress } ``` 2. Tạo đối tượng cấu hình flow, truyền resolver của bạn vào tham số `observerModeResolver:`: ```swift showLineNumbers title="Swift" do { let flowConfiguration = try await AdaptyUI.getFlowConfiguration( forFlow: flow, observerModeResolver: <AdaptyObserverModeResolver> ) } catch { // handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------- | | **forFlow** | bắt buộc | Đối tượng `AdaptyFlow` lấy qua `Adapty.getFlow(placementId:)`. Xem [Lấy flow và paywall](get-pb-paywalls). | | **observerModeResolver** | bắt buộc | `AdaptyObserverModeResolver` bạn đã implement ở trên. | 3. Khởi tạo flow controller bằng `AdaptyUI.flowController(with:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualFlow = try AdaptyUI.flowController( with: flowConfiguration, delegate: <AdaptyFlowControllerDelegate> ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow. Xem [Lấy flow và paywall](get-pb-paywalls). | | **delegate** | bắt buộc | `AdaptyFlowControllerDelegate` để lắng nghe các sự kiện của flow. Xem [Xử lý sự kiện flow & paywall](ios-handling-events). | Giá trị trả về: | Đối tượng | Mô tả | | :------------------- | :----------------------------------------------------- | | AdaptyFlowController | Đối tượng đại diện cho màn hình flow được yêu cầu. | 4. Hiển thị controller: ```swift showLineNumbers title="Swift" present(visualFlow, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: </TabItem> <TabItem value="swiftui" label="SwiftUI" default> Trong SwiftUI, lấy cấu hình flow cùng với resolver và truyền vào modifier `.flow`: ```swift showLineNumbers title="SwiftUI" @State var flowPresented = false @State var flowConfiguration: AdaptyUI.FlowConfiguration? var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false default: break } }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false } ) .task { flowConfiguration = try? await AdaptyUI.getFlowConfiguration( forFlow: flow, observerModeResolver: <AdaptyObserverModeResolver> ) } } ``` Tham số `observerModeResolver:` trong `getFlowConfiguration` là thứ giúp flow được render tôn trọng logic mua hàng tùy chỉnh của bạn — bản thân modifier sử dụng các callback giống như full mode. :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: </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 (Click để 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 framework cho [iOS](sdk-installation-ios#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình paywall, gán sản phẩm cho chúng](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 5. [Tạo placement và gán paywall vào đó](create-placement) trong Adapty Dashboard. 6. [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng. </details> <p> </p> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> 1. Implement đối tượng `AdaptyObserverModeResolver`: ```swift showLineNumbers title="Swift" func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, onFinishRestore: @escaping () -> Void) { // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore } ``` Sự kiện `observerMode(didInitiatePurchase:onStartPurchase:onFinishPurchase:)` sẽ thông báo cho bạn rằng người dùng đã bắt đầu một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình khi nhận được callback này. Sự kiện `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)` sẽ thông báo cho bạn rằng người dùng đã bắt đầu khôi phục. Bạn có thể kích hoạt flow khôi phục tùy chỉnh của mình khi nhận được callback này. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về tiến trình mua hàng hoặc khôi phục. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị loader: | Callback | Mô tả | | :----------------- | :------------------------------------------------------------------------------- | | onStartPurchase() | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã bắt đầu. | | onFinishPurchase() | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã hoàn tất. | | onStartRestore() | Gọi callback này để thông báo cho AdaptyUI rằng việc khôi phục đã bắt đầu. | | onFinishRestore() | Gọi callback này để thông báo cho AdaptyUI rằng việc khôi phục đã hoàn tất. | 2. Tạo đối tượng cấu hình paywall: ```swift showLineNumbers title="Swift" do { let paywallConfiguration = try AdaptyUI.getPaywallConfiguration( forPaywall: <paywall object>, observerModeResolver: <AdaptyObserverModeResolver> ) } catch { // handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **ObserverModeResolver** | bắt buộc | Đối tượng `AdaptyObserverModeResolver` bạn đã implement ở bước trước. | 3. Khởi tạo paywall trực quan muốn hiển thị bằng phương thức `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: <paywall configuration object>, delegate: <AdaptyPaywallControllerDelegate> ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Delegate** | bắt buộc | `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện paywall. Tham khảo chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | Đối tượng đại diện cho màn hình paywall được yêu cầu. | Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó như sau: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: </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 } ) } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu thời gian hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **TagResolver** | tùy chọn | Xác định một dictionary các tag tùy chỉnh và giá trị tương ứng. Tag tùy chỉnh đóng vai trò placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Tag tùy chỉnh trong Paywall Builder để biết thêm chi tiết. | | **ObserverModeResolver** | tùy chọn | Đối tượng `AdaptyObserverModeResolver` bạn đã implement ở bước trước. | Tham số closure: | Tham số closure | Mô tả | | :------------------- | :-------------------------------------------------------------------------------- | | **didFinishRestore** | Được gọi nếu Adapty.restorePurchases() thành công. | | **didFailRestore** | Được gọi nếu Adapty.restorePurchases() thất bại. | | **didFailRendering** | Được gọi nếu xảy ra lỗi trong quá trình render giao diện. | Tham khảo chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết các tham số closure khác. :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: </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 (Click để 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 framework cho [iOS](sdk-installation-ios#activate-adapty-module-of-adapty-sdk), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk) và [Unity](sdk-installation-unity#activate-adapty-module-of-adapty-sdk). 2. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 3. [Cấu hình paywall, gán sản phẩm cho chúng](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 4. [Tạo placement và gán paywall vào đó](create-placement) trong Adapty Dashboard. 5. [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng. </details> <p> </p> <Tabs groupId="current-os" queryString> <TabItem value="swift" label="Swift" default> 1. Implement đối tượng `AdaptyObserverModeDelegate`: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } ``` Sự kiện `paywallController(_:didInitiatePurchase:onStartPurchase:onFinishPurchase:)` sẽ thông báo cho bạn rằng người dùng đã bắt đầu một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình khi nhận được sự kiện này. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về tiến trình mua hàng. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị loader: | Callback | Mô tả | | :--------------- | :------------------------------------------------------------------------------- | | onStartPurchase | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã bắt đầu. | | onFinishPurchase | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã hoàn tất. | 2. Khởi tạo paywall trực quan muốn hiển thị bằng phương thức `.paywallController(for:products:viewConfiguration:delegate:observerModeDelegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( for: <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 thời gian hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **ViewConfiguration** | bắt buộc | Đối tượng `AdaptyUI.LocalizedViewConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getViewConfiguration(paywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Delegate** | bắt buộc | `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện paywall. Tham khảo chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | | **ObserverModeDelegate** | bắt buộc | Đối tượng `AdaptyObserverModeDelegate` bạn đã implement ở bước trước. | | **TagResolver** | tùy chọn | Xác định một dictionary các tag tùy chỉnh và giá trị tương ứng. Tag tùy chỉnh đóng vai trò placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Tag tùy chỉnh trong Paywall Builder để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | Đối tượng đại diện cho màn hình paywall được yêu cầu. | Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó như sau: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: </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, 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 }, ) } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **Product** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu thời gian hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **Configuration** | bắt buộc | Đối tượng `AdaptyUI.LocalizedViewConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getViewConfiguration(paywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **TagResolver** | tùy chọn | Xác định một dictionary các tag tùy chỉnh và giá trị tương ứng. Tag tùy chỉnh đóng vai trò placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Tag tùy chỉnh trong paywall builder để biết thêm chi tiết. | Tham số closure: | Tham số closure | Mô tả | | :---------------------------------- | :-------------------------------------------------------------------------------- | | **didFinishRestore** | Được gọi nếu Adapty.restorePurchases() thành công. | | **didFailRestore** | Được gọi nếu Adapty.restorePurchases() thất bại. | | **didFailRendering** | Được gọi nếu xảy ra lỗi trong quá trình render giao diện. | | **observerModeDidInitiatePurchase** | Được gọi khi người dùng bắt đầu một giao dịch mua. | Tham khảo chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết các tham số closure khác. :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: </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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <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\} <Callout type="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. </Callout> 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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong iOS SDK" description: "Tìm hiểu cách thiết lập thuộc tính người dùng trong Adapty để phân khúc đối tượng hiệu quả hơn." --- Bạn có thể thiết lập các thuộc tính tùy chọn như email, số điện thoại, v.v. cho người dùng của ứng dụng. Sau đó, bạn có thể sử dụng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM. ### Thiết lập thuộc tính người dùng \{#setting-user-attributes\} Để thiết lập thuộc tính người dùng, hãy gọi phương thức `.updateProfile()`: <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ế độ Kids Mode trong iOS SDK" description: "Dễ dàng bật Kids Mode để tuân thủ chính sách của Apple. Không thu thập IDFA hay dữ liệu quảng cáo trong iOS SDK." --- Nếu ứng dụng iOS của bạn dành cho trẻ em, bạn phải tuân thủ các chính sách của [Apple](https://developer.apple.com/kids/). Nếu đang sử dụng Adapty SDK, bạn chỉ cần thực hiện vài bước đơn giản để cấu hình SDK theo đúng các chính sách này và vượt qua quy trình kiểm duyệt của cửa hàng. ## Yêu cầu cần thực hiện \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách cẩn thận. ID người dùng có định dạng `<FirstName.LastName>` chắc chắn sẽ bị xem là thu thập dữ liệu cá nhân, cũng như việc dùng email. Trong Kids Mode, cách tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: hashed ID hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ. ## Bật Kids Mode \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\} Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để thực hiện, vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** trong phần **Collect users' IP address**. ### Cập nhật trong code của ứng dụng \{#updates-in-your-mobile-app-code\} Để tuân thủ các chính sách, hãy tắt việc thu thập IDFA và địa chỉ IP của người dùng. <Tabs> <TabItem value="spm" label="Swift Package Manager" default> Nếu bạn dùng Swift Package Manager, bạn có thể bật Kids Mode bằng cách chọn module **Adapty_KidsMode** trong Xcode khi cài đặt SDK. Trong Xcode, vào **File** -> **Add Package Dependency...**. Lưu ý rằng các bước thêm package dependency có thể khác nhau giữa các phiên bản Xcode, hãy tham khảo tài liệu Xcode nếu cần. 1. Nhập URL repository: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Chọn phiên bản (khuyến nghị dùng phiên bản ổn định mới nhất) và nhấn **Add Package**. 3. Trong cửa sổ **Choose Package Products**, chọn các module bạn cần: - **Adapty_KidsMode** (module cốt lõi) - **AdaptyUI_KidsMode** (tùy chọn - chỉ cần nếu bạn dự định dùng Paywall Builder) Bạn không cần thêm bất kỳ package nào khác. 4. Nhấn **Add Package** để hoàn tất cài đặt. 5. Trong code của bạn, viết `import Adapty_KidsMode` thay vì `import Adapty`, và `import AdaptyUI_KidsMode` thay vì `import AdaptyUI`: ```swift ``` </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 hợp nhất các dòng được đánh dấu vào đó. ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. Chạy lệnh sau để áp dụng các thay đổi: ```sh showLineNumbers title="Shell" pod install ``` </TabItem> </Tabs> --- # File: get-onboardings --- --- title: "Lấy onboarding và cấu hình của chúng" description: "Tìm hiểu cách lấy onboarding trong Adapty." --- :::tip **Bắt đầu từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một giải pháp mạnh mẽ hơn so với onboarding. Khác với onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, tốc độ tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement cùng cấu hình hiển thị của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS, Android, React Native hoặc Flutter SDK](installation-of-adapty-sdks) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Lấy onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng builder no-code của chúng tôi, nó được lưu trữ dưới dạng một container với cấu hình mà ứng dụng của bạn cần lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần triển khai tính năng theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy một onboarding, sử dụng phương thức `getOnboarding`: ```swift showLineNumbers do { let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // the requested onboarding } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của bản dịch onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai subtag được phân tách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.</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 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ẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ yếu đến mức nào. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</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ữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để lấy onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</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 được đạt tới, 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 `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://swift.adapty.io/documentation/adapty/adaptyonboarding) với: định danh và cấu hình onboarding, Remote Config, và một số thuộc tính khác. | ## Tăng tốc lấy onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như được mô tả chi tiết trong phần [Lấy onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, đòi hỏi phải thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không chính xác. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ khả năng nhắm mục tiêu theo quốc gia, attribution hoặc thuộc tính tùy chỉnh. Nếu tốc độ lấy nhanh hơn vượt trội hơn những hạn chế này với trường hợp sử dụng của bạn, hãy dùng `getOnboardingForDefaultAudience` như hiển thị bên dưới. Nếu không, hãy sử dụng `getOnboarding` như được mô tả [ở trên](#fetch-onboarding). ::: ```swift showLineNumbers Adapty.getOnboardingForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(onboarding): // the requested onboarding case let .failure(error): // handle the error } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** | <p>tùy chọn</p><p>mặc định: `en`</p> | <p>Định danh của bản dịch onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai subtag được phân tách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.</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 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ẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ yếu đến mức nào. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.</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ữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để lấy onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</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 **Từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một giải pháp mạnh mẽ hơn so với onboarding. Khác với onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, tốc độ tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Onboarding đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS SDK](sdk-installation-ios) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Hiển thị onboarding bằng Swift \{#present-onboardings-in-swift\} Để hiển thị onboarding trực quan trên màn hình thiết bị, hãy thực hiện các bước sau: 1. Lấy cấu hình view onboarding bằng phương thức `.getOnboardingConfiguration`. 2. Khởi tạo onboarding trực quan mà bạn muốn hiển thị bằng phương thức `.onboardingController`: Tham số đầu vào: | Tham số | Bắt buộc | Mô tả | |:------------------------------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding configuration** | bắt buộc | Một đối tượng `AdaptyUI.OnboardingConfiguration` chứa tất cả các thuộc tính của onboarding. Dùng phương thức `AdaptyUI.getOnboardingConfiguration` để lấy nó. | | **delegate** | bắt buộc | Một `AdaptyOnboardingControllerDelegate` để lắng nghe các sự kiện của onboarding. | Kết quả trả về: | Đối tượng | Mô tả | |:-------------------------------|:--------------------------------------------------------| | **AdaptyOnboardingController** | Một đối tượng đại diện cho màn hình onboarding được yêu cầu | 3. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó trên màn hình thiết bị: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI // 0. Get an onboarding if you haven't done it yet let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Create Onboarding View Controller let onboardingController = try AdaptyUI.onboardingController( with: configuration, delegate: <AdaptyOnboardingControllerDelegate> ) // 3. Present it to the user present(onboardingController, animated: true) ``` ## Hiển thị onboarding bằng SwiftUI \{#present-onboardings-in-swiftui\} Để hiển thị onboarding trực quan trên màn hình thiết bị trong SwiftUI: ```swift showLineNumbers title="SwiftUI" // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Display the Onboarding View within your view hierarchy AdaptyOnboardingView( configuration: configuration, placeholder: { Text("Your Placeholder View") }, onCloseAction: { action in // hide the onboarding view }, onError: { error in // handle the error } ) ``` ## Thêm hiệu ứng chuyển cảnh mượt mà giữa màn hình splash và onboarding \{#add-smooth-transitions-between-the-splash-screen-and-onboarding\} Mặc định, giữa màn hình splash và onboarding, bạn sẽ thấy màn hình tải cho đến khi onboarding được load xong. Tuy nhiên, nếu muốn quá trình chuyển cảnh mượt mà hơn, bạn có thể tùy chỉnh — kéo dài màn hình splash hoặc hiển thị nội dung khác trong thời gian chờ. Để làm điều này, hãy định nghĩa một placeholder (nội dung sẽ hiển thị trong khi onboarding đang được tải). Nếu bạn định nghĩa placeholder, onboarding sẽ được tải ngầm và tự động hiển thị khi sẵn sàng. <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ở link trong onboarding \{#customize-how-links-open-in-onboardings\} :::important Tùy chỉnh cách mở link trong onboarding được hỗ trợ từ Adapty SDK v3.15.1 trở lên. ::: Mặc định, các link trong onboarding sẽ mở trong trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch bằng cách hiển thị trang web ngay trong ứng dụng, giúp người dùng xem nội dung mà không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở link bằng trình duyệt ngoài thay thế, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `.externalBrowser`: ```swift showLineNumbers let configuration = try AdaptyUI.getOnboardingConfiguration( forOnboarding: onboarding, externalUrlsPresentation: .externalBrowser // default – .inAppBrowser ) ``` --- # File: ios-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong iOS SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong iOS bằng Adapty." --- :::tip **Từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboarding. Không giống như onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS SDK](sdk-installation-ios) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể xử lý. Hãy tìm hiểu cách xử lý các sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình onboarding trong ứng dụng di động của bạn, hãy triển khai các phương thức `AdaptyOnboardingControllerDelegate`. ## Hành động tùy chỉnh \{#custom-actions\} Trong builder, bạn có thể thêm hành động **custom** cho một nút và gán cho nó một ID. <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ụ: khi người dùng nhấn vào một nút tùy chỉnh như **Login** hoặc **Allow notifications**, phương thức delegate `onboardingController` sẽ được kích hoạt với case `.custom(id:)` và tham số `actionId` chính là **Action ID** từ builder. Bạn có thể tự tạo ID của riêng mình, chẳng hạn như "allowNotifications". ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCustomAction action: AdaptyOnboardingsCustomAction) { if action.actionId == "allowNotifications" { // Request notification permissions } } func onboardingController(_ controller: AdaptyOnboardingController, didFailWithError error: AdaptyUIError) { // Handle errors } ``` <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ự xử lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ: bạn cần dừng hiển thị onboarding đó. ::: Ví dụ: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCloseAction action: AdaptyOnboardingsCloseAction) { controller.dismiss(animated: true) } ``` <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 đóng onboarding, có một cách đơn giản hơn — xử lý [`AdaptyOnboardingsCloseAction`](#closing-onboarding) và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall. Như vậy, sau sự kiện `AdaptyOnboardingsOpenPaywallAction`, bạn có thể dùng placement ID để lấy và mở paywall ngay lập tức. Lưu ý rằng tại một thời điểm chỉ có thể hiển thị một view (paywall hoặc onboarding) trên màn hình. Nếu bạn hiển thị paywall chồng lên onboarding, bạn không thể lập trình điều khiển onboarding ở nền. Việc cố gắng dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn dismiss view onboarding trước khi hiển thị paywall. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onPaywallAction action: AdaptyOnboardingsOpenPaywallAction) { // Dismiss onboarding before presenting the flow controller.dismiss(animated: true) { Task { do { // Get the flow using the placement ID from the action let flow = try await Adapty.getFlow(placementId: action.actionId) // Get the flow configuration let flowConfiguration = try await AdaptyUI.getFlowConfiguration( forFlow: flow ) // Create and present the flow controller let flowController = try AdaptyUI.flowController( with: flowConfiguration, delegate: self ) // Present the flow from the root view controller if let rootVC = UIApplication.shared.windows.first?.rootViewController { rootVC.present(flowController, animated: true) } } catch { // Handle any errors that occur during flow loading print("Failed to present flow: \(error)") } } } } ``` <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 hoàn tất tải, phương thức này sẽ được gọi: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, didFinishLoading action: OnboardingsDidFinishLoadingAction) { // Handle loading completion } ``` <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ể là một trong các loại sau: |Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải xong | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình hoàn tất. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn thành) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `secondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy [gán ID `final` cho màn hình cuối cùng](design-onboarding). | | `unknown` | Cho bất kỳ loại sự kiện không nhận dạng được. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của onboarding flow | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | Đây là ví dụ về cách bạn có thể sử dụng các sự kiện analytics để theo dõi: ```swift func onboardingController(_ controller: AdaptyOnboardingController, onAnalyticsEvent event: AdaptyOnboardingsAnalyticsEvent) { switch event { case .onboardingStarted(let meta): // Track onboarding start trackEvent("onboarding_started", meta: meta) case .screenPresented(let meta): // Track screen presentation trackEvent("screen_presented", meta: meta) case .screenCompleted(let meta, let elementId, let reply): // Track screen completion with user response trackEvent("screen_completed", meta: meta, elementId: elementId, reply: reply) case .onboardingCompleted(let meta): // Track successful onboarding completion trackEvent("onboarding_completed", meta: meta) case .unknown(let meta, let name): // Handle unknown events trackEvent(name, meta: meta) // Handle other cases as needed } } ``` <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: ios-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong iOS SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng iOS của bạn với Adapty SDK." --- :::tip **Bắt đầu từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboarding. Khác với onboarding chạy trong WebView, flow render trực tiếp trên thiết bị — mang lại hiệu ứng chuyển động mượt mà hơn, giao diện iOS nhất quán, tốc độ tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời câu hỏi trong quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Ví dụ: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .select(let params): // Handle single selection case .multiSelect(let params): // Handle multiple selections case .input(let params): // Handle text input case .datePicker(let params): // Handle date selection } } ``` Đối tượng `action` chứa: | Tham số | Mô tả | |----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `elementId` | Định danh duy nhất của phần tử nhập liệu. Bạn có thể dùng nó để liên kết câu hỏi với câu trả lời khi lưu dữ liệu. | | `params` | Đối tượng dữ liệu đầu vào của người dùng, chứa các thuộc tính type và value. | | `params.type` | Loại phần tử nhập liệu. Có thể là:<br/>• `"select"` - Chọn một trong các tùy chọn<br/>• `"multiSelect"` - Chọn nhiều trong các tùy chọn<br/>• `"input"` - Trường nhập văn bản<br/>• `"datePicker"` - Chọn ngày tháng | | `params.value` | Giá trị người dùng đã chọn hoặc nhập. Cấu trúc tùy theo loại:<br/>• `select`: Object gồm `id`, `value`, `label`<br/>• `multiSelect`: Mảng các object gồm `id`, `value`, `label`<br/>• `input`: Object gồm `type`, `value`<br/>• `datePicker`: Object gồm `day`, `month`, `year` | <Details> <summary>Ví dụ về dữ liệu đã lưu (có thể khác trong cài đặt 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 dữ liệu vào hồ sơ người dùng \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ lặp lại cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](setting-user-attributes) với dữ liệu đầu vào khi xử lý action. Ví dụ, bạn yêu cầu người dùng nhập tên vào trường văn bản có ID là `name` và muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code ứng dụng, nó có thể trông như thế này: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .input(let params): // Handle text input let builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field switch action.elementId { case "name": builder.with(firstName: params.value.value) case "email": builder.with(email: params.value.value) default: break } // Delegate methods are synchronous; kick off the async update in a Task. Task { do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } default: break } } ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Sử dụng quiz trong onboarding, bạn cũng có thể tùy chỉnh các paywall hiển thị cho người dùng sau khi họ hoàn thành onboarding. Ví dụ, bạn có thể hỏi người dùng về kinh nghiệm thể thao của họ và hiển thị các CTA cùng sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) trong trình tạo onboarding và gán các ID có ý nghĩa cho các tùy chọn. 2. Xử lý các câu trả lời quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](setting-user-attributes) cho người dùng. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Handle quiz responses and set custom attributes switch action.params { case .select(let params): // Handle quiz selection let builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes switch action.elementId { case "experience": // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) try? builder.with(customAttribute: params.value.value, forKey: "experience") default: break } // Delegate methods are synchronous; kick off the async update in a Task. Task { do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } default: break } } ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho từng phân khúc bạn đã tạo. 5. [Hiển thị paywall](ios-paywalls) cho placement trong code ứng dụng của bạn. Nếu onboarding có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho action của nút đó](ios-handling-onboarding-events#opening-a-paywall). --- # File: ios-sdk-call-order --- --- title: "Thứ tự gọi trong iOS SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution và lỗi #2002 không liên tục bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự." --- `Adapty.activate()` phải hoàn thành trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi nó hoàn tất, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` sẽ thất bại với lỗi [`#2002 notActivated`](ios-sdk-error-handling#network-errors). Nếu ứng dụng của bạn xác thực người dùng và bạn thu thập customer user ID sau khi khởi chạy, hãy gọi `Adapty.identify()` tại thời điểm đó. Đừng gọi các phương thức liên quan đến hành động người dùng cho đến khi `identify` hoàn tất. Các lệnh gọi thực hiện song song với nó sẽ thất bại với lỗi [`#3006 profileWasChanged`](ios-sdk-error-handling#general-errors), hoặc đổ vào hồ sơ người dùng ẩn danh được tạo lúc activation. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu lượt cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo cùng một quy tắc. Hãy khởi tạo chúng trước và chờ callback UID của chúng trước khi gọi `Adapty.activate`. Nếu không, MMP ID sẽ gắn vào một hồ sơ người dùng ẩn danh tồn tại ngắn và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Về chi tiết cụ thể với AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai yếu tố: khi nào bạn biết customer user ID và bạn có sử dụng SDK MMP hoặc analytics hay không. - **Bước 2 và 5**: Bắt buộc cho mọi ứng dụng. Activate SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ bắt buộc nếu bạn tích hợp SDK MMP hoặc analytics (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ bắt buộc nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn có customer user ID ngay khi ứng dụng khởi chạy, hãy truyền trực tiếp vào `activate()` (bước 2a). Lộ trình này không bao giờ tạo hồ sơ người dùng ẩn danh, do đó bước 4 là không cần thiết. | Bước | Lệnh gọi | Thời điểm | Lưu ý | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi chạy ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty.activate(with: config)` với `customerUserId` được đặt trên config | Khởi chạy ứng dụng, sau bước 1, nếu bạn có customer user ID | Được khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty.activate(with: config)` không có `customerUserId` | Khởi chạy ứng dụng, sau bước 1, nếu bạn không có customer user ID (hoặc không thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. | | 3 | `Adapty.setIntegrationIdentifier(...)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi hành động người dùng nào | Bắt buộc để MMP ID gắn vào đúng hồ sơ người dùng. | | 4 | `try await Adapty.identify("YOUR_USER_ID")` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên lộ trình 2b có xác thực | Luôn `await`. Các lệnh gọi đồng thời trong quá trình `identify` sẽ tạo ra lỗi `#3006 profileWasChanged`. | | 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này sẽ gây ra mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về sai đối tượng. ::: ## Cài đặt web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng qua web checkout (Stripe, Paddle, FunnelFox) và sau đó cài đặt ứng dụng native, lần `activate()` đầu tiên trên thiết bị sẽ tạo một hồ sơ người dùng ẩn danh mới. Hồ sơ này không được liên kết với hồ sơ web. Nếu bạn có thể xác định customer user ID trước khi khởi chạy ứng dụng (từ luồng xác thực hoặc install referrer), hãy truyền trực tiếp vào `activate()`. Nếu không, giao dịch mua hàng trên web sẽ không hiển thị trên thiết bị cho đến khi bạn gọi `identify("YOUR_USER_ID")` và sau đó `restorePurchases`. Về metadata cần gửi kèm theo mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: ios-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong iOS SDK" description: "Tải paywall Adapty đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho iOS." --- Một lần tải paywall đáng tin cậy trên iOS cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng, và xử lý dự phòng tốt khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định `Adapty.activate()` và `Adapty.identify()` đã hoàn thành. Xem [Thứ tự gọi trong iOS SDK](ios-sdk-call-order). ::: ## Quy tắc và những lỗi thường gặp \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement mà bạn sắp hiển thị. | Prefetch tất cả các placement đồng thời khi khởi chạy. | Bulk prefetch chặn main thread và gây màn hình đen trong thời gian burst. | | Gọi `getPaywall` sau khi attribution có cơ hội hoàn tất — ví dụ, 1–2 giây sau `activate` hoặc sau khi `onProfileUpdate` được kích hoạt. | Gọi `getPaywall` tại `App.init()`. | Attribution chưa được xử lý xong. Paywall sẽ được phân giải theo đối tượng mặc định và bỏ qua các phân khúc cũng như cá nhân hóa ASA mà không có cảnh báo. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` vô thời hạn. | Nếu không có timeout, người dùng có kết nối kém sẽ thấy màn hình trống cho đến khi mạng phục hồi — hoặc họ sẽ đóng ứng dụng. | Xem [Tải paywalls và sản phẩm](fetch-paywalls-and-products) để tham khảo tham số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn đúng placement. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối kém liên tục (khu vực nông thôn, phương tiện công cộng, các vùng bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy: .returnCacheDataElseLoad` cho mọi lần tải ngoại trừ lần đầu tiên. - Cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement trong Adapty dashboard. - Đặt `loadTimeout` từ 3–5 giây và chấp nhận phương án dự phòng khi timeout xảy ra. - Không chặn việc hiển thị paywall vào `getProfile()`. Gọi `getPaywall` độc lập để một profile chậm không làm chặn giao diện. --- # File: ios-show-aa-targeted-paywall --- --- title: "Hiển thị paywall được nhắm mục tiêu bởi AA khi khởi chạy lần đầu trong iOS SDK" description: "Chờ attribution từ Apple Ads trước khi yêu cầu paywall trên iOS bằng AdaptyProfile.appliedAttributionSources." --- Attribution từ Apple Ads (AA) được nhận không đồng bộ sau khi gọi `Adapty.activate()`. Nếu bạn gọi `getPaywall` sớm, attribution thường chưa được ghi nhận và Adapty sẽ xử lý placement theo đối tượng mặc định — bỏ qua các paywall được phân khúc theo AA. `AdaptyProfile.appliedAttributionSources` cho phép ứng dụng phát hiện khi nào attribution AA đã được áp dụng vào hồ sơ người dùng, để yêu cầu paywall có thể chờ cho đến khi phân khúc AA được xử lý chính xác. ## Trước khi bắt đầu \{#before-you-start\} Bạn cần: - Adapty iOS SDK **3.17.1** trở lên. - Apple Ads đã được cấu hình cho ứng dụng trong Adapty. Xem [Apple Ads](apple-search-ads). ## Cách hoạt động \{#how-it-works\} Sau khi gọi `Adapty.activate()`, SDK sẽ yêu cầu attribution Apple Ads từ Apple trong nền và chuyển kết quả đến backend của Adapty. Khi AA trở thành nguồn attribution đang hoạt động cho hồ sơ người dùng, SDK sẽ cung cấp một `AdaptyProfile` đã được cập nhật có mảng `appliedAttributionSources` chứa `.appleAds`. Mảng rỗng có thể có nghĩa là: - Attribution Apple Ads chưa được xử lý cho hồ sơ người dùng này. - Chưa có attribution nào đến. Ngay cả với mảng rỗng, `getPaywall` vẫn an toàn để gọi — Adapty sẽ xử lý yêu cầu theo đối tượng phù hợp với trạng thái hồ sơ người dùng hiện tại, thường là đối tượng mặc định. :::important Việc chờ chỉ áp dụng cho **lần khởi chạy đầu tiên**. Sau khi attribution Apple Ads đã được ghi nhận, nó được lưu trữ vĩnh viễn trên hồ sơ người dùng. Ở mọi lần khởi chạy tiếp theo, hồ sơ đã được cache sẵn chứa `.appleAds` trong `appliedAttributionSources`, `didLoadLatestProfile` sẽ kích hoạt với giá trị đó ngay lập tức, và `getPaywall` sẽ trả về paywall được phân khúc theo Apple Ads mà không cần chờ đợi. ::: ## Triển khai \{#implementation\} Ở lần khởi chạy đầu tiên, hãy theo dõi `.appleAds` trong hồ sơ người dùng và áp dụng một timeout cứng — nếu attribution Apple Ads không bao giờ đến, những người dùng đó vẫn cần được xem paywall. 1. **Kích hoạt SDK.** Xem [Cài đặt & cấu hình iOS SDK](sdk-installation-ios). 2. **Đăng ký nhận cập nhật hồ sơ người dùng** bằng cách tuân thủ `AdaptyDelegate` và triển khai `didLoadLatestProfile`. Nếu bạn chưa thiết lập delegate, xem [Lắng nghe cập nhật gói đăng ký](ios-check-subscription-status#listen-to-subscription-updates). 3. **Theo dõi `.appleAds` trong `appliedAttributionSources`.** Khi nó xuất hiện, hãy yêu cầu paywall — Adapty sẽ trả về biến thể được phân khúc theo AA: ```swift extension <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\} --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <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 (beta) bằng cách thay thế các paywall API bằng flow API, tương thích với cả Flow Builder và Paywall Builder." --- Adapty iOS SDK 4.0 (beta) giới thiệu flow và đổi tên các paywall API cho phù hợp. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi cấu hình trên Adapty Dashboard. ## Tham chiếu nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `Adapty.getPaywall(placementId:locale:)` | `Adapty.getFlow(placementId:)` | | `AdaptyUI.getPaywallConfiguration(forPaywall:)` | `AdaptyUI.getFlowConfiguration(forFlow:locale:)` | | `Adapty.getPaywallProducts(paywall:)` | `Adapty.getPaywallProducts(flow:)` | | `Adapty.logShowPaywall(_:)` | `Adapty.logShowFlow(_:)` | | `AdaptyPaywallController` | `AdaptyFlowController` | | `AdaptyPaywallControllerDelegate` | `AdaptyFlowControllerDelegate` | | `AdaptyUI.paywallController(with:delegate:)` | `AdaptyUI.flowController(with:delegate:)` | | `.paywall()` (SwiftUI modifier) | `.flow()` | | `AdaptyPaywallView` | `AdaptyFlowView` | | `didFailRenderingWith:` / `didFailRendering:` | `didReceiveError:` | | `Adapty.updateAttribution(_:source:)` (`source: String`) | `Adapty.updateAttribution(_:source:)` (`source: AdaptyAttributionSource`) | | `Adapty.setIntegrationIdentifier(key:value:)` | `Adapty.setIntegrationIdentifier(_:)` (`AdaptyIntegrationIdentifier`) | ## Phiên bản iOS tối thiểu \{#minimum-ios-version\} Adapty iOS SDK 4.0 nâng deployment target tối thiểu từ iOS 13.0 lên **iOS 15.0**. Hãy đặt iOS Deployment Target của dự án lên 15.0 hoặc cao hơn trước khi nâng cấp. ## Cài đặt: CocoaPods không còn được hỗ trợ \{#installation-cocoapods-no-longer-supported\} Adapty iOS SDK 4.0 bỏ hỗ trợ CocoaPods. Hãy cài đặt SDK bằng [Swift Package Manager](sdk-installation-ios#install-adapty-sdk). Nếu dự án của bạn vẫn đang dùng CocoaPods, hãy xóa các pod `Adapty` và `AdaptyUI` khỏi `Podfile`, chạy `pod install` để dọn sạch, rồi thêm package trong Xcode qua **File → Add Package Dependency** với URL `https://github.com/adaptyteam/AdaptySDK-iOS.git`. ## Các API đã bị xóa \{#removed-apis\} - **`Adapty.getPaywallProductsWithoutDeterminingOffer(paywall:)`** — đã xóa. Tất cả sản phẩm giờ đây đã bao gồm thông tin ưu đãi, nên bước kiểm tra tính đủ điều kiện riêng biệt không còn cần thiết nữa. - **`AdaptyPaywallProductWithoutDeterminingOffer`** — đã xóa. Các callback trước đây nhận kiểu này (chẳng hạn `didSelectProduct`) giờ nhận `AdaptyPaywallProduct`. ## Tạm thời xóa tính năng mua in-app purchase được quảng bá trên App Store \{#app-store-promoted-in-app-purchases-temporarily-removed\} Trong quá trình migrate sang StoreKit 2, Adapty iOS SDK 4.0 xóa hỗ trợ cho in-app purchase được quảng bá trên App Store. Phương thức delegate `shouldAddStorePayment(for:)` và kiểu `AdaptyDeferredProduct` mà nó nhận đều không khả dụng trong phiên bản 4.0. :::warning Việc xóa này chỉ là tạm thời — hỗ trợ in-app purchase được quảng bá sẽ quay trở lại trong một bản phát hành 4.x tiếp theo. Nếu ứng dụng của bạn phụ thuộc vào in-app purchase được quảng bá, hãy tiếp tục dùng iOS SDK 3.x cho đến khi tính năng này được khôi phục. ::: ## Lấy paywall \{#fetching-paywalls\} ### getPaywall + getPaywallConfiguration → getFlow + getFlowConfiguration \{#getpaywall--getpaywallconfiguration--getflow--getflowconfiguration\} Các kiểu trả về thay đổi từ `AdaptyPaywall` / `AdaptyUI.PaywallConfiguration` sang `AdaptyFlow` / `AdaptyUI.FlowConfiguration`. Tham số `locale` được chuyển ra khỏi lệnh gọi fetch và sang `getFlowConfiguration`: ```diff showLineNumbers - let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") - let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall) + let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") + let flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow, locale: "en") ``` ### getPaywallProducts(paywall:) → getPaywallProducts(flow:) \{#getpaywallproductspaywall--getpaywallproductsflow\} `getPaywallProducts` giờ nhận `AdaptyFlow` được trả về bởi `Adapty.getFlow`: ```diff showLineNumbers - let products = try await Adapty.getPaywallProducts(paywall: paywall) + let products = try await Adapty.getPaywallProducts(flow: flow) ``` ## Theo dõi lượt xem paywall \{#tracking-paywall-views\} ### logShowPaywall(_:) → logShowFlow(_:) \{#logshowpaywall_--logshowflow_\} `logShowPaywall` được đổi tên thành `logShowFlow` và giờ nhận `AdaptyFlow` thay vì `AdaptyPaywall`. Sự kiện vẫn được ghi lại theo cùng một biến thể, vì vậy các chỉ số funnel và A/B test hiện có vẫn hoạt động bình thường mà không cần thay đổi trên dashboard. ```diff showLineNumbers - try await Adapty.logShowPaywall(paywall) + try await Adapty.logShowFlow(flow) ``` Như trong v3, bạn không cần gọi phương thức này khi hiển thị flow hoặc paywall được render bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi các lượt xem đó. ## UIKit \{#uikit\} ### AdaptyPaywallController → AdaptyFlowController \{#adaptypaywall controller--adaptyflo wcontroller\} Đổi tên kiểu controller và factory method: ```diff showLineNumbers - let controller = try AdaptyUI.paywallController( - with: paywallConfiguration, - delegate: self - ) + let controller = try AdaptyUI.flowController( + with: flowConfiguration, + delegate: self + ) ``` ### AdaptyPaywallControllerDelegate → AdaptyFlowControllerDelegate \{#adaptypaywall controllerdelegate--adaptyflo wcontrollerdelegate\} Đổi tên protocol và cập nhật mọi chữ ký phương thức. Lưu ý rằng `didSelectProduct` giờ nhận `AdaptyPaywallProduct` thay vì `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xóa. ```diff showLineNumbers - class YourClass: AdaptyPaywallControllerDelegate { + class YourClass: AdaptyFlowControllerDelegate { - func paywallControllerDidAppear(_ controller: AdaptyPaywallController) { } + func flowControllerDidAppear(_ controller: AdaptyFlowController) { } - func paywallControllerDidDisappear(_ controller: AdaptyPaywallController) { } + func flowControllerDidDisappear(_ controller: AdaptyFlowController) { } - func paywallController(_ controller: AdaptyPaywallController, - didPerform action: AdaptyUI.Action) { } + func flowController(_ controller: AdaptyFlowController, + didPerform action: AdaptyUI.Action) { } - func paywallController(_ controller: AdaptyPaywallController, - didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer) { } + func flowController(_ controller: AdaptyFlowController, + didSelectProduct product: AdaptyPaywallProduct) { } - func paywallController(_ controller: AdaptyPaywallController, - didStartPurchase product: AdaptyPaywallProduct) { } + func flowController(_ controller: AdaptyFlowController, + didStartPurchase product: AdaptyPaywallProduct) { } - func paywallController(_ controller: AdaptyPaywallController, - didFinishPurchase product: AdaptyPaywallProduct, - purchaseResult: AdaptyPurchaseResult) { } + func flowController(_ controller: AdaptyFlowController, + didFinishPurchase product: AdaptyPaywallProduct, + purchaseResult: AdaptyPurchaseResult) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailPurchase product: AdaptyPaywallProduct, - error: AdaptyError) { } + func flowController(_ controller: AdaptyFlowController, + didFailPurchase product: AdaptyPaywallProduct, + error: AdaptyError) { } - func paywallControllerDidStartRestore(_ controller: AdaptyPaywallController) { } + func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { } - func paywallController(_ controller: AdaptyPaywallController, - didFinishRestoreWith profile: AdaptyProfile) { } + func flowController(_ controller: AdaptyFlowController, + didFinishRestoreWith profile: AdaptyProfile) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailRestoreWith error: AdaptyError) { } + func flowController(_ controller: AdaptyFlowController, + didFailRestoreWith error: AdaptyError) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailRenderingWith error: AdaptyUIError) { } + func flowController(_ controller: AdaptyFlowController, + didReceiveError error: AdaptyUIError) { } - func paywallController(_ controller: AdaptyPaywallController, - didFailLoadingProductsWith error: AdaptyError) -> Bool { } + func flowController(_ controller: AdaptyFlowController, + didFailLoadingProductsWith error: AdaptyError) -> Bool { } - func paywallController(_ controller: AdaptyPaywallController, - didPartiallyLoadProducts failedIds: [String]) { } + func flowController(_ controller: AdaptyFlowController, + didPartiallyLoadProducts failedIds: [String]) { } - func paywallController(_ controller: AdaptyPaywallController, - didFinishWebPaymentNavigation product: AdaptyPaywallProduct?, - error: AdaptyError?) { } + func flowController(_ controller: AdaptyFlowController, + didFinishWebPaymentNavigation product: AdaptyPaywallProduct?, + error: AdaptyError?) { } } ``` ## SwiftUI \{#swiftui\} ### Modifier .paywall() → .flow() \{#paywall-modifier--flow\} Đổi tên modifier và cập nhật tên tham số cấu hình: ```diff showLineNumbers @State var flowPresented = false // đổi tên tùy ý — tên biến là do bạn chọn var body: some View { Text("Hello, AdaptyUI!") - .paywall( + .flow( isPresented: $flowPresented, - paywallConfiguration: paywallConfiguration, + flowConfiguration: flowConfiguration, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, - didFailRendering: { error in flowPresented = false } + didReceiveError: { error in flowPresented = false } ) } ``` Callback được đổi tên này kích hoạt cho các lỗi render giống như `didFailRendering` trước đây, cộng thêm các lỗi runtime mới từ flow script (JavaScript exception trên `AdaptyUIError` code `4105` — `.jsException`). Phần thân handler hiện có không cần thay đổi code — chỉ cần đổi tên tham số. ### AdaptyPaywallView → AdaptyFlowView \{#adaptypaywall view--adaptyflo wview\} Đổi tên view, cập nhật tham số cấu hình, và cập nhật bất kỳ closure `didSelectProduct` nào — giờ nó nhận `AdaptyPaywallProduct` thay vì `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xóa: ```diff showLineNumbers - AdaptyPaywallView( - paywallConfiguration: paywallConfiguration, - didSelectProduct: { product: AdaptyPaywallProductWithoutDeterminingOffer in /* handle */ }, + AdaptyFlowView( + flowConfiguration: flowConfiguration, + didSelectProduct: { product: AdaptyPaywallProduct in /* handle */ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, - didFailRendering: { error in /* handle the error */ } + didReceiveError: { error in /* handle the error */ } ) ``` ## Asset tùy chỉnh của AdaptyUI \{#adaptyui-custom-assets\} ### AdaptyUICustomVideoAsset \{#adaptyuicustomvideoasset\} Có hai thay đổi ảnh hưởng đến mọi call site hiện có: - `.player` giờ nhận `AVPlayer` thay vì `AVQueuePlayer`. - Mỗi case được thêm tham số cuối `resolution: CGSize?`. Truyền `nil` để giữ nguyên hành vi hiện tại, hoặc truyền kích thước pixel thực tế để player có thể dành trước không gian layout (tỷ lệ khung hình = `width / height`) trước khi video tải xong. ```diff showLineNumbers - case file(url: URL, preview: AdaptyUICustomImageAsset?) - case remote(url: URL, preview: AdaptyUICustomImageAsset?) - case player(item: AVPlayerItem, player: AVQueuePlayer, preview: AdaptyUICustomImageAsset?) + case file(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?) + case remote(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?) + case player(item: AVPlayerItem, player: AVPlayer, preview: AdaptyUICustomImageAsset?, resolution: CGSize?) ``` ## Attribution và integration identifier \{#attribution-and-integration-identifiers\} ### updateAttribution(_:source:) \{#updateattribution_source\} Tham số `source` thay đổi từ `String` sang kiểu `AdaptyAttributionSource` mới, và `AdaptyProfile.AttributionSource` trước đây được lồng bên trong giờ được đổi tên thành `AdaptyAttributionSource` ở cấp độ top-level. Sử dụng một trong các source được định nghĩa sẵn, hoặc truyền string literal cho bất kỳ source nào khác — `AdaptyAttributionSource` tuân thủ `ExpressibleByStringLiteral`, nên các lệnh gọi dùng string literal hiện có vẫn biên dịch được. ```diff showLineNumbers - try await Adapty.updateAttribution(attribution, source: "adjust") + try await Adapty.updateAttribution(attribution, source: .adjust) ``` Các source được định nghĩa sẵn: `.appleAds`, `.adjust`, `.appsflyer`, `.branch`, `.tenjin`. Nếu bạn lưu source trong biến `String`, hãy wrap nó lại: `AdaptyAttributionSource(rawValue: yourSource)`. ### setIntegrationIdentifier(_:) \{#setintegrationidentifier_\} `setIntegrationIdentifier(key:value:)` được thay thế bằng một phương thức variadic nhận một hoặc nhiều giá trị `AdaptyIntegrationIdentifier`. Sử dụng các factory method được định nghĩa sẵn thay vì raw string key: ```diff showLineNumbers - try await Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid) + try await Adapty.setIntegrationIdentifier(.appsflyerId(uid)) ``` Bạn có thể đặt nhiều identifier trong một lần gọi: ```swift showLineNumbers try await Adapty.setIntegrationIdentifier( .appsflyerId(uid), .adjustDeviceId(adid) ) ``` Thay thế mỗi chuỗi key cũ bằng factory method tương ứng: | Key v3 | Factory v4 | |---|---| | `"adjust_device_id"` | `.adjustDeviceId(_:)` | | `"airbridge_device_id"` | `.airbridgeDeviceId(_:)` | | `"amplitude_user_id"` | `.amplitudeUserId(_:)` | | `"amplitude_device_id"` | `.amplitudeDeviceId(_:)` | | `"appmetrica_device_id"` | `.appmetricaDeviceId(_:)` | | `"appmetrica_profile_id"` | `.appmetricaProfileId(_:)` | | `"appsflyer_id"` | `.appsflyerId(_:)` | | `"branch_id"` | `.branchId(_:)` | | `"facebook_anonymous_id"` | `.facebookAnonymousId(_:)` | | `"firebase_app_instance_id"` | `.firebaseAppInstanceId(_:)` | | `"mixpanel_user_id"` | `.mixpanelUserId(_:)` | | `"one_signal_subscription_id"` | `.oneSignalSubscriptionId(_:)` | | `"one_signal_player_id"` | `.oneSignalPlayerId(_:)` | | `"posthog_distinct_user_id"` | `.posthogDistinctUserId(_:)` | | `"pushwoosh_hwid"` | `.pushwooshHWID(_:)` | | `"tenjin_analytics_installation_id"` | `.tenjinAnalyticsInstallationId(_:)` | --- # File: migration-to-ios-315 --- --- title: "Migrate Adapty iOS SDK to v3.15" description: "Migrate to Adapty iOS SDK v3.15 for better performance and new monetization features." --- Nếu bạn đang dùng [Paywall Builder](adapty-paywall-builder) trong [Observer mode](observer-vs-full-mode), bắt đầu từ iOS SDK 3.15, bạn cần triển khai một phương thức mới là `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)`. Phương thức này cung cấp khả năng kiểm soát chi tiết hơn đối với logic khôi phục, cho phép bạn xử lý việc khôi phục giao dịch mua trong flow tùy chỉnh của mình. Để biết chi tiết triển khai đầy đủ, tham khảo [Hiển thị paywall Paywall Builder trong Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode). ```diff showLineNumbers func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } + func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, + onFinishRestore: @escaping () -> Void) { + // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore + } ``` --- # File: migration-to-ios-sdk-34 --- --- title: "Migrate Adapty iOS SDK to v3.4" description: "Migrate to Adapty iOS SDK v3.4 for better performance and new monetization features." --- Adapty SDK 3.4.0 là một bản phát hành lớn, giới thiệu các cải tiến yêu cầu bạn thực hiện các bước migration. ## Cập nhật khởi tạo Adapty SDK \{#update-adapty-sdk-activation\} <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-06-24T14:36:38.743Z_ _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-06-24T14:36:38.746Z Total files: 50 --- # File: kmp-sdk-overview --- --- title: "Kotlin Multiplatform SDK overview" description: "Learn about Adapty Kotlin Multiplatform SDK and its key features." --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-KMP.svg?style=flat&logo=kotlin)](https://github.com/adaptyteam/AdaptySDK-KMP/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the Adapty Kotlin Multiplatform SDK to take the headache out of in-app purchases so you can focus on what you do best – building amazing apps. Here's what we handle for you: - Handle purchases, receipt validation, and subscription management out of the box - Create and test paywalls without app updates - Get detailed purchase analytics with zero setup - cohorts, LTV, churn, and funnel analysis included - Keep the user subscription status always up to date across app sessions and devices - Integrate your app with marketing attribution and analytics services using just one line of code :::note Before diving into the code, you'll need to integrate Adapty with Google Play Console and set up products in the dashboard. Check out our [quickstart guide](quickstart) to get everything configured first. ::: ## 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. Here's what we'll cover in the integration guide: 1. [Install & configure SDK](sdk-installation-kotlin-multiplatform): Add the SDK as a dependency to your project and activate it in the code. 2. [Enable purchases through paywalls](kmp-quickstart-paywalls): Set up the purchase flow so users can buy products. 3. [Check the subscription status](kmp-check-subscription-status): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](kmp-quickstart-identify): Associate users with their Adapty profiles to ensure their data is stored consistently across devices. ### See it in action Want to see how it all comes together? We've got you covered: - **Sample app**: Check out our [complete example](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example) that demonstrates the full setup - **Video tutorial**: Follow along with our step-by-step implementation video below <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> ## Main concepts Before diving into the code, let's get familiar with the key concepts that make Adapty work. The beauty of Adapty's approach is that only placements are hardcoded in your app. Everything else – products, paywall designs, pricing, and offers – can be managed flexibly from the Adapty dashboard without app updates: 1. [**Product**](product) - Anything available for purchase in your app – subscription, consumable product, or lifetime access. 2. [**Paywall**](paywalls) - The only way to retrieve products from Adapty and use it to its full power. We've designed it this way to make it easier to track how different product combinations affect your monetization metrics. A paywall in Adapty serves as both a specific set of your products and the visual configuration that accompanies them. 3. [**Placement**](placements) - A strategic point in your user journey where you want to show a paywall. Think of placements as the "where" and "when" of your monetization strategy. Common placements include: - `main` - Your primary paywall location - `onboarding` - Shown during the user onboarding flow - `settings` - Accessible from your app's settings Start with the basics like `main` or `onboarding` for your first integration, then [think about where else in your app users might be ready to purchase](choose-meaningful-placements). 4. [**Profile**](profiles-crm) - When users purchase a product, their profile is assigned an **access level** which you use to define access to paid features. --- # 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 ứng dụng Kotlin Multiplatform." --- Adapty SDK bao gồm hai module chính để tích hợp liền mạch vào ứng dụng của bạn: - **Core Adapty**: SDK cốt lõi bắt buộc để Adapty hoạt độ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 dùng [Adapty Paywall Builder](adapty-paywall-builder) với lớp render Compose Multiplatform (`view.present()`). Nếu dự án của bạn không dùng Compose Multiplatform, bạn có thể sử dụng [`createNativePaywallView`](kmp-present-paywalls#without-compose-multiplatform) và [`createNativeOnboardingView`](kmp-present-onboardings#without-compose-multiplatform) từ module core thay thế. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example) của chúng tôi, minh họa toàn bộ quá trình cài đặt bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: Bạn cũng có thể xem video hướng dẫn triển khai đầy đủ: <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. ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## Cài đặt Adapty SDK qua Gradle \{#install-adapty-sdk-via-gradle\} Việc 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: - Gradle thông thường: Thêm dependency vào file `build.gradle` **cấp module** - Nếu dự án dùng file `.gradle.kts`, thêm dependency vào file `build.gradle.kts` **cấp module** - Nếu bạn dùng version catalog, thêm dependency vào file `libs.versions.toml` rồi tham chiếu trong `build.gradle.kts` <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="Versions library" 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 Gradle scripts. <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() } } ``` Ngược lại, 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\} ### Cài đặt cơ bản \{#basic-setup\} Thêm đoạn khởi tạo sớm nhất có thể — 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**: 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 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. ::: Tiếp theo, 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](kmp-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](kmp-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn muốn kích hoạt module **AdaptyUI** để dùng [Adapty Paywall Builder](kmp-present-paywalls), hãy đặt `.withActivateUI(true)` trong cấu hình. :::info 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. ::: ```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\} ### Logging \{#logging\} #### Thiết lập hệ thống logging \{#set-up-the-logging-system\} Adapty ghi log các lỗi và thông tin quan trọng để giúp bạn hiểu những gì đang xảy ra. Các cấp độ log có sẵn như sau: | Cấp độ | Mô tả | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------------- | | `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 khác nhau. 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, API query, 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 cấp độ 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 thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt việc thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Dùng tham số này để tăng cường quyền riêng tư người dùng, tuân thủ các quy định bảo vệ dữ liệu khu vực (như GDPR hoặc CCPA), hoặc giảm thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không bắt buộc cho ứng dụng của bạn. ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build() ``` #### Tắt 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 advertising identifier. Giá trị mặc định là `false`. Dùng tham số này để tuân thủ chính sách App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc nếu ứng dụng của bạn không cần attribution quảng cáo hay analytics dựa trên advertising ID. ```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 media cache 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 sử dụng băng thông mạng. Bạn có thể tùy chỉnh cài đặt cache bằng cách cung cấp cấu hình riêng. 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 backup \{#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ừ backup iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng được cache, chi tiết sản phẩm và paywall. SDK sau đó khởi tạo với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ xóa cache SDK cục bộ. Lịch sử giao dịch với Apple và dữ liệu người dùng trên 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 Android backup (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, áp dụng các thay đổi này trong Android application module (module tạo ra APK/AAB), ví dụ `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 đúng, chỉ sử dụng chế độ launch `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" /> ``` --- # File: kmp-quickstart-paywalls --- --- title: "Kích hoạt mua hàng bằng cách sử dụng paywall trong Kotlin Multiplatform SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký trong ứng dụng." --- Để kích hoạt in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**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 các ư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) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (ví dụ như `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trên dashboard, sau đó yêu cầu chúng bằng placement ID trong code. Điều này giúp bạn dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho các đối tượng khác nhau. Adapty cung cấp ba cách để kích hoạt mua hàng trong ứng dụng của bạn. Hãy chọn một trong số chúng tùy theo yêu cầu ứ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 hàng trong no-code builder](quickstart-paywalls). Adapty tự động hiển thị 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ạo thủ công | 🟡 Trung bình | Bạn tự triển khai UI 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 việc cung cấp sản phẩm. Xem [hướng dẫn](kmp-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 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 paywall được tạo trong Adapty paywall builder.** Nếu bạn không muốn sử dụng paywall builder, hãy xem [hướng dẫn xử lý mua hàng trong paywall tạo thủ công](kmp-making-purchases). ::: Để hiển thị paywall được tạo trong Adapty paywall builder, trong code ứng dụng của bạn, bạn chỉ cần: 1. **Lấy paywall**: Lấy paywall từ Adapty. 2. **Hiển thị paywall và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị container paywall bạn đã lấy được trong ứng dụng. 3. **Xử lý các hành động của nút**: Liên kết các tương tác của người dùng với paywall với phản hồi của ứng dụng. Ví dụ: mở liên kết hoặc đóng paywall khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. Kết nối ứng dụng của bạn với [App Store](initial_ios) và/hoặc [Google Play](initial-android) trong Adapty Dashboard. 2. [Tạo sản phẩm](create-product) trong Adapty. 3. [Tạo paywall và thêm sản phẩm vào đó](create-paywall). 4. [Tạo placement và thêm paywall vào đó](create-placement). 5. [Cài đặt và kích hoạt Adapty 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 nhanh](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy paywall \{#1-get-the-paywall\} Các paywall của bạn được liên kết với các placement được cấu hình trên dashboard. Placement cho phép bạn chạy các paywall khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy paywall được tạo trong Adapty paywall builder, bạn cần: 1. Lấy đối tượng `paywall` theo [placement](placements) ID bằng phương thức `getPaywall` và kiểm tra xem đó có phải là paywall được tạo trong builder hay không. 2. Lấy cấu hình view của paywall bằng phương thức `createPaywallView`. Cấu hình view chứa các phần tử UI và kiểu dáng cần thiết để hiển thị paywall. :::important Để lấy cấu hình view, bạn phải bật nút **Show on device** trong Paywall Builder. Nếu không, bạn sẽ nhận được cấu hình view trống và paywall sẽ không được hiển thị. ::: ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID") .onSuccess { paywall -> if (!paywall.hasViewConfiguration) { return@onSuccess } val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() } .onError { error -> // handle the error } ``` ## 2. Hiển thị paywall \{#2-display-the-paywall\} Bây giờ khi bạn đã có cấu hình paywall, chỉ cần thêm vài dòng code là có thể hiển thị paywall của bạn. Để hiển thị paywall trực quan trên màn hình thiết bị, trước tiên bạn phải cấu hình nó. Để làm điều này, hãy gọi phương thức `AdaptyUI.createPaywallView()`: ```kotlin showLineNumbers val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() ``` 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ị. :::tip Để biết thêm chi tiết về cách hiển thị paywall, hãy xem [hướng dẫn](kmp-present-paywalls) của chúng tôi. ::: ## 3. Xử lý các hành động của nút \{#3-handle-button-actions\} Khi người dùng nhấn các nút trong paywall, Kotlin Multiplatform SDK tự động xử lý mua hàng, khôi phục, đóng paywall và mở liên kết. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được định nghĩa trước và yêu cầu xử lý hành động trong code của bạn. Hoặc, bạn có thể muốn ghi đè hành vi mặc định của chúng. Ví dụ: đây là hành vi mặc định cho 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. :::tip Đọc 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. ::: ```kotlin showLineNumbers AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { AdaptyUIAction.CloseAction, AdaptyUIAction.AndroidSystemBackAction -> 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ất cả các bước đó có thể được tích hợp cùng nhau trong ứng dụng của bạn. ```kotlin showLineNumbers // Set up the observer for handling paywall actions AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CloseAction -> view.dismiss() } } }) // Get and display the paywall Adapty.getPaywall("YOUR_PLACEMENT_ID") .onSuccess { paywall -> if (!paywall.hasViewConfiguration) { // Use custom logic return@onSuccess } val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() } .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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-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: "Paywalls in Kotlin Multiplatform SDK" description: "Learn how to work with paywalls in your Kotlin Multiplatform app with Adapty SDK." --- ## Display paywalls ### Adapty Paywall Builder <CustomDocCardList ids={['kmp-get-pb-paywalls', 'kmp-present-paywalls', 'kmp-handling-events', 'kmp-handle-paywall-actions']} /> :::tip To get started with the Adapty Paywall Builder paywalls quickly, see our [quickstart guide](kmp-quickstart-paywalls). ::: ### Implement paywalls manually <CustomDocCardList ids={['kmp-quickstart-manual', 'fetch-paywalls-and-products-kmp', 'present-remote-config-paywalls-kmp', 'kmp-making-purchases']} /> For more guides on implementing paywalls and handling purchases manually, see the [category](kmp-implement-paywalls-manually). ## Useful features <CustomDocCardList ids={['kmp-use-fallback-paywalls', 'kmp-web-paywalls']} /> --- # File: kmp-get-pb-paywalls --- --- title: "Lấy paywall Paywall Builder và cấu hình của chúng trong Kotlin Multiplatform 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 Kotlin Multiplatform 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 quá trình này là lấy paywall gắn với placement và cấu hình view của nó như mô tả bên dưới. 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 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 di động (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 đó](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) trong ứng dụng di động của bạn. </details> ## Lấy paywall được thiết kế với Paywall Builder \{#fetch-paywall-designed-with-paywall-builder\} Nếu bạn đã [thiết kế paywall bằng Paywall Builder](adapty-paywall-builder), bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy chứa cả nội dung cần hiển thị lẫn cách hiển thị nó. Dù vậy, 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 di độ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](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, để hì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`: ```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 dịch paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai thẻ con được 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à 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><p>Xem [Các bản dịch 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: `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 đã được lưu trong cache 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 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ẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ có kém đế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à 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 cài đặt lại ứ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 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>Với Kotlin Multiplatform: Bạn có thể tạo `TimeInterval` bằng các hàm mở rộng (như `5.seconds`, trong đó `.seconds` 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/) với danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình view của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bật nút **Show on device** trong paywall builder. Nếu tùy chọn này 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ó bao gồm `ViewConfiguration` hay không — điều này cho biết paywall được tạo bằng Paywall Builder. Thông tin này sẽ hướng dẫn bạn cách hiển thị paywall. Nếu `ViewConfiguration` có mặt, hãy xử lý nó như một paywall Paywall Builder; nếu không, [xử lý nó như một remote config paywall](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 } ``` | 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** | 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 đã 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 suất. Khi được bật, sản phẩm được tải trước, giảm thời gian cần thiết để hiển thị paywall. | | **productPurchaseParams** | tùy chọn | Một map từ [`AdaptyProductIdentifier`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-product-identifier/) sang [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). Sử dụng tham số này để 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 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 dịch cho Paywall Builder](add-paywall-locale-in-adapty-paywall-builder). ::: Sau khi tải xong, [trình bày 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 lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và 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 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 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ể: - **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 paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với paywall không được render. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đ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 để hưởng lợi từ việc lấy paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Ngược lại, hãy sử dụng `getPaywall` được 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 được kỳ vọng là một 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 ngang (**-**). 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><p></p><p>Xem [Các bản dịch 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: `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 đã được lưu trong cache 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 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ẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ có kém đế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à 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 cài đặt lại ứng dụng hoặc xóa thủ công.</p> | ## Tùy chỉnh tài nguyên \{#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 sẵn: `hero_image` và `hero_video`. Trong một bundle custom asset, bạn nhắm mục tiêu 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. :::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. ::: Đây là ví dụ về cách bạn có thể cung cấp custom assets thông qua một map: :::info Kotlin Multiplatform SDK chỉ hỗ trợ tài nguyên cục bộ. Đối với nội dung từ xa, bạn nên tải xuống và lưu vào cache cục bộ trước khi sử dụng chúng trong custom assets. ::: ```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. ::: --- # File: kmp-present-paywalls --- --- title: "Kotlin Multiplatform - Hiển thị paywall mới được thiết kế bằng Paywall Builder" description: "Tìm hiểu cách hiển thị paywall trên Kotlin Multiplatform để tối ưu hóa doanh thu." --- 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 đó đã chứa đầy đủ thông tin về nội dung cần hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này chỉ dành cho **paywall mới được thiết kế bằng Paywall Builder**. 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). ::: Adapty Kotlin Multiplatform SDK cung cấp hai cách để hiển thị paywall: - **Với Compose Multiplatform** - **Không dùng Compose Multiplatform** ## Với Compose Multiplatform \{#with-compose-multiplatform\} Để 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`](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` 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ị dialog \{#show-dialog\} Sử dụng phương thức này thay vì các alert dialog gốc khi paywall view đang được hiển thị trên Android. Trên Android, các alert thông thường xuất hiện phía sau paywall view, khiến người dùng không thể nhìn thấy. Phương thức này đảm bảo dialog được hiển thị đúng cách phía trên paywall trên tất cả các 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 trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận 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 `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 paywall mà không cần Compose Multiplatform, hãy gọi `createNativePaywallView`. Phương thức 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 lớp base mở 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 sẽ 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() ``` ## Custom tags \{#custom-tags\} Custom tags giúp bạn tránh phải tạo các paywall riêng biệt cho từng tình huống khác nhau. Hãy hình dung một paywall duy nhất có thể thích nghi linh hoạt dựa trên dữ liệu người dùng. Ví dụ, thay vì lời chào chung chung "Xin chào!", bạn có thể chào người dùng cá nhân hóa như "Xin chào, John!" hay "Xin chào, Ann!" Dưới đây là một số cách bạn có thể sử dụng custom tags: - 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 hiện tại để thúc đẩy doanh số (ví dụ: "Thứ Năm vui vẻ"). - Thêm thông tin cá nhân hóa về sản phẩm bạn đang bán (như tên chương trình thể dục hoặc số điện thoại trong ứng dụng VoIP). Custom tags giúp bạn tạo một 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ũ 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 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 tags 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> ## Custom timers \{#custom-timers\} Bộ đếm thời gian paywall là một công cụ tuyệt vời để quảng bá các ưu đãi đặc biệt và theo mùa có thời hạn. Tuy nhiên, cần lưu ý rằng bộ đếm thời gian 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 chiến dịch. Đây chỉ đơn giản 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 đạt về không, không có gì xảy ra—nó chỉ dừng ở 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, chẳng hạn: "Ưu đãi kết thúc sau: 10:00 giây." Để sử dụng custom timers 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> --- # File: kmp-handle-paywall-actions --- --- title: "Xử lý hành động nút trong Kotlin Multiplatform SDK" description: "Xử lý các hành động nút trên paywall trong Kotlin Multiplatform bằng Adapty để tối ưu hóa doanh thu ứng dụng." --- :::warning **Chỉ có cá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, như đóng paywall hoặc mở liên kết, đều cần được triển khai phản hồi trong mã ứng dụng. ::: Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm 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 mã 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 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 mã của bạn. ## Thiết lập AdaptyUIPaywallsEventsObserver \{#set-up-the-adaptyuipaywallseventsObserver\} Để xử lý các hành động paywall, bạn cần triển khai interface `AdaptyUIPaywallsEventsObserver` và thiết lập nó bằng `AdaptyUI.setPaywallsEventsObserver()`. 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 khởi tạo ứng dụng. ```kotlin // In your app initialization AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` ## Đóng paywall \{#close-paywalls\} Để thêm một nút sẽ đóng paywall của bạn: 1. Trong paywall builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong mã ứng dụng của bạn, triển khai trình xử lý cho hành động `close` để đóng paywall. :::info Trong Kotlin Multiplatform SDK, `CloseAction` và `AndroidSystemBackAction` sẽ kích hoạt đóng paywall theo mặc định. Tuy nhiên, bạn có thể ghi đè hành vi này trong mã của mình nếu cần. Ví dụ, đóng một paywall có thể kích hoạt 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ó hiệu lực — view được nhúng vào layout của bạn, không được hiển thị thông 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 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 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 mã ứ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 Kotlin Multiplatform SDK, `OpenUrlAction` cung cấp URL cần được 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 một 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 mã ứng dụng của bạn, triển khai trình xử lý cho hành động tùy chỉnh để xác định người dùng của bạn. ```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ý 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 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ý 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: ```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()) ``` --- # File: kmp-handling-events --- --- title: "Kotlin Multiplatform - Xử lý sự kiện paywall" description: "Xử lý các sự kiện gói đăng ký trong Kotlin Multiplatform hiệu quả với các công cụ theo dõi sự kiện của Adapty." --- 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 thao tác nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua trên paywall. Tìm hiểu cách phản hồi các sự kiện này bên dưới. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới**. ::: Để kiểm soát hoặc theo dõi các quy trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai các phương thức của interface `AdaptyUIPaywallsEventsObserver`. Một số phương thức có triển khai mặc định tự động xử lý các tình huống phổ biến. :::note Đây là nơi bạn thêm logic tùy chỉnh để phản hồi các sự kiện paywall. Bạn có thể dùng `view.dismiss()` để đóng paywall, hoặc triển khai 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\} ### Paywall hiển thị và ẩn đi \{#paywall-appearance-and-disappearance\} Khi một paywall xuất hiện hoặc biến mất, các phương thức này 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à một 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) 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ụ sự kiện (Nhấn để 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 \{#started-purchase\} Nếu người dùng khởi tạo quá trình mua, phương thức này sẽ được gọi: ```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 thành công, bị 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. Theo mặc định, nó sẽ tự động đóng paywall trừ khi người dùng hủy giao dịch: ```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 giao dịch mua thành công. ### 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 (giới hạn 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 việc người dùng hủy sẽ kích hoạt `paywallViewDidFinishPurchase` với kết quả đã hủy, và các khoản thanh toán đang chờ xử lý 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 tạo 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 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 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 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 khởi tạo quá trình mua bằng [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ụ 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ừ 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 xảy ra lỗi trong quá trình hiển thị giao diện, lỗi đó sẽ được báo cáo bởi 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 điều kiện bình thường, các lỗi như vậy không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết. --- # File: 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, di 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, 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 paywall hoặc onboarding mục tiêu. 3. Đặt tham số `assetId` tùy theo nền tảng mục tiêu của bạn. * Android: Sử dụng đường dẫn file tương đối so với thư mục `assets`. * iOS: Sử dụng tên file đầy đủ. ```kotlin showLineNumbers Adapty.setFallback(assetId = "fallback.json") .onSuccess { // Fallback paywalls loaded successfully } .onError { error -> // Handle the error } ``` Tham số: | Tham số | Mô tả | | :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **assetId** | Tên file cấu hình dự phòng (iOS). <br /> Đường dẫn file cấu hình dự phòng, tương đối so với 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 localizations và locale codes trong Kotlin Multiplatform SDK" description: "Quản lý localizations và locale codes để tiếp cận người dùng toàn cầu trong ứng dụng Kotlin Multiplatform của bạn." --- ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một số tình huống mà locale codes được sử 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 giữa các nền tảng, chúng tôi sử dụng một chuẩn nội bộ cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, do các mã này phức tạp, bạn cần hiểu rõ mình đang gửi gì đến máy chủ để nhận đúng localization và điều gì xảy ra tiếp theo — để bạn 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 được điều chỉnh nhẹ của [chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi mã gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số ví dụ: `en` (Tiếng Anh), `pt-br` (Tiếng Bồ Đào Nha (Brazil)), `zh` (Tiếng Trung Giản thể), `zh-hant` (Tiếng Trung Phồn thể). ## Khớp locale code \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client với locale code và bắt đầu tìm kiếm localization tương ứng của một paywall, quá trình xử lý 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ó locale code 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` từ `pt-br`) và tiếp tục 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` 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` đều sẽ nhận được kết quả giống nhau. ## 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 string resources được bản địa hóa trong dự án. Trong trường hợp đó, chúng tôi khuyến nghị đặt một cặp key-value chứa Adapty locale code dự kiến vào mỗi file resource cho localization tương ứng. Sau đó trích xuất giá trị của key này 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 } } ``` Cách này giúp bạn kiểm soát hoàn toàn localization nào sẽ được lấy cho mỗi người dùng của ứ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)) — lưu Adapty locale code dưới dạng string trong resource bundle của từng locale và đọc nó trước khi gọi SDK. ## 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 nhau) mà không cần định nghĩa rõ ràng locale codes cho từng localization. Cách đó là trích xuất locale code 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ó shared locale API 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ữ 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 string được 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ự động với cách tiếp cận được khuyến nghị ở trên. 2. Rất khó đoán thiết bị sẽ trả về chính xác gì và liệu nó có khớp với localization trong Adapty hay không. Locale của thiết bị có thể bao gồm các extension hoặc mã khu vực mà bạn chưa cấu hình trong Adapty, trong trường hợp đó SDK sẽ fallback về kết quả khớp subtag đầu tiên hoặc cuối cùng là `en`. 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 sử dụng liên quan. --- # 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 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 ra một URL duy nhất cho phép Adapty liên kết paywall cụ thể được hiển thị cho từng 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 và 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. Theo 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ẽ được 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à cũng 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. Sử dụng phương thức 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 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 người dùng liền mạch, bạn có thể mở web paywall trong trình duyệt trong ứng dụng. Điều này hiển thị trang mua hàng web ngay trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, hã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-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\} **Sự cố**: 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: "Implement paywalls manually in Kotlin Multiplatform SDK" description: "Learn how to implement paywalls manually in your Kotlin Multiplatform app with Adapty SDK." --- ## Accept purchases If you are working with paywalls you've implemented yourself, you can delegate handling purchases to Adapty, using the `makePurchase` method. This way, we will handle all the user scenarios, and you will only need to handle the purchase results. :::important `makePurchase` works with products created in the Adapty dashboard. Make sure you configure products and ways to retrieve them in the dashboard by following the [quickstart guide](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']} /> ## Observer mode If you want to implement your own purchase handling logic from scratch, but still want to benefit from the advanced analytics in Adapty, you can use the observer mode. :::important Consider the observer mode limitations [here](observer-vs-full-mode). ::: <CustomDocCardList ids={['implement-observer-mode-kmp', 'report-transactions-observer-mode-kmp', 'kmp-troubleshoot-purchases']} /> --- # File: kmp-quickstart-manual --- --- title: "Kích hoạt mua hàng trong paywall tùy chỉnh của bạn trong Kotlin Multiplatform SDK" description: "Tích hợp Adapty SDK vào paywall tùy chỉnh Kotlin Multiplatform 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 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 lo việc lấy sản phẩm, xử lý mua hàng 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 Paywall Builder](kmp-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic mua hàng, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để 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 cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi sản phẩm, giá cả và ưu đãi mà không cần chỉnh sửa code ứng dụng. - [**Placements**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (như `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trên dashboard, sau đó yêu cầu chúng bằng placement ID trong code. Điều này giúp bạn dễ dàng chạy A/B test và hiển thị paywall khác nhau cho từng 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 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 sản phẩm. Để hiểu những gì cần làm trên dashboard, hãy làm theo 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 từ phía mình. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và đã xác định theo cách khác nhau. Đọc [hướng dẫn nhanh về xác định người dùng](kmp-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`. ```kotlin showLineNumbers fun loadPaywall() { Adapty.getPaywall(placementId = "YOUR_PLACEMENT_ID") .onSuccess { paywall -> Adapty.getPaywallProducts(paywall = paywall) .onSuccess { products -> // Use products to build your custom paywall UI } .onError { error -> // Handle the error } } .onError { error -> // Handle the error } } ``` ## Bước 2. Chấp nhận mua hàng \{#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, 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ý luồng mua hàng và trả về hồ sơ người dùng đã 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 \{#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 của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Phương thức này sẽ đồng bộ lịch sử mua hàng của họ với Adapty và trả về hồ sơ người dùng đã cập nhật. ```kotlin showLineNumbers fun restorePurchases() { Adapty.restorePurchases() .onSuccess { profile -> // Restore successful, profile updated } .onError { error -> // Handle the error } } ``` ## Các bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra giao dịch 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 thử nghiệm từ paywall. Để xem cách hoạt động trong một triển khai thực tế, hãy xem [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ý mua hàng với xử lý lỗi và quản lý trạng thái đúng cách. Tiếp theo, [kiểm tra xem người dùng đã hoàn tất giao dịch chưa](kmp-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-kmp --- --- title: "Lấy paywall và sản phẩm cho remote config paywall trong Kotlin Multiplatform SDK" description: "Lấy paywall và sản phẩm trong Adapty Kotlin Multiplatform 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à 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 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 mobile của bạn (click để mở rộng)</summary> 1. [Tạo sản phẩm](create-product) của bạn 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-kotlin-multiplatform) 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 paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng mobile. Để hiển thị các sản phẩm, bạn cần lấy [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. ::: ```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 được kỳ vọng là mã ngôn ngữ gồm một hoặc nhiều thẻ phụ được phân tách bằng ký tự dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ 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ừ server 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 xuyên gặp vấn đề kết nối internet 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 thời gian tải sẽ nhanh hơn, bất kể chất lượng kết nối internet của họ. Cache được cập nhật thường xuyên nê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 ứ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 ở 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 paywall nhanh hơn và một server 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 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.</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 hạn chậm hơn một chút so với thời gian 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 ID sản phẩm! Vì paywall được cấu hình từ xa, các sản phẩm khả dụng, số lượng sản phẩm và ư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 mà không cần thay đổi code. Thứ duy nhất bạn phải hardcode là placement ID. Các 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/) với: danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy 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 } ``` Các 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: định danh sản phẩm, tên sản phẩm, giá, tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi 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 này 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 đầ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 việc bản địa hóa dựa trên quốc gia cửa hàng được chọn của người dùng chứ 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`. 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 chương 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ụ: đối 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ỉ số khác cho biết gói đăng ký có ưu đãi giới thiệu, hãy xem 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á 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`. Dùng thử miễn phí sẽ thuộc loại `FREE_TRIAL`.<br/>• `price`: Giá được giảm dưới dạng số. Đối 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 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ự cho các ưu đãi như phần trước đã mô tả.<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 độ 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 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 bạn muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như đã 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 Lý do 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ể: - **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ế 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 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 nhắm mục tiêu cá nhân hóa (bao gồm cả 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 nhược điểm này để được hưởng lợi từ việc lấy 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-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 | Đị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 được kỳ vọng là mã ngôn ngữ gồm một hoặc nhiều thẻ phụ được phân tách bằng ký tự dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ 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> | | **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 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 xuyên gặp vấn đề kết nối internet 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 thời gian tải sẽ nhanh hơn, bất kể chất lượng kết nối internet của họ. Cache được cập nhật thường xuyên nê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 ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.</p> | --- # 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." --- 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 để trình bày nó cho người dùng. Vì Remote Config linh hoạt và phụ thuộc vào 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 paywall sẽ 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ị 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, hãy 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( 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 đây, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và lắp rá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 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 liền mạch và thân thiện với người dùng trên nhiều 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 hoàn tất việc hiển thị paywall, 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](kmp-making-purchases). Chúng tôi khuyên bạn nên [tạo một paywall dự phòng gọi là fallback paywall](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 các tình huống này. ## Ghi lạ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ề các lần 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 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/). | --- # 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <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\} <Callout type="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. </Callout> 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 Observer mode trong Kotlin Multiplatform 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 Kotlin Multiplatform SDK." --- Nếu bạn đã có cơ sở 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ể tham khảo [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 phù hợp với nhu cầu của bạn, bạn chỉ cần: 1. Bật Observer mode 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 [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform). 2. [Báo cáo giao dịch](report-transactions-observer-mode-kmp) từ cơ sở hạ tầ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ý giao dịch mua 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à phân tích. :::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ý việc đó. ::: ```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ố: | 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ố 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: "Users & access in Kotlin Multiplatform SDK" description: "Learn how to work with users and access levels in your Kotlin Multiplatform app with Adapty SDK." --- This page contains all guides for working with users and access levels in your Kotlin Multiplatform app. Choose the topic you need: - **[Identify users](kmp-identifying-users)** - Learn how to identify users in your app - **[Update user data](kmp-setting-user-attributes)** - Set user attributes and profile data - **[Listen for subscription status changes](kmp-listen-subscription-changes)** - Monitor subscription changes in real-time - **[Kids Mode](kids-mode-kmp)** - Implement Kids Mode for your app --- # 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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: 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 in Kotlin Multiplatform SDK" description: "Learn how to work with onboardings in your Kotlin Multiplatform app with Adapty SDK." --- <CustomDocCardList /> --- # File: kmp-get-onboardings --- --- title: "Lấy onboarding trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho Kotlin Multiplatform." --- Sau khi [bạn đã thiết kế phần hiển thị cho onboarding](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Kotlin Multiplatform của mình. Bước đầu tiên trong quá trình này là lấy onboarding gắn 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 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). ## Lấy onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng builder no-code của chúng tôi, nó được lưu trữ dưới dạng một container chứa cấu hình mà ứng dụng của bạn cần lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm - nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập 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 phải tự triển khai tính năng 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 về 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( 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 | 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 đị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 phần phụ được phân tách bằng dấu trừ (**-**). Phần phụ đầu tiên là ngôn ngữ, phần thứ hai là vùng.<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 thường xuyên 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 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 thế nào. 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à 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 đượ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à 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 onboarding 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 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ả | |:----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-onboarding/) chứa: 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 lấy onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào 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à phương pháp đượ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ể tạo 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 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 các thuộc tính tùy chỉnh. Nếu tố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 dùng `getOnboardingForDefaultAudience` như bên dưới. Nếu không, 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 đị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 phần phụ được phân tách bằng dấu trừ (**-**). Phần phụ đầu tiên là ngôn ngữ, phần thứ hai là 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 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 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 internet của họ có bị gián đoạn thế nào. 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à 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 đượ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à 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 onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.</p> | --- # File: kmp-present-onboardings --- --- title: "Trình bày onboarding trong Kotlin Multiplatform SDK" description: "Tìm hiểu cách trình bày 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 về việc render nó trong code ứng dụng Kotlin Multiplatform để hiển thị cho người dùng. Onboarding đó đã 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 đã 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 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 để trình bày onboarding: - **Với Compose Multiplatform** - **Không dùng Compose Multiplatform** ## Với Compose Multiplatform \{#with-compose-multiplatform\} Để 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 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 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. ::: ```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 trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách onboarding được trình bày 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`. ```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\} 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 bằng 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.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 core `io.adapty:adapty-kmp`. 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 onboarding mà không dùng Compose Multiplatform, gọi `createNativeOnboardingView`. Phương thức 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 trong interface KMP sẽ trở thành `@required` trong Swift, bạn không thể implement `AdaptyUIOnboardingsEventsObserver` trực tiếp từ Swift. Hãy khai báo một lớp base mở trong `iosMain` trước: ```kotlin showLineNumbers title="iosMain (Kotlin)" open class BaseOnboardingObserver : AdaptyUIOnboardingsEventsObserver ``` Sau đó kế thừa nó trong Swift, ghi đè 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." --- 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. Xem 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ạo ID tùy ý, chẳng hạn 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ấ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 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ị onboarding đó. ::: Nếu bạn đang dùng [`createNativeOnboardingView`](kmp-present-onboardings#without-compose-multiplatform), `view.isStandaloneView` là `false` — cách triển khai mặc định không gọi `view.dismiss()`. 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ở paywall \{#opening-a-paywall\} :::tip Xử lý sự kiện này để mở paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi onboarding đóng 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 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. Bằng 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ụ 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 hoàn tất quá trình 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ấn để 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 xong | | `AdaptyOnboardingsAnalyticsEventScreenPresented` | Khi có 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 các kiểu sự kiện không xác định. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của onboarding flow | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | 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: ```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ụ các sự kiện (Nhấn để 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." --- 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ụ về 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\} ### Bổ sung dữ liệu vào hồ sơ người dùng \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ 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](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 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 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 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 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 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 xây dựng onboarding và gán ID có ý nghĩa cho các tùy chọn của nó. 2. Xử lý các câu trả lời quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](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 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](kmp-paywalls) cho placement trong code ứng dụng của bạn. Nếu onboarding của bạn có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho action của nút đó](kmp-handling-onboarding-events#opening-a-paywall). --- # File: kmp-best-practices --- --- title: "Best practices in Kotlin Multiplatform SDK" description: "Reference patterns for integrating Adapty SDK on Kotlin Multiplatform — call order, error handling, and other production-readiness rules." --- <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 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 tất trước khi bạn gọi bất kỳ phương thức Adapty SDK nào khác. 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 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()` 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 đua với nó sẽ trả về lỗi 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 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 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ẽ rơi vào 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\} Luồng 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ỉ 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 lúc 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). Luồng 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 | Lưu ý | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khi khởi chạy ứng dụng, thực hiện đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty.activate(configuration = AdaptyConfig.Builder("KEY").withCustomerUserId(...).build())` | Khi 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(configuration = AdaptyConfig.Builder("KEY").build())` không có `withCustomerUserId` | Khi khởi chạy ứng dụng, sau bước 1, nếu bạn chưa có customer user ID (hoặc không thu thập) | Adapty tạo hồ sơ người dùng ẩn danh. | | 3 | `Adapty.setIntegrationIdentifier("appsflyer_id", uid)` 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 được ghi 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ỉ trên luồng 2b có xác thực | Chờ `onSuccess` trước bất kỳ lệnh gọi hành động người dùng nào. Các lệnh gọi đồng thời trong quá trình `identify` có thể rơi vào 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ẽ khiến người dùng quay lại mất quyền truy cập premium, `appsflyer_id` bị thiếu trên hồ sơ người dùng, và paywall được trả về cho 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 đáng tin cậy: thời điểm, bộ nhớ đệm, và các phương án dự phòng cho Kotlin Multiplatform." --- Việc tải paywall đáng tin cậy trong Kotlin Multiplatform cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall đã được nhắm mục tiêu theo đối tượng, và có phương án 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, bộ nhớ đệm, và các phương án dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định `Adapty.activate()` và `Adapty.identify()` đã được giải quyết xong. Xem [Thứ tự gọi trong Kotlin Multiplatform SDK](kmp-sdk-call-order). ::: ## Quy tắc và những lỗi cần tránh \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement bạn sắp hiển thị. | Tải trước tất cả các placement đồng thời khi khởi chạy. | Tải trước hàng loạt sẽ chặn luồng chính và gây ra màn hình đen trong thời gian đó. | | Gọi `getPaywall` sau khi attribution đã có cơ hội được xử lý — ví dụ, 1–2 giây sau `activate` hoặc sau khi `setOnProfileUpdatedListener` kích hoạt. | Gọi `getPaywall` khi khởi chạy ứng dụng. | Attribution chưa được xử lý xong. Paywall sẽ được giải quyết dựa trên đối tượng mặc định và 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 ở 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 ứ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 đú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 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-best-practices --- --- title: "Best practices in Kotlin Multiplatform SDK" description: "Reference patterns for integrating Adapty SDK on Kotlin Multiplatform — call order, error handling, and other production-readiness rules." --- <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 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 tất trước khi bạn gọi bất kỳ phương thức Adapty SDK nào khác. 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 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()` 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 đua với nó sẽ trả về lỗi 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 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 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ẽ rơi vào 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\} Luồng 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ỉ 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 lúc 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). Luồng 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 | Lưu ý | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khi khởi chạy ứng dụng, thực hiện đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty.activate(configuration = AdaptyConfig.Builder("KEY").withCustomerUserId(...).build())` | Khi 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(configuration = AdaptyConfig.Builder("KEY").build())` không có `withCustomerUserId` | Khi khởi chạy ứng dụng, sau bước 1, nếu bạn chưa có customer user ID (hoặc không thu thập) | Adapty tạo hồ sơ người dùng ẩn danh. | | 3 | `Adapty.setIntegrationIdentifier("appsflyer_id", uid)` 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 được ghi 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ỉ trên luồng 2b có xác thực | Chờ `onSuccess` trước bất kỳ lệnh gọi hành động người dùng nào. Các lệnh gọi đồng thời trong quá trình `identify` có thể rơi vào 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ẽ khiến người dùng quay lại mất quyền truy cập premium, `appsflyer_id` bị thiếu trên hồ sơ người dùng, và paywall được trả về cho 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 đáng tin cậy: thời điểm, bộ nhớ đệm, và các phương án dự phòng cho Kotlin Multiplatform." --- Việc tải paywall đáng tin cậy trong Kotlin Multiplatform cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall đã được nhắm mục tiêu theo đối tượng, và có phương án 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, bộ nhớ đệm, và các phương án dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định `Adapty.activate()` và `Adapty.identify()` đã được giải quyết xong. Xem [Thứ tự gọi trong Kotlin Multiplatform SDK](kmp-sdk-call-order). ::: ## Quy tắc và những lỗi cần tránh \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement bạn sắp hiển thị. | Tải trước tất cả các placement đồng thời khi khởi chạy. | Tải trước hàng loạt sẽ chặn luồng chính và gây ra màn hình đen trong thời gian đó. | | Gọi `getPaywall` sau khi attribution đã có cơ hội được xử lý — ví dụ, 1–2 giây sau `activate` hoặc sau khi `setOnProfileUpdatedListener` kích hoạt. | Gọi `getPaywall` khi khởi chạy ứng dụng. | Attribution chưa được xử lý xong. Paywall sẽ được giải quyết dựa trên đối tượng mặc định và 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 ở 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 ứ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 đú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 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: "Reference for Kotlin Multiplatform SDK" description: "Reference documentation for Adapty Kotlin Multiplatform SDK." --- This page contains reference documentation for Adapty Kotlin Multiplatform SDK. Choose the topic you need: - **[SDK models](https://kmp.adapty.io/adapty/)** - Data models and structures used by the SDK - **[Handle errors](kmp-handle-errors)** - Error handling and troubleshooting --- # File: kmp-handle-errors --- --- title: "Handle errors in Kotlin Multiplatform SDK" description: "Learn how to handle errors in your Kotlin Multiplatform app with Adapty." --- This page covers error handling in the Adapty Kotlin Multiplatform SDK. ## Error handling basics All Adapty SDK methods return results that can be either success or error. Always handle both cases: <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> ## Common error codes | Error Code | Description | Solution | |------------|-------------|----------| | 1000 | No product IDs found | Check product configuration in dashboard | | 1001 | Network error | Check internet connection | | 1002 | Invalid SDK key | Verify your SDK key | | 1003 | Can't make payments | Device doesn't support payments | | 1004 | Product not available | Product not configured in store | ## Handle specific errors ### 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> ### 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> ## Error recovery strategies ### 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> ### 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> ## Next steps - [Fix for Code-1000 noProductIDsFound error](InvalidProductIdentifiers-kmp) - [Fix for Code-1003 cantMakePayments error](cantMakePayments-kmp) - [Complete API reference](https://android.adapty.io) - Full SDK documentation --- # 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\} --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <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: "Kotlin Multiplatform SDK Migration Guides" description: "Migration guides for Adapty Kotlin Multiplatform SDK versions." --- This page contains all migration guides for Adapty Kotlin Multiplatform SDK. Choose the version you want to migrate to for detailed instructions: - **[Migrate to v3.15](migration-to-kmp-315)** --- # 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-06-24T14:36:38.787Z_ _Successfully processed: 50/50 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-06-24T14:36:38.789Z Total files: 44 --- # File: sdk-installation-react-native-expo --- --- title: "Cài đặt & cấu hình Adapty React Native SDK trong dự án Expo" description: "Hướng dẫn từng bước cài đặt Adapty React Native SDK trong dự án Expo cho ứng dụng dựa trên gói đăng ký." --- :::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 dự án Expo**. Nếu bạn đang sử dụng **pure React Native (không dùng 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 để 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 để dễ dàng tạo paywall đa nền tảng. AdaptyUI được kích hoạt tự động cùng với module core. Nếu bạn cần hướng dẫn đầy đủ về cách triển khai IAP trong ứng dụng React Native, hãy xem [bài viết này](https://adapty.io/blog/react-native-in-app-purchases-tutorial/). :::tip 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) cho đầy đủ chức năng bao gồm mua hàng thực 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ũng có thể xem video: <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 hỗ trợ iOS 13.0 trở lên, nhưng để sử dụng paywall tạo trong [Adapty paywall builder](adapty-paywall-builder) cần iOS 15.0 trở lên. :::info Từ SDK v3.17, Adapty SDK sử dụng Google Play Billing Library v8.0.0 theo mặc định. ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![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/) (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ể sử 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ó mua hàng thực và không có AdaptyUI/Paywall Builder rendering). ::: 1. Cài đặt Adapty SDK (thao tác này cũng tự động cài đặt `@adapty/core`): ```sh npx expo install react-native-adapty npx expo prebuild ``` 2. Build ứng dụ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 ``` ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Để lấy **Public SDK Key**: 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 `"YOUR_PUBLIC_SDK_KEY"` trong code. :::important - Đảm bảo bạn dùng **Public SDK key** để khởi tạo Adapty, còn **Secret key** chỉ 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 chắc chắn 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 Adapty SDK nào khác. 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 làm theo [hướng dẫn nhanh Paywall Builder](react-native-quickstart-paywalls). - Nếu bạn tự xây dựng UI 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 dùng 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 định 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 core module; bạn không cần làm thêm gì khác. ## Thiết lập tùy chọn \{#optional-setup\} ### Logging \{#logging\} #### Thiết lập hệ thống logging \{#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 cấp độ sau: | Cấp độ | Mô tả | | ---------- | ------------------------------------------------------------ | | `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 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 loại thông báo thông tin | | `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. | Bạn có thể đặt cấp độ log trong ứng dụng trước hoặc trong khi 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ể triển khai 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 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 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 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ần thiết cho ứng dụng của bạn. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { 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 thu thập các advertising identifier. Giá trị mặc định là `false`. Dùng tham số này để tuân thủ chính sách App Store/Play Store, tránh kích hoạt yêu cầu App Tracking Transparency, 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 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 media cache cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Theo mặc định, AdaptyUI cache 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 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: ```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 }, }); ``` Các tham số: | Tham số | Bắt buộc | Mô tả | |-----------|----------|-------------| | memoryStorageTotalCostLimit | tùy chọn | Tổng kích thước cache trong bộ nhớ tính bằng byte. Mặc định theo giá trị cụ thể của nền tảng. | | memoryStorageCountLimit | tùy chọn | Giới hạn số lượng item trong memory storage. Mặc định theo giá trị cụ thể của 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ụ thể của nền tảng. | ### Bật local access levels (Android) \{#enable-local-access-levels-android\} Theo mặc định, [local access levels](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 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 tất cả dữ liệu SDK được lưu 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 mới. 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\} #### Thiết lập chế độ mock cho Expo Go / Expo Web \{#set-up-mock-mode-for-expo-go--expo-web\} Môi trường 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 thử UI và 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 thử mua hàng thực: - 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 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 Xcode/Android build hoặc API key không hợp lệ cũng sẽ không gây ra lỗi. Để kiểm thử mua hàng thực và paywall 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ì 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 trả về dữ liệu mock mà không gửi yêu cầu mạng tới máy chủ Adapty. - Theo mặc định, hồ sơ người dùng mock ban đầu không có gói đăng ký đang hoạt động. - Theo mặc định, `makePurchase(...)` mô phỏng một lần mua hàng 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 config 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 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 kích hoạt 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 sự cố trong iOS Simulator, vốn 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 nó 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 ngăn các yêu cầu xác thực không cần thiết nếu không cần thiết. Điều quan trọng cần lưu ý là **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 tiềm năng. Trong môi trường production, không nên trì hoãn kích hoạt vì 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 thông tin đăng nhập. Đây là cách tiếp cận đượ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 với 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.` Lỗi 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 kích hoạt trong quá trình phát triển. Để ngăn điều này, hãy dùng tùy chọn `__ignoreActivationOnFastRefresh` đặt thành `__DEV__` (flag 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 } ``` ## Xử lý 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ể thấy lỗi về **phiên bản iOS tối thiểu** hoặc deployment target, đặc biệt nếu bạn dùng paywall được tạo trong [Adapty paywall builder](adapty-paywall-builder), yêu cầu **iOS 15.0 trở lên**. Vì Expo tạo dự án iOS (bao gồm `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 Expo config (`app.json` hoặc `app.config.js`) để đặt iOS deployment target: ``` { "expo": { // ...other Expo config... "plugins": [ [ "expo-build-properties", { "ios": { // Use "13.0" for core Adapty features only, // or "15.0" if you use paywalls created in the paywall builder. "deploymentTarget": "15.0" } } ], ] } } ``` 3. Tạo lại dự án iOS native và rebuild: ``` 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ó cấu hình Android Auto Backup (như Adapty, AppsFlyer, hoặc expo-secure-store), bạn có thể gặp xung đột manifest merger. Lỗi thường thấy 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 dùng `expo-secure-store`, hãy tắt thiết lập backup của nó để tránh trùng lặp. Đây là cách cấu hình `app.json`: ```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 Android backup. Thêm `"configureAndroidBackup": false` nếu bạn dùng `expo-secure-store` để tránh cảnh báo, vì cấu hình backup của SecureStore sẽ được Adapty xử lý. :::important Thiết lập này chỉ áp dụng các yêu cầu backup cho Adapty, AppsFlyer và expo-secure-store. Nếu các thư viện khác trong dự án của bạn định nghĩa các quy tắc backup tùy chỉnh, bạn sẽ cần cấu hình thủ công cho những thư viện đó. ::: --- # File: sdk-installation-react-native-pure --- --- title: "Cài đặt & cấu hình Adapty SDK trong dự án React Native thuần" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên React Native cho ứng dụng có tính năng đăng ký." --- :::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 làm theo [hướng dẫn cài đặt cho Expo](sdk-installation-react-native-expo). ::: Adapty SDK bao gồm hai module chính để tích hợp 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 để 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 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ộ quy 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 hỗ trợ iOS 13.0 trở lên, nhưng để sử dụng paywall được tạo trong [Adapty paywall builder](adapty-paywall-builder) thì cần iOS 15.0 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. ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![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 (lệnh này cũng tự động cài `@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 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> ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Để lấy **Public SDK Key**: 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 `"YOUR_PUBLIC_SDK_KEY"` trong code. :::important - Đảm bảo bạn dùng **Public SDK key** để khởi tạo Adapty, còn **Secret key** chỉ 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 chắc chắn 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 chờ `activate` hoàn thành trước khi gọi bất kỳ phương thức nào khác của Adapty SDK. Xem [Thứ tự gọi trong 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), làm theo [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, 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 sử dụng [các mẹo sau](#development-environment-tips). ::: ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn dự định dùng [Paywall Builder](adapty-paywall-builder), 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 làm thêm gì nữa. ## Thiết lập tùy chọn \{#optional-setup\} ### Logging \{#logging\} #### Thiết lập hệ thống logging \{#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ấp độ log hiện có: | Cấp độ | Mô tả | | ---------- | ------------------------------------------------------------ | | `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 khác nhau | | `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 cấp độ 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 gửi thông tin đó một cách rõ ràng, nhưng bạn có thể triển khai 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 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 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 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 title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { 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 thu thập các 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 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 media (như hình ảnh và video) để cải thiện hiệu suất và giảm sử dụng mạng. Bạn có thể tùy chỉnh cài đặt bộ nhớ đệm bằng cách cung cấp cấu hình tùy chỉnh. Dùng `mediaCache` để ghi đè 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ố: | Tham số | Bắt buộc | Mô tả | |-----------|----------|-------------| | memoryStorageTotalCostLimit | tùy chọn | Tổng kích thước bộ nhớ đệm trong RAM 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ớ đệm RAM. 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\} 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, đặ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 backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK phát hiện khi ứng dụng được khôi phục từ backup iCloud và xóa toàn bộ dữ liệu SDK được lưu cục bộ, bao gồm thông tin hồ sơ người dùng đã cache, chi tiết sản phẩm và paywall. SDK sau đó khởi tạo với 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 không bị 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 khi kích hoạt 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 sự cố 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 do 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ời gọi activate sẽ được giữ lại cho đến khi bạn thực hiện lời gọi Adapty SDK tiếp theo. Điều này ngăn các lời nhắc xác thực không cần thiết nếu 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 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 kích hoạt vì thiết bị thực thường nhớ dữ liệu xác thực và không liên tục yêu cầu thông tin đăng nhập. Đâ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 với 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.` Lỗi 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, sử dụng tùy chọn `__ignoreActivationOnFastRefresh` đặ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 thử 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 để tránh cần tài khoản sandbox App Store/Google Play và tăng tốc độ lặp. Chế độ mock hoàn toàn bỏ qua các module native của Adapty và trả về dữ liệu mô phỏng. :::important Chế độ mock **không** phải là công cụ để kiểm thử mua hàng thực: - 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 module native của Adapty bị **bỏ qua hoàn toàn** — ngay cả khi thiếu file SDK native trong build Xcode/Android hay API key không hợp lệ cũng sẽ không gây ra lỗi. - Không có dữ liệu nào được gửi đến máy chủ của Adapty. Để kiểm thử mua hàng thực và paywall của Paywall Builder, hãy tắt chế độ mock và sử dụng tài khoản sandbox. ::: Để bật chế độ mock, đặ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 Adapty trả về dữ liệu mock mà không thực hiện yêu cầu mạng đến máy chủ 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 lần 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 cách dùng `mockConfig` trong quá trình kích hoạt. Xem định dạng config 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 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 về phiên bản iOS tối thiểu, hãy cập nhật Podfile của bạn: ```diff -platform :ios, min_ios_version_supported +platform :ios, '13.0' # For core features only # OR +platform :ios, '15.0' # If using paywalls created in the paywall builder ``` #### Xung đột Android Auto Backup manifest \{#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"/> #### 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 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 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 chế độ khởi động `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`, đảm bảo Activity khởi động 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\} Khi build ứng dụng React Native cho iOS, bạn có thể thấy lỗi biên dịch Swift 6 trên các pod target của Adapty. Các triệu chứng điển hình 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 ứng dụng 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`) cần được build với Swift 6. Nguyên nhân phổ biến nhất là hook `post_install` trong `ios/Podfile` ghi đè `SWIFT_VERSION` cho mọi pod target: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` **Cách sửa**: Loại trừ các pod target của Adapty khỏi việc ghi đè: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| next if %w[Adapty AdaptyUI AdaptyUIBuilder AdaptyLogger AdaptyPlugin].include?(target.name) target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` Sau đó chạy `pod install` từ thư mục `ios/` và build lại. Để xác minh, mở `ios/Pods/Pods.xcodeproj`, chọn pod target `Adapty` → **Build Settings** → **Swift Language Version**. Giá trị phải là **Swift 6**. --- # File: react-native-quickstart-paywalls --- --- title: "Kích hoạt mua hàng bằng cách sử dụng paywall trong React Native SDK" description: "Tìm hiểu cách hiển thị paywall trong ứng dụng React Native của bạn với Adapty SDK." --- Để kích hoạt in-app purchase, bạn cần nắm 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) - [**Paywall**](paywalls) là các 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 thay đổi ưu đãi, giá cả và kết hợp sản phẩm 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 (như `main`, `onboarding`, `settings`). Bạn thiết lập 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 dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho các nhóm người dùng khác nhau. Adapty cung cấp ba cách để kích hoạt mua hàng trong ứng dụng. 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 thanh toán trong trình tạo no-code](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ý. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự xây dựng UI paywall trong code ứng dụng, nhưng vẫn lấy đối tượng paywall từ Adapty để duy trì sự linh hoạt trong danh mục sản phẩm. Xem [hướng dẫn](react-native-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 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 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ự tạo](react-native-making-purchases). ::: Để hiển thị paywall được tạo trong Adapty Paywall Builder, trong code ứng dụng bạn chỉ cần: 1. **Lấy paywall**: Lấy paywall từ Adapty. 2. **Hiển thị paywall và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị container paywall bạn đã lấy 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 paywall với phản hồi tương ứng trong ứng dụng. Ví dụ, mở link 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 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 paywall \{#1-get-the-paywall\} Các paywall của bạn được liên kết với các placement được cấu hình 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ử UI và style 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ị. ::: ```typescript showLineNumbers title="React Native" try { const placementId = 'YOUR_PLACEMENT_ID'; const paywall = await adapty.getPaywall(placementId); // the requested paywall } catch (error) { // handle the error } if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { //use your custom logic } ``` ## 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. <Tabs groupId="presentation-method" queryString> <TabItem value="platform" label="React component" default> Để nhúng 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 cấu trúc component React Native: ```typescript showLineNumbers title="React Native (TSX)" function MyPaywall({ paywall }) { const onCloseButtonPress = useCallback<EventHandlers['onCloseButtonPress']>(() => {}, []); const onUrlPress = useCallback<EventHandlers['onUrlPress']>((url) => { Linking.openURL(url); }, []); return ( <AdaptyPaywallView paywall={paywall} style={styles.container} onCloseButtonPress={onCloseButtonPress} onUrlPress={onUrlPress} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Để hiển thị paywall như một 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`. Mỗi `view` chỉ có thể được dùng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo một instance `view` mới. ```typescript showLineNumbers title="React Native" try { await view.present(); } catch (error) { // handle the error } ``` </TabItem> </Tabs> :::tip Để biết thêm chi tiết về cách hiển thị paywall, hãy xem [hướng dẫn](react-native-present-paywalls) của chúng tôi. ::: ## 3. Xử lý các hành động nút \{#3-handle-button-actions\} Khi người dùng nhấn các nút trong paywall, React Native SDK tự động xử lý mua hàng, khôi phục, đóng paywall và mở URL. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được định nghĩa 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 động mặc định của chúng. Ví dụ, đây là hành động mặc định cho 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. <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 trực tiếp trong component `AdaptyPaywallView`: ```typescript showLineNumbers title="React Native (TSX)" function MyPaywall({ paywall }) { const onUrlPress = useCallback<EventHandlers['onUrlPress']>((url) => { Linking.openURL(url); }, []); const onCloseButtonPress = useCallback<EventHandlers['onCloseButtonPress']>(() => {}, []); const onCustomAction = useCallback<EventHandlers['onCustomAction']>((actionId) => {}, []); return ( <AdaptyPaywallView paywall={paywall} style={styles.container} onUrlPress={onUrlPress} onCloseButtonPress={onCloseButtonPress} onCustomAction={onCustomAction} /> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> Với modal presentation, hãy triển khai các event handler bằng `setEventHandlers`: ```typescript showLineNumbers title="React Native" const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; // allow paywall closing } }); ``` </TabItem> </Tabs> :::tip Đọc các hướng dẫn của chúng tôi về cách xử lý [hành động](react-native-handle-paywall-actions) và [sự kiện](react-native-handling-events-1) của nút. ::: ## Các bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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\} Đây là cách tất cả các bước đó có thể được tích hợp trong ứ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 PaywallScreen() { const [paywall, setPaywall] = useState(null); const loadPaywall = async () => { try { const paywallData = await adapty.getPaywall('YOUR_PLACEMENT_ID'); if (paywallData.hasViewConfiguration) { setPaywall(paywallData); } } catch (error) { console.warn('Error loading paywall:', error); } }; const onUrlPress = useCallback<EventHandlers['onUrlPress']>((url) => { Linking.openURL(url); }, []); const onCloseButtonPress = useCallback<EventHandlers['onCloseButtonPress']>(() => { // Handle close button press }, []); useEffect(() => { loadPaywall(); }, []); return ( <View style={{ flex: 1 }}> {paywall ? ( <AdaptyPaywallView paywall={paywall} style={{ flex: 1 }} onUrlPress={onUrlPress} onCloseButtonPress={onCloseButtonPress} /> ) : ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Button title="Load Paywall" onPress={loadPaywall} /> </View> )} </View> ); } ``` </TabItem> <TabItem value="standalone" label="Modal presentation"> ```javascript showLineNumbers title="React Native" export default function PaywallScreen() { const showPaywall = async () => { try { const paywall = await adapty.getPaywall('YOUR_PLACEMENT_ID'); if (!paywall.hasViewConfiguration) { // use your custom logic return; } const view = await createPaywallView(paywall); view.setEventHandlers({ onCloseButtonPress() { return true; }, }); await view.present(); } catch (error) { // handle any error that may occur during the process console.warn('Error showing paywall:', error); } }; // you can add a button to manually trigger the paywall for testing purposes return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Button title="Show Paywall" onPress={showPaywall} /> </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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-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 paywalls Paywall Builder và cấu hình của chúng trong React Native SDK" description: "Tìm hiểu cách truy xuất PB paywalls trong Adapty để kiểm soát gói đăng ký tốt hơn trong ứng dụng React Native 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 quá trình này là lấy paywall 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. ::: 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 paywalls và sản phẩm cho remote config paywalls 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ị paywalls 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à tích hợp các sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placements và tích hợp paywall của bạn vào đó](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 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 phải 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ị và cách thức hiển thị. Tuy nhiên, bạn cần lấy ID của nó qua placement, cấu hình view, rồi hiển thị nó trong ứng dụng di động của mình. Để đả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](react-native-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) càng sớm càng tốt, để có đủ 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); // 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 kỳ vọng 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 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 trong trường hợp 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 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 hoàn toàn, 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ọ như thế nào. Cache được cập nhật thường xuyên nên an toàn để sử dụng trong phiên nhằm 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 dọn dẹp thủ công.</p><p></p><p>Adapty SDK lưu trữ paywalls 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 paywalls 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 đượ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 paywalls 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 timeout cho phương thức này. Nếu timeout được đạt đến, 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 `loadTimeout`, vì thao tác có thể bao gồm các yêu cầu khác nhau bên dưới.</p><p>Với Android: Bạn có thể tạo `TimeInterval` với 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://react-native.adapty.io/interfaces/adaptypaywall) với danh sách ID sản phẩm, định danh paywall, remote config và một số thuộc tính khác. | ## Lấy cấu hình view của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Đả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 có sẵn để truy xuất. ::: Sau khi lấy paywall, hãy kiểm tra xem nó có bao gồm `ViewConfiguration` hay 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ư 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 lấy cấu hình view theo cách 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` lần nữa. 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 | Một đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **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ể để 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ối ưu hóa thời gian hiển thị sản phẩm trên màn hình. Khi `true`, AdaptyUI sẽ tự động lấy 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, paywalls đượ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à paywalls, 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ị paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như được mô tả chi tiết trong phần [Lấy thông tin Paywall](#fetch-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ố nhược điểm đáng kể: - **Vấn đề khả năng tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywalls khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế paywalls hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng có phiên bản hiện tại (cũ) có thể gặp sự cố với paywalls không được render. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng nhắm mục tiêu 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 sẵn sàng chấp nhận những nhược điểm này để 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 sử 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` có sẵn 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 | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard của bạn. | | **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 được kỳ vọng 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ẻ 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></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ừ server và trả về dữ liệu đã cache trong trường hợp 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 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 hoàn toàn, 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ọ như thế nào. Cache được cập nhật thường xuyên nên an toàn để sử dụng trong phiên nhằm 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 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. Hình ảnh hero và video có ID được xác định trước: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm mục tiêu các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị hình ảnh preview cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh preview trước khi chạy video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty React Native SDK lên phiên bản 3.8.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: ```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ẽ quay lại giao diện mặc định của nó. ::: --- # File: react-native-present-paywalls --- --- title: "React Native - Hiển thị paywall mới bằng Paywall Builder" description: "Hiển thị paywall trong ứng dụng React Native bằng Adapty." --- 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 mã ứng dụng di động để hiển thị cho người dùng. Một paywall 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 đã [tạo một paywall](create-paywall). 2. Bạn đã thêm paywall vào một [placement](placements). 3. Bạn đã [tải 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 của 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 và paywall dùng Remote Config. - Để hiển thị **paywall dùng Remote Config**, xem [Render 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 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. - **Hiển thị dạng modal** ## React component \{#react-component\} :::note Cách dùng **React component** yêu cầu SDK 3.14.0 trở lên. ::: Để nhúng paywall vào cây component hiện có của bạn, hãy dùng component `AdaptyPaywallView` trực tiếp trong hệ thống phân cấp component React Native. Component được nhúng 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. :::note Trên Android, nếu paywall không kéo dài ra sau thanh trạng thái, một lớp phủ trực quan có thể xuất hiện ở phía trên cùng. Chúng tôi khuyên bạn nê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} /> ); } ``` ## Hiển thị dạng modal \{#modal-presentation\} Để hiển thị 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 [`createPaywallView`](react-native-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 bạn cần hiển thị paywall lại, 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 là không được phép. Điều 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 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 handler đã đặt trước đó cho những sự kiện cụ thể đó. ::: </TabItem> <TabItem value="old" label="SDK version < 3.14" default> ```typescript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); view.registerEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` </TabItem> </Tabs> ### Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`. ```typescript showLineNumbers try { await view.present(iosPresentationStyle: 'page_sheet'); } catch (error) { // handle the error } ``` ## Dùng bộ đếm thời gian do nhà phát triển định nghĩa \{#use-developer-defined-timer\} Để sử dụng bộ đếm thời gian 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 bộ đếm thời gian do nhà phát triển định nghĩa mà bạn đã đặt trong Adapty dashboard. Điều này đảm bảo ứng dụng của bạn cập nhật động bộ đếm thời gian với giá trị chính xác — như `13d 09h 03m 34s` (được tính bằng thời gian kết thúc của bộ đếm, chẳng hạn 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 bộ đếm thời gian do nhà phát triển định nghĩa mà bạn đã đặt trong Adapty dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động bộ đếm thời gian với giá trị chính xác — như `13d 09h 03m 34s` (được tính bằng thời gian kết thúc của bộ đếm, chẳng hạn Ngày đầu 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 vì 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 alert thông thường của RN 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 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 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ố gắng mua một gói đăng ký mới trong khi đang có gói đăng ký khác hoạt động trên Android, bạn có thể kiểm soát cách xử lý giao dịch mới bằng cách truyền 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 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ủ trực quan ở đầ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 túy. Nếu bạn đang dùng Expo managed workflow, bạn không thể thêm tài nguyên Android này trực tiếp. Để áp dụng cài đặt này, bạn phải tạo một Expo config plugin tùy chỉnh để thêm tài nguyên Android 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ý dự án Android gốc cho bạn. ::: Nếu `AdaptyPaywallView` không kéo dài ra 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 cùng. Để xóa nó, hãy thêm tài nguyên boolean sau vào ứng dụng của bạn: 1. Đi đến `android/app/src/main/res/values`. Nếu không có file `bools.xml`, hãy tạo file đó. 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 cho tất cả paywall trong ứng dụng của bạn. --- # File: react-native-handle-paywall-actions --- --- title: "Xử lý hành động nút trong React Native SDK" description: "Xử lý các hành động nút trên paywall trong React Native 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 các nút đúng cách là rất quan trọng: 1. Thêm [nút trong paywall builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một ID hành động tùy chỉnh. 2. Viết code trong ứng dụng để 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à các 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 cần được triển khai xử lý trong code ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong paywall builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code ứng dụng, triển khai handler cho hành động `close` để đóng paywall. :::info Trong React Native SDK, hành động `close` 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. ::: <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> Với React component, xử lý hành động đóng thông qua các prop handler sự kiện riêng lẻ: ```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"> Với modal presentation, triển khai handler đóng: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; // allow paywall closing } }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14" default> Với SDK version < 3.14, chỉ hỗ trợ modal presentation: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ 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 mua hàng), 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 (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 handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong React Native SDK, hành động `openUrl` mặc định sẽ kích hoạt việc mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: <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> Với React component, xử lý việc mở URL thông qua prop handler sự kiện: ```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"> Với modal presentation, triển khai handler URL: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onUrlPress(url) { Linking.openURL(url); return false; // Keep paywall open }, }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14" default> Với SDK version < 3.14, chỉ hỗ trợ modal presentation: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ 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 handler cho hành động `login` để xác định người dùng. <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> Với React component, xử lý đăng nhập thông qua prop handler sự kiện: ```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"> Với modal presentation, triển khai handler đăng nhập: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'login') { navigation.navigate('Login'); } } }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14" default> Với SDK version < 3.14, chỉ hỗ trợ modal presentation: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ onCustomAction(actionId) { if (actionId === 'login') { navigation.navigate('Login'); } } }); ``` </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 paywall builder, thêm một nút, gán cho nó hành động **Custom** và đặt mộ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: <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> Với React component, xử lý các hành động tùy chỉnh thông qua prop handler sự kiện: ```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"> Với modal presentation, triển khai các handler hành động tùy chỉnh: ```javascript const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another paywall } }, }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14" default> Với SDK version < 3.14, chỉ hỗ trợ modal presentation: ```javascript const unsubscribe = view.registerEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another paywall } }, }); ``` </TabItem> </Tabs> --- # File: react-native-handling-events-1 --- --- title: "React Native - Xử lý sự kiện paywall" description: "Xử lý các sự kiện gói đăng ký trong React Native 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, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý các 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. Những 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 trên paywall. Tìm hiểu cách phản hồi các sự kiện này bên dưới. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** yêu cầu Adapty SDK v3.0 trở lên. ::: Để kiểm soát hoặc theo dõi các quy trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai các 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> Với React component, bạn xử lý các sự kiện thông qua các prop event handler 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"> Với modal presentation, hãy triển khai phương thức event handler. :::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ể đó. ::: ```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, product) { /***/ }, onProductSelected(productId) { /***/}, onRenderingFailed(error) { /***/ }, onLoadingProductsFailed(error) { /***/ }, onUrlPress(url) { Linking.openURL(url); return false; // Keep paywall open }, onPaywallShown() { /***/ }, onPaywallClosed() { /***/ }, onWebPaymentNavigationFinished() { /***/ }, }); ``` </TabItem> </Tabs> </TabItem> <TabItem value="old" label="SDK version < 3.14" default> Với SDK version < 3.14, chỉ hỗ trợ modal presentation: ```javascript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ 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) { /***/}, 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ấn để mở rộng)</summary> ```javascript // onCloseButtonPress { "event": "close_button_press" } // onAndroidSystemBack { "event": "android_system_back" } // 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" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } } // 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" } } } // onPaywallShown { "event": "paywall_shown" } // onPaywallClosed { "event": "paywall_closed" } // onWebPaymentNavigationFinished { "event": "web_payment_navigation_finished" } ``` </Details> Bạn có thể đăng ký những event handler mà bạn cần và bỏ qua những cái không cần. Trong trường hợp đó, các event listener không dùng đến sẽ không được tạo. 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, do đó 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 hiển thị của nó thất bại. ### Các event handler \{#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 của bạn. | | **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ên đóng màn hình paywall trong handler này. | | **onPurchaseCompleted** | Được gọi khi giao dịch mua hoàn thành, dù thành công, bị người dùng hủy, hay đang chờ phê duyệt. Trong trường hợp mua thành công, nó cung cấp `AdaptyProfile` đã được cập nhật. Việc 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 "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ờ xử lý, những trường hợp này 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 chủ đề [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 paywall view được chọn, cho phép bạn theo dõi những gì người dùng chọn trước khi mua. | | **onRenderingFailed** | Được gọi khi xảy ra lỗi trong quá trình hiển thị view 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, 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ừ server. | | **onPaywallShown** | Được gọi khi paywall 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) bên trong paywall và web paywall mở trong trình duyệt trong ứng dụng. | | **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 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. | | **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. | --- # File: react-native-use-fallback-paywalls-expo --- --- title: "Sử dụng fallback paywall trong dự án Expo" description: "Cấu hình fallback paywall 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 **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 fallback 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 fallback từ bundle **native** — một resource iOS bên trong package `.app`, hoặc một entry trong `android/app/src/main/assets/`. Trong dự án Expo, lệnh `npx expo prebuild --clean` sẽ tái tạo lại các thư mục đó sau mỗi lần chạy, nên bạn không thể đặt file vào thủ công. Config plugin `react-native-adapty` sẽ tự động tích hợp file vào bundle native cho bạn. :::tip Một ví dụ hoàn chỉnh đã được cung cấp 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 fallback ở 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 platform là tùy chọn — chỉ cần cấu hình những platform 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 file JSON fallback khác nhau cho từng platform — ID sản phẩm của Apple trên iOS, ID sản phẩm Google Play trên Android. Hãy trỏ từng platform đến file của riêng nó. ::: 3. Tái tạo lại các dự án native: ```sh title="Shell" npx expo prebuild ``` Plugin sẽ thêm file iOS vào bundle resources của dự án Xcode và sao chép file Android vào `android/app/src/main/assets/`. Output của prebuild sẽ có các dòng như sau: ``` [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 fetch bất kỳ paywall hoặc onboarding nào. ::: ## 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 **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 fallback 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 \{#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 (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 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 kèm 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="Hiện tại (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="Cũ (trước v3.8)" default> ```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> Các tham số: | Tham số | Mô tả | | :------------------- | :------------------------------------------------------- | | **fileLocation** | Đối tượng đại diện cho vị trí của file cấu hình dự phòng. | --- # File: react-native-localizations-and-locale-codes --- --- title: "Sử dụng bản địa hóa và mã ngôn ngữ trong React Native SDK" description: "Tìm hiểu cách bản địa hóa paywall trong ứng dụng React Native 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ã ngôn ngữ (locale code) trở nên cần thiết — ví dụ, khi bạn muốn lấy đúng paywall phù hợp với bản địa hóa 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 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ư mong đợi. ## Chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Adapty sử dụng chuẩn [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) với một số đ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. 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 yêu cầu từ SDK phía client kèm theo mã ngôn ngữ và bắt đầu tìm kiếm bản địa hóa 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 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 phần chuỗi trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tìm kiếm bản địa hóa khớp 4. Nếu vẫn không tìm thấy, hệ thống trả về bản địa hóa 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 kết quả. ## Cách triển khai bản địa hóa được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang tìm hiểu về bản địa hóa, khả năng cao 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 đặt một cặp key-value chứa mã ngôn ngữ Adapty mong muốn vào từng file tương ứng. 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); }; }; ``` Cách này giúp bạn kiểm soát hoàn toàn bản địa hóa nào sẽ được lấy về cho từng người dùng của ứng dụng. ## Cách triển khai bản địa hóa thay thế \{#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 rõ ràng mã ngôn ngữ cho từng bản địa hóa. Cách này đồng nghĩa với việc trích xuất mã ngôn ngữ từ thiết bị, chẳng hạn 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 là hai thứ khác nhau. Nếu bạn muốn bản địa hóa được chọn đúng, bạn phải dựa vào logic xử lý của Apple — vốn hoạt động tự động khi bạn dùng cách khuyến nghị với file chuỗi đã bản địa hóa — hoặc tự tái tạo 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ẽ fallback về kết quả khớp theo subtag đầu tiên hoặc cuối cùng là `en` — điều này có thể không phải ngôn ngữ mà bạn muốn mặc định cho người dùng đó. Nếu bạn vẫn quyết định 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: 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ế trong Adapty Paywall Builder với React Native 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 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 bị nhân đôi \{#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**: Có thể bạn đang gọi `logShowPaywall` trong code, khiến số lượt xem bị tính hai lần 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\} **Sự cố**: Bạn gặp phải các vấn đề 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 sự cố đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: react-native-quickstart-manual --- --- title: "Bật tính năng mua hàng trong paywall tùy chỉnh với React Native SDK" description: "Tích hợp Adapty SDK vào các paywall React Native tùy chỉnh để bậ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 giữ 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 triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để bật tính năng mua hàng, hãy sử dụng [Adapty Paywall Builder](react-native-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic mua hàng, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để bật in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn chỉnh sửa sản phẩm, giá cả và ưu đãi mà không cần thay đổi 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 trên dashboard, rồi truy xuất 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 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 vào một **placement**. Thiết lập này cho phép bạn lấy sản phẩm của mình. Để hiểu những gì cần làm trên dashboard, hãy làm theo 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ầ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 sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `paywall` bằng cách truyền 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`. ```typescript showLineNumbers async function loadPaywall() { try { const paywall: AdaptyPaywall = await adapty.getPaywall('YOUR_PLACEMENT_ID'); const products: AdaptyPaywallProduct[] = await adapty.getPaywallProducts(paywall); // Use products to build your custom paywall UI } catch (error) { // Handle the error } } ``` ## 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, 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. ```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 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. Phương thức này sẽ đồng bộ lịch sử mua hàng của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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 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. Để 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 ví dụ của chúng tô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 loading và quản lý trạng thái giao diện. Tiếp theo, [kiểm tra xem người dùng đã hoàn thành giao dịch mua chưa](react-native-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-react-native --- --- title: "Lấy thông tin paywalls và sản phẩm cho remote config paywalls trong React Native SDK" description: "Lấy thông tin paywalls và sản phẩm trong Adapty React Native 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à custom paywalls, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này liên quan đến remote config và custom paywalls. Để biết cách lấy paywalls cho các paywall được tùy chỉnh bằng Paywall Builder, hãy xem [Lấy paywalls 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 paywalls 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](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-reactnative) trong ứ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 lẫn Google Play. Các sản phẩm đa nền tảng này được tích hợp vào paywalls, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của mình bằng phương thức `getPaywall`. :::important **Đừng hardcode product ID.** ID duy nhất bạn nên hardcode là placement ID. Paywalls đượ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 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 địa hóa paywall](add-remote-config-locale). Tham số này là một 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ẻ đầu tiên là ngôn ngữ, thẻ 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 and locale codes](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ị 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 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 không tốt. 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 gỡ cài đặt ứng dụng hoặc xóa thủ công.</p><p></p><p>Adapty SDK lưu trữ paywalls theo 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 để lấy paywalls nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản 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> | | **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 `loadTimeout` đã chỉ định, 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ì paywalls được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và ư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 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 đó. Nhưng 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 code. Điều duy nhất bạn phải hardcode là placement ID. Tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://react-native.adapty.io/interfaces/adaptypaywall) với: danh sách product ID, đị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ó: ```typescript showLineNumbers try { // ...paywall const products = await adapty.getPaywallProducts(paywall); // the requested products list } catch (error) { // handle the error } ``` Tham số trả về: | Tham số | Mô tả | | :-------- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://react-native.adapty.io/interfaces/adaptypaywallproduct) với: đị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 này 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 để 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, 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, 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 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ệ liên quan, 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 cách lập trình, sử dụng `product.subscription?.subscriptionPeriod`. Từ đó bạn có thể truy cập thuộc tính `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.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ó loại `'free_trial'`.<br/>• `price`: Giá giảm dưới dạng số. Với dùng thử miễn phí, tìm giá trị `0` ở đây.<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 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 tương tự như mô tả trong phần trước dành cho ưu đãi.<br/>• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo locale của người dùng cho phần giảm giá. | ## Tăng tốc độ lấy paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywalls được lấy gần như ngay lập tức, 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à paywalls, và người dùng của bạn có kết nối internet yếu, việc lấy paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị 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 điều này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như được mô tả chi tiết trong phần [Lấy thông tin paywall](fetch-paywalls-and-products-react-native#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ể: - **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ế paywalls 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 paywalls 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 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 để được hưởng lợi từ tốc độ lấy 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 sử 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` có sẵn 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 | Đị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ột 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ẻ đầu tiên là ngôn ngữ, thẻ 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 and locale codes](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ị 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 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 không tốt. 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 gỡ cài đặt ứng dụng hoặc xóa thủ công.</p> | --- # File: present-remote-config-paywalls-react-native --- --- title: "Render paywall designed by remote config in 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." --- Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần render trong code của ứng dụng để hiển thị nó cho người dùng. Vì Remote Config mang lại sự 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, giúp bạn tự do hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## Lấy Remote Config của paywall và hiển thị nó \{#get-paywall-remote-config-and-present-it\} Để lấy Remote Config của một paywall, hãy truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: "YOUR_PLACEMENT_ID" }); const headerText = paywall.remoteConfig?.data?.["header_text"]; } catch (error) { // handle the error } ``` 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 đẹ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, 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 đả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 dữ liệu cho các 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 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](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ẽ được hiển thị cho người dùng khi không có kết nối internet hoặ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 hỗ trợ 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 có sự tham gia của bạn vì chỉ bạn mới biết khi nào người dùng thực sự 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ị 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). | --- # 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <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\} <Callout type="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. </Callout> 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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: 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ế Độ Trẻ Em trong React Native 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 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 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. ## Cần làm gì? \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách thận trọng. ID người dùng 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 dùng email. Đối với Chế Độ Trẻ Em, phương pháp tốt nhất là sử dụng các định danh được tạo 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ế Độ 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 việc thu thập địa chỉ IP. Để làm điều nà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 \{#updates-in-your-mobile-app-code\} Để tuân thủ các chính sách, hãy tắt việc thu thập IDFA (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 trong Android manifest \{#updates-in-your-android-manifest\} :::note Nếu ứng dụng của bạn chỉ dành cho 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 quá trình manifest merging. Việc đặt `adIdCollectionDisabled` chỉ ngăn Adapty thu thập ID, nhưng không xóa quyền mà một 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ử `<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." --- 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 React Native của mình. Bước đầu tiên trong quá trình này là lấy onboarding gắn 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). ## Lấy onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng builder 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 lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hay dữ liệu nhập vào 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 view 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 một onboarding, 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ể đượ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`. ::: ```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 } ``` 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 của bản dịch onboarding. Tham số này phải là mã ngôn ngữ gồm một hoặc hai subtag phân cách bằng 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 [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ừ 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 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 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 dù kết nối mạng không ổn định. Cache được cập nhật thường xuyên, vì vậy có thể dùng nó trong phiên để tránh các request mạng.</p><p></p><p>Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi 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ữ onboarding cục bộ trong 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 server 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 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> | | **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`, do thao tác có thể bao gồm nhiều request khác nhau bên dưới.</p> | Các tham số trả về: | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://react-native.adapty.io/interfaces/adaptyonboarding) gồ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 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ó kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn bạn 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ị onboarding nào. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement đã 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ả trong phần [Lấy onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc dùng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, đò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á nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", bỏ qua 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 tốc độ lấy nhanh hơn vượt trội hơn những hạn chế 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). ::: ```typescript showLineNumbers try { const placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const onboarding = await adapty.getOnboardingForDefaultAudience(placementId, locale); // the requested onboarding } catch (error) { // 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 của bản dịch onboarding. Tham số này phải là mã ngôn ngữ gồm một hoặc hai subtag phân cách bằng 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 [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ừ 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 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 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 dù kết nối mạng không ổn định. Cache được cập nhật thường xuyên, vì vậy có thể dùng nó trong phiên để tránh các request mạng.</p><p></p><p>Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi 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ữ onboarding cục bộ trong 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 server 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 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: 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." --- 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.registerEventHandlers(); // 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." --- 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 thức hiển thị bạn đang dùng: - **Modal presentation**: Yêu cầu thiết lập các event handler để xử lý sự kiện cho tất 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> Với React component, bạn xử lý sự kiện thông qua các prop event handler 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"> Với modal presentation, hãy triển khai phương thức event handler. :::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 handler đã được 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"> Với SDK phiên bản < 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ý 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ể 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** từ builder. Bạn có thể tạo ID riêng của mình, 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ấp để 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: <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 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ự quản lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ: bạn cần dừng hiển thị chính onboarding đó. ::: <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, 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ấp để 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ó ngay bên trong onboarding. Nếu bạn muốn mở paywall sau khi onboarding đó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 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. <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ị paywall chồng lên onboarding, bạn không thể điều khiển onboarding ở nền bằng code. Cố gắng dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn dismiss 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ị paywall chồng lên onboarding, bạn không thể điều khiển onboarding ở nền bằng code. Cố gắng dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn dismiss onboarding view 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ấp để 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: <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 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> Object `event` có thể là một trong các kiểu sau: |Kiểu | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải xong | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình hoàn 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 diện. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của onboarding flow | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | <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: 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." --- 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ụ: ```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ụ 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 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\} --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <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-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-06-24T14:36:38.818Z_ _Successfully processed: 44/44 files_ # TUTORIAL - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-06-24T14:36:38.820Z Total files: 265 --- # 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 trường hợp sử dụng của bạn không. Dù bạn đang ra mắt ứng dụng mới, tối ưu hóa doanh thu, hay migrate từ một công cụ khác — hãy bắt đầu từ đây." --- Adapty là nền tảng in-app purchase dành cho ứng dụng di động. Nền tảng này xử lý gói đăng ký, sản phẩm mua một lần và consumable — từ xử lý thanh toán và xác thực biên lai đến phân tích, A/B test và tích hợp. Dưới đây là cách Adapty hoạt động trong các tình huống khác nhau. ## 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 muố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ý thanh toán**: 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ả được quản lý sẵn cho bạn. - **Paywall Builder không cần code**: Thiết kế và triển khai paywall mà không cần viết code UI. - **Phân tích ngay từ ngày đầu**: Theo dõi doanh thu, trial, 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? Làm theo [hướng dẫn Quickstart](quickstart). ## Tôi muốn A/B test, phân tích 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**: Kiểm tra các mức giá, thiết kế paywall, thời gian trial và ưu đãi khác nhau để tìm ra điều gì chuyển đổi tốt nhất. Sử dụng [Growth Autopilot](autopilot) để nhận các đề xuất A/B test được cá nhân hóa cho ứng dụng của bạn, dựa trên dữ liệu từ hơn 20.000 ứng dụng gói đăng ký. - **Biểu đồ phân tích**: Theo dõi MRR, LTV, churn, retention và hàng chục chỉ số khác. - **Phân khúc đối tượng**: Nhắm mục tiêu 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 ứng dụng 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à đội nhóm của bạn đang dùng. Khám phá [A/B testing](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 LLM \{#i-want-to-implement-in-app-purchases-with-an-llm\} Tài liệu của Adapty được tối ưu để sử dụng với các trợ lý code AI như Cursor, Claude, ChatGPT và các công cụ khác. Mỗi trang đều có sẵn dạng Markdown thuần túy, và chúng tôi cung cấp hướng dẫn triển khai từng bước có sự hỗ trợ của LLM cho từng nền tảng: - **Hướng dẫn sẵn sàng copy-paste**: Gửi hướng dẫn cho LLM của bạn và để nó dẫn 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 **Copy for LLM** để lấy phiên bản văn bản thuần túy. - **Hỗ trợ Context7 MCP**: Kết nối tài liệu Adapty trực tiếp với IDE được hỗ trợ bởi LLM. 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, Apple Ads Manager của Adapty kết nối hiệu suất chiến dịch 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 đầu cuối**: Theo dõi toàn bộ chuỗi từ tìm kiếm đến cài đặt, đến trial, đến gói đăng ký và LTV. - **Dự đoán và đề xuất từ AI**: Dự báo lợi nhuận và nhận gợi ý mở rộng quy mô. - **Tự động hóa dựa trên quy tắc**: Duy trì ổn định các mục tiêu CPA và ROAS. Bắt đầu với [Apple Ads Manager](adapty-ads-manager). ## Tôi muốn theo dõi nguồn gốc người dùng của mình \{#i-want-to-track-where-my-users-come-from\} Adapty User Acquisition là giải pháp attribution tích hợp sẵn, kết nối chi tiêu quảng cáo với lượt cài đặt ứng dụng và doanh thu gói đăng ký: - **Dashboard marketing thống nhất**: Xem ROAS, lượt cài đặt và doanh thu trên tất cả các kênh của bạn ở một nơi. - **Attribution tích hợp**: Kết nối 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 link**: Tạo link trong Adapty và thêm vào chiến dịch của bạn để attribution chính xác. - **Deferred deeplink**: Điều hướng người dùng đến đúng nội dung sau khi cài đặt, ngay cả khi họ chưa có ứng dụng lúc nhấp vào. - **Phân tích cohort**: Phân tích hiệu suất thu hút người dùng và hành vi người dùng theo thời gian. Tìm hiểu thêm về [Adapty User Acquisition](adapty-user-acquisition). ## Tôi muốn cập nhật nhanh mà không cần phát hành ứng dụng \{#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 diễn ra trên dashboard — không cần phiên bản ứng dụng mới: - **Paywall Builder không cần code**: Thiết kế và cập nhật paywall trong trình chỉnh sửa trực quan, xuất bản thay đổi ngay lập tức. - **A/B testing 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. - **Dashboard analytics**: Theo dõi doanh thu, churn, trial 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 đội nhóm của bạn. Khám phá [Paywall Builder](adapty-paywall-builder) hoặc xem [Analytics](charts). ## Tôi bán hàng trên web và cần thêm ứ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 đồng bộ hóa các giao dịch mua trên các 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 mobile**: Người dùng đã thanh toán trên web sẽ có quyền truy cập trong ứng dụng của bạn, và ngược lại. - **Analytics đa nền tảng thống nhất**: Xem doanh thu web và mobile trên một dashboard. 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 đăng ký web và mobile](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 để migrate 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, sau đó mở rộng khi sẵn sàng. - **Nhập dữ liệu lịch sử**: Đưa lịch sử giao dịch hiện có của bạn vào Adapty để analytics luôn đầy đủ. Tìm hiểu về [migrate 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 đó. ::: 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 (khuyến nghị)**: 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ả natively, nên bạn không cần viết code UI. - **Paywall thủ công**: Bạn 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 những gì đã xây dựng vào một [placement](placements). Placement là nơi ứng dụng của bạn gọi khi chạy để lấy nội dung phù hợp cho đúng người dùng. <Tabs groupId="purchase-setup" queryString> <TabItem value="flow-builder" label="Dùng Flow Builder" default> :::important Flow Builder hiện chỉ hỗ trợ iOS 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 với 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 code. Adapty SDK render các flow natively trên từng nền tảng. Ứng dụng của bạn gọi `getFlow`, và SDK hiển thị các màn hình, xử lý giao dịch mua và báo cáo sự kiện. Không cần code UI riêng, không cần duy trì paywall song song. ## 1. Xây dựng flow \{#1-build-the-flow\} 1. Vào [**Flows**](https://app.adapty.io/flows) trong menu chính của Adapty. 2. Nhấp **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 template dưới đây sẽ đi qua các mẫu phổ biến nhất từng bước: <CustomDocCardList ids={['basic-paywall-screen', 'show-plans-bottom-sheet', 'paywall-with-tabs', 'paywall-features-per-product', 'onboarding-flow-tutorial']} /> Khi flow đã được lưu và xuất bản, hãy tiến hành gắn 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 không thể thêm nó 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 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 flow khi chạy. Hãy bắt đầu với placement quan trọng nhất — placement onboarding. Sau này, bạn có thể thêm nhiều [placement có ý nghĩa](choose-meaningful-placements) hơn trong 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 **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 xây dựng. 6. Nhấp **Save & publish**. Trong code ứng dụng, 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 nó 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 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 nhiều 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="manual-paywall" label="Triển khai paywall thủ cô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/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> Paywall là một container được cấu hình từ xa cho một hoặc nhiều sản phẩm. Adapty cung cấp danh sách sản phẩm và một payload JSON [remote config](customize-paywall-with-remote-config) tùy chọn — code ứng dụng của bạn đọc chúng và vẽ UI. :::tip Đang thiết lập Adapty theo cách 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 paywall \{#1-create-a-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. (Tùy chọn) Mở tab **Remote config** và thêm payload JSON mà ứng dụng của bạn 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 chi tiết. 6. Nhấp **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. <InlineTooltip tooltip="triển khai paywall thủ công">Làm theo 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 placement \{#2-add-the-paywall-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 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 khi chạy. Hãy bắt đầu với placement quan trọng nhất — placement onboarding. Sau này, bạn có thể thêm nhiều [placement có ý nghĩa](choose-meaningful-placements) hơn trong 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ấ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 paywall của placement. 5. Nhấp **Run paywall** và chọn paywall bạn vừa tạo. 6. Nhấp **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ứ khác — paywall nào chạy, sản phẩm nào nó bán, remote config — đều được cấu hình trong 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 paywall khác nhau cho nhiều 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 sản phẩm được nhúng trực tiếp. Adapty SDK render natively, nên bạn không cần viết code UI. :::warning Paywall Builder vẫn hoạt động đầy đủ, nhưng Adapty không còn thêm 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 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 placement \{#2-add-the-paywall-to-a-placement-1\} 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 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 khi chạy. 1. Vào [**Placements**](https://app.adapty.io/placements) trong menu chính của Adapty và chuyển sang tab **Paywalls**. 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 paywall của placement. 5. Nhấp **Run paywall** và chọn paywall bạn đã xây dựng. 6. Nhấp **Save & publish**. Trong code ứng dụng, bạn chỉ cần hardcode các placement ID. Mọi thứ khác — paywall nào chạy, sản phẩm nào nó 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 cứ 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. --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> --- # 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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 6 năm 2026 \{#june-2026\} - **A/B Test CPP trong Apple Ads Manager**: So sánh các trang sản phẩm tùy chỉnh với nhau ngay 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 traffic luân phiên 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 đến Adapty Mail trực tiếp từ server 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, 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 trình tạo 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ị theo cách native mà không cần web view, và bạn có thể cập nhật nội dung, thiết kế cũng như logic mà không cần phát hành bản cập nhật ứng dụng. Hiện hỗ trợ iOS SDK v4 trở lên. [Tìm hiểu thêm](adapty-flow-builder) - **Autopilot giờ đây thích nghi với kết quả test của bạn**: Hoạt động như một AI growth manager, Autopilot cập nhật kế hoạch tăng trưởng sau mỗi vòng hoàn thành. Giả thuyết tiếp theo được xây dựng dựa trên những thí nghiệm bạn đã chạy, những thí nghiệm nào đã thắng, và những hướng nào vẫn 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-autopilot-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: 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. ::: --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <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: 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 Flow Builder chính bao gồm tất cả các công cụ cần thiết để thêm các thành phần hiển thị, 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 giới thiệu từng khu vực của giao diện: chức năng và vị trí của từng thành phần. ## Đ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 flow. * **App name** App: Hiển thị tên ứng dụng mà flow thuộc về. * **All flows** Flows: Mở danh sách tất cả các flow của ứng dụng này. * **Rename the flow**: Nhấn vào biểu tượng bút chì Pencil bên cạnh tên flow để đổi tên. * **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ấn 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ấn **Save draft** để lưu tiến trình mà chưa xuất bản (⌘ / 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 một [placement](create-placement) sau khi đã xuất bản. ## Khu vực xem trước (ở giữa) \{#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 trên thiết bị di động. * Để chọn một thành phần và chỉnh sửa thuộc tính, nhấn vào nó. Để chọn một thành phần con bên trong một container, nhấn vào container trước, rồi nhấn vào thành phần con. * Để chỉnh sửa thuộc tính của chính màn hình đó, nhấn vào vùng trống bên ngoài tất cả các thành phần, hoặc chọn màn hình trong bảng Screens and Layers. * Để thay đổi thứ tự của một thành phần, kéo mục tương ứng 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 bố cục responsive. Do đó, bạn **không thể thay đổi vị trí các thành phần thủ công** — bạn chỉ có thể thay đổi thứ tự của chúng. Cài đặt bố cục của mỗi container quyết định cách các thành phần bên trong được phân bố. ::: ### Thanh màn hình đang hoạt động (phía trên bản xem trước thiết bị) \{#active-screen-bar-above-the-device-preview\} - **Screen name** — một pill hiển thị tên màn hình hiện tại. - **Toggle animations** Toggle animations — bật hoặc tắt bản xem trước animation của các thành phần; animation chạy liên tục cho đến khi tắt. Chỉ hiển thị khi màn hình đang hoạt động có ít nhất một [animation](builder-styling#animation). Không ảnh hưởng đến việc hiển thị animation trên thiết bị thực. - **Add element** Plus — mở [thư viện thành phần](builder-elements) ở màn hình hiện tại. Tương đương với nút **+** ở đầu bảng Screens and Layers — tiện dụng khi bảng đang thu gọn. ### Điều khiển chế độ xem (thanh công cụ dưới cùng) \{#view-controls-bottom-toolbar\} Các công cụ trong thanh công cụ dưới cùng cho phép bạn điều chỉnh bản xem trước. * **Device**: Chọn một trong các mẫu iPhone và Android có sẵn để thay đổi kích thước viewport và hình dạng thiết bị. * **Screen orientation**: Chuyển đổi giữa chế độ dọc Portrait và ngang Landscape để xem trước flow theo 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 ngôn ngữ/vùng để xem trước flow 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à các đường 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: [Screens and Layers](paywall-layout-and-products) ::: Khi không có thành phần 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 UI 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ư [quiz 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 thành phần \{#element-properties\} Khi bạn chọn một thành phần, bảng bên phải cho phép bạn thay đổi kiểu và 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: [Định vị thành phần](manage-paywall-ui-elements), [Tạo kiểu thành phần](builder-styling) ::: Tab **Design** cho phép bạn cấu hình giao diện và bố cục của thành phần được chọn: * **Visibility**: Hiển thị hoặc ẩn thành phần. Bật **Conditional** để đặt quy tắc xác định khi nào thành phần sẽ hiển thị. * **Position**: Chọn giữa các kiểu định vị Relative, Absolute hoặc Fixed. * **Content** (chỉ áp dụng cho thành phần văn bản): Chỉnh sửa nội dung văn bản của thành phần, chèn [biến](#variables) và quản lý bản địa hóa. * **Typography** (chỉ áp dụng cho thành phần 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 ngắn. * **Spacing**: Đặt margin và padding của thành phần. * **Effects**: Thêm đổ bóng ngoài, đổ bóng trong, làm mờ nền hoặc làm mờ layer. * **Animation**: Thêm các hiệu ứng animation (ví dụ: Pulse) và cấu hình thời gian và 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 thành phần con. #### Thuộc tính tương tác \{#interactions-properties\} :::link Tìm hiểu thêm: [Actions](onboarding-actions), [Điều hướng và tương tác](onboarding-navigation-branching) ::: Tab **Interactions** cho phép bạn xác định điều gì xảy ra khi người dùng tương tác với thành phần được chọn. Mỗi tương tác bao gồm một **trigger** và một hoặc nhiều **action**: * **Trigger** xác định *khi nào* điều gì đó xảy ra — ví dụ: **On Tap** (người dùng nhấn vào thành phần). * **Action** xác định *đ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 action vào một trigger để thực thi chúng theo thứ tự. Bạn có thể thêm nhiều trigger vào cùng một thành phần để thực thi nhiều action theo thứ tự. ## 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. ### Saved styles \{#saved-styles\} :::info Tìm hiểu thêm: - [Tạo kiểu thành phầ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 thành phần trong flow của bạn sử dụng cùng một kiểu chữ hoặc màu sắc, hãy lưu dữ liệu này dưới dạng kiểu toàn cục. Sau đó bạn có thể tái sử dụng nó chỉ với một cú nhấp. Hiện tại, Flow Builder hỗ trợ hai loại kiểu toàn cục — Font styles và Color styles. Mỗi Color style có thể tùy chọn có giá trị riêng cho chế độ tối. ### Variables \{#variables\} :::link Bài viết chính: [Variables](onboarding-variables) ::: Nút brackets Variables mở Variables. 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 thay thế các placeholder biến bằng giá trị thực — 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**: Các biến bạn tạo và kiểm soát thông qua các action. * **Elements**: Các 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 hoặc lựa chọn tab. 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 bảng này. Tham chiếu trực tiếp đến chúng khi chỉnh sửa thành phần văn bản. Sử dụng biến để: * **Bind text**: Hiển thị nội dung động thay vì chuỗi tĩnh. * **Control visibility**: Hiển thị hoặc ẩn các thành phần dựa trên điều kiện (ví dụ: ẩn nút nâng cấp cho người dùng premium). * **Interact with the user**: Truy cập dữ liệu từ các trường nhập liệu của người dùng, chẳng hạn như form hoặc quiz. ### Localization \{#localization\} :::link Bài viết chính: [Localization](add-paywall-locale-in-adapty-paywall-builder) ::: Chế độ xem Localization cho phép bạn quản lý tất cả nội dung có thể dịch trong flow của mình. Nó hiển thị một bảng gồm tất cả các chuỗi văn bản và hình ảnh, được tổ chức theo màn hình, với các cột cho từng ngôn ngữ/vùng. Từ chế độ xem này, bạn có thể: * Thêm ngôn ngữ/vùng 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ị các bản dịch còn thiếu. * Sử dụng **AI Translate** để tự động dịch nội dung, hoặc **Import/Export** bản dịch theo lô. --- # File: flow-builder-recipes --- --- title: "Common flow recipes" description: "Step-by-step guides for building common screen templates in the Flow Builder." --- This section walks through how to build the most common screen templates in the Flow Builder — element by element, from the layout choices to the interactions. Each guide is self-contained and uses the standard Flow Builder elements. <CustomDocCardList /> Follow this quickstart video to create a basic personalized flow: <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> --- # 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 nó ở cuối một [flow](adapty-flow-builder) gồm 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 ở chân trang để 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 các style tái sử dụng \{#1-set-up-reusable-styles\} Các style 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 cho mọi màn hình chỉ với một cú nhấp chuột. Mỗi flow mới đều đi kèm bộ text style 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 style cho các màu thương hiệu bạn sẽ dùng xuyên suốt màn hình. Xem hướng dẫn đầy đủ tại [Định dạng phần tử — Reusable styles](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 **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 chưa đáp ứng được nhu cầu của bạn. 3. Trên tab **Colors**, nhấp **Plus Create style** và thêm các màu bạn muốn tái sử dụng trong 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ò là 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 sau phân bổ đúng cách. Xem danh sách đầy đủ các thuộc tính màn hình tại [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. 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 rìa màn hình. 3. Trong **Layout**, đặt hướng thành **Vertical** Vertical và phân bổ thành **Space evenly**. 4. Trong **Fill**, chọn loại nền — màu đơn sắc, gradient, hoặc hình ảnh. Ví dụ này dùng **Gradient** Gradient với hai color stop. ## 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 nó với nút đóng \{#4-add-the-title-and-pair-it-with-the-close-button\} H1 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 horizontal container. Để thêm tiêu đề: 1. Nhấp **+** > **Text** > **H1**. 2. Khi H1 được chọn, mở tab **Design** trong bảng bên phải 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 mới. Để 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 gọn trên cùng một hàng. 2. Khi horizontal container được chọn, đặt căn chỉnh và phân bổ trong bảng bên phải để các phần tử thẳng hàng đúng cách. ## 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 \{#6-add-the-feature-list\} Danh sách tính năng làm nổi bật những gì có trong gói đăng ký khi mở khóa. Mỗi hàng có một biểu tượng, tên tính năng và mô tả ngắn. Xem đầy đủ các preset danh sách tại [Phần tử — List](builder-elements#list). Để thêm danh sách tính năng: 1. Nhấp **+** > **List** và chọn một list preset. Icon List là lựa chọn phổ biến nhất cho paywall. 2. Khi mỗi hàng được chọn, chỉnh sửa 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 \{#7-add-the-product-list\} Danh sách sản phẩm hiển thị các tùy chọn gói đăng ký để người dùng 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ẻ được tự động đánh dấu là mặc định. Để biết thêm về quản lý sản phẩm, xem [Thiết lập giao dịch mua](paywall-product-block). Để thêm và cấu hình sản phẩm: 1. Nhấp **+** > **Products** và chọn một layout 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ị tất cả sản phẩm đã được cấu hình trong Adapty Dashboard. 3. Để thay đổi sản phẩm được 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 nhãn giảm giá, mở rộng thẻ sản phẩm trong bảng **Layers**, chọn layer nhãn, và chỉnh sửa văn bản trong trường **Content**. Ẩn nhãn 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 nhãn. ## 8. Thêm nút mua hàng \{#8-add-the-purchase-button\} Nút mua hàng bắt đầu in-app purchase cho sản phẩm mà người dùng đã chọn. Biến `products.selectedProduct` luôn phân giải thành sản phẩm đang được chọn trên màn hình. Để thêm nút mua hàng: 1. Nhấp **+** > **Buttons** và chọn một button preset. 2. Khi nút được chọn, mở tab **Interactions** trong bảng bên phải. 3. Nhấp **Add trigger** > **On tap**, rồi nhấp **Add action**. 4. Đặt **Action** thành **Purchase** và **Product** thành `products.selectedProduct`. ## 9. Thêm các liên kết chân trang \{#9-add-footer-links\} Chân trang 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 (theo yêu cầu của các cửa hàng ứng dụng) và một nút để khôi phục giao dịch trước đó. Để 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 với 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ấp vào hành động và nhập URL đích. 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 nút **Restore Purchases**. Hành động của nó đã được cấu hình sẵn. :::tip Nếu vị trí của bất kỳ phần tử nào cảm thấy quá cao hoặc quá thấp, hoặc bạn muốn thêm khoảng trống ở 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à hình ảnh, nhưng màu đơn hoặc gradient cũng hoạt động tương tự. Để xem toàn bộ thuộc tính màn hình, xem [Màn hình và layers — 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 mép 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 khoảng trống 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 với nút đóng \{#3-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 ngang hai phần tử, hãy bọc chúng trong một horizontal container. Để thêm tiêu đề: 1. Nhấp **+** > **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**. Để nhóm tiêu đề với nút đóng: 1. Trong bảng **Layers**, nhấp menu ba chấm Context menu trên layer nút đóng và chọn **Wrap** > **Wrap in Horizontal Stack**. 2. Kéo layer H1 vào horizontal container mới. Để 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 gọn trên cùng một dòng. 2. Với horizontal container được chọn, đặt căn chỉnh và phân bố trong bảng bên phải để các phần tử thẳng hàng đúng cách. ## 4. Thêm tab và cấu hình nhãn tab \{#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ó container 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 [Phần tử — Tabs](builder-elements#tabs). Để tìm hiểu thêm về các nhóm có thể chọn, xem [Phần tử và nhóm có thể chọn](flow-selectable-elements). Để thêm tab: 1. Nhấp **+** > **Tabs** và chọn một preset — Segment control, Button Tabs, hoặc Underline. 2. Với tên mỗi tab được chọn trên canvas hoặc trong bảng **Layers**, chỉnh sửa trường **Content** trên 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\} Danh sách tính năng ngắn gọn bên trong tab đầu tiên cho người dùng biết gói đó bao gồm những gì. Để xem toàn bộ preset danh sách, xem [Phần tử — List](builder-elements#list). Để thêm danh sách tính năng: 1. Nhấp **+** > **List** và chọn một list preset. Icon List là dạng gọn nhất cho paywall. Phần tử xuất hiện ở cuối cây layer. 2. Với mỗi hàng được chọn, 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 \{#6-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ó. Để 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ấp **+** > **Products** và chọn một layout preset. 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 layer. 2. Chọn từng thẻ sản phẩm trên canvas và chọn sản phẩm từ menu dropdown trong tab **Design**. 3. Trong bảng **Layers**, kéo layer Products vào container **Content** của tab đầu tiên. ## 7. Thêm nút mua vào tab đầu tiên \{#7-add-the-purchase-button-to-the-first-tab\} Nút mua khởi động 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: 1. Nhấp **+** > **Buttons** và chọn một button preset. Phần tử xuất hiện ở cuối cây layer. 2. Với nút được chọn, mở tab **Design** và đặt con trỏ vào trường **Content**. Nhấp biểu tượng variable Variable icon, chọn `products.selectedProduct`, sau đó 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ấp **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 tab đầu tiên sang tab thứ hai \{#8-copy-the-first-tabs-content-into-the-second-tab\} Thay vì xây dựng lại cùng một 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 từ tab đầu tiên sang tab thứ hai. Bạn chỉ cần cập nhật các giá trị sau đó. Để 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, và dán bằng ⌘V / Ctrl+V. Các bản sao xuất hiện ở cuối cây layer. 3. Kéo từng phần tử đã sao chép vào container **Content** của tab thứ hai. ## 9. Cập nhật nội dung tab thứ hai \{#9-update-the-second-tabs-content\} Tab thứ hai giờ đây phản chiếu tab đầu tiên. 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 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 sản phẩm của gói thứ hai từ menu dropdown. Phần tử Products này tự động trở thành một nhóm có thể chọn riêng (`products2`). 3. Chọn nút mua trong tab thứ hai. Trong trường **Content** của tab **Design**, thay đổ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** của hành động **Purchase** từ `products.selectedProduct` thành `products2.selectedProduct`. ## 10. Thêm các liên kết footer chung \{#10-add-the-shared-footer-links\} Điều khoản sử dụng, chính sách quyền riêng tư và khôi phục mua hàng luôn hiển thị bất kể tab nào đang hoạt động. Thêm chúng ở cấp độ màn hình — bên ngoài cả hai container nội dung tab — để chúng được chia sẻ giữa các tab. Để thêm liên kết footer: 1. Nhấp **+** > **Buttons** > **Links**. Thao tác này thêm một hàng với Restore Purchases, Terms of Use và Privacy Policy ở cuối cây layer, đúng vị trí bạn muốn — ở gốc của màn hình, không lồng trong tab. 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 cho nút **Privacy Policy** với URL 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ị các 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 thuộc vào sản phẩm người dùng chọn, sử dụng chế độ hiển thị có điều kiện." --- Template này sử dụng chế độ 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ụ Pro và Pro+ — và danh sách tính năng tương ứng sẽ xuất hiện 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ị khi màn hình mới tải. ## 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 chứa mọi thứ bạn thêm vào. Trong ví dụ này, nền là một hình ảnh, nhưng màu đặc hoặc gradient cũng hoạt động tương tự. Để xem toàn bộ danh sách thuộc tính màn hình, 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. Dưới **System UI**, tắt **Safe area** để nền kéo dài đến các cạnh màn hình. 3. Dưới **Fill**, chọn loại nền và cấu hình nó. Ví dụ này sử dụng **Image** Image, nhưng màu đặc hoặc gradient cũng hoạt động tương tự. 4. Dưới **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 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 \{#3-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, bọc cả hai trong một container ngang. Để thêm tiêu đề: 1. Nhấp **+** > **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**. Để 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 ngang mới. Để 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 được chọn, đặt căn chỉnh và phân phối trong bảng bên phải để các phần tử xếp thẳng hàng. ## 4. Thêm danh sách sản phẩm \{#4-add-the-product-list\} Thêm các sản phẩm mà người dùng có thể 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 mới tải. Để 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 một preset 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ừ dropdown 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 \{#5-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. Để biết thêm về chế độ hiển thị có điều kiện, xem [Chế độ hiển thị có điều kiện](onboarding-element-visibility). :::tip Thay vì hai danh sách riêng biệt, bạn có thể thêm một danh sách duy nhất và đặt các phần tử văn bản bên trong nó theo điều kiện, để một danh sách thích ứng với sản phẩm được 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ấp **+** > **List** và chọn một preset danh sách nhỏ gọn. Icon List phù hợp 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. Với danh sách vẫn được chọn, mở tab **Design**. Dưới **Visibility**, chọn **Conditional** Conditional. 4. Thiết lập điều kiện để danh sách chỉ hiển thị khi sản phẩm đầu tiên đang được chọn. So sánh với biến `products.selectedProduct.prod_title`. Với **Value**, nhấp 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ẽ được giải thành 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 tiếp cận 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 hiển thị tại một thời điểm, dựa trên sản phẩm đang được chọn. Để thêm danh sách tính năng thứ hai: 1. Nhấp **+** > **List** và chọn cùng preset nhỏ gọn để đồng nhất về mặt hình thức. 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. Dưới **Visibility**, chọn **Conditional** Conditional và thiết lập cùng điều kiện 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 \{#7-add-the-purchase-button\} Nút mua hàng bắt đầu in-app purchase cho sản phẩm mà người dùng đã chọn. Nhãn của nó sử dụng 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 [Hành động — 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. Với 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 đủ được giải 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ấp **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 ở footer \{#8-add-the-footer-links\} Điều khoản sử dụng, chính sách bảo mật và khôi phục giao dịch nằm phía dưới nội dung chính. Để thêm các liên kết ở footer: 1. Nhấp **+** > **Buttons** > **Links**. Thao tác này thêm một hàng có Restore Purchases, Terms of Use và Privacy Policy ở 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** và URL chính sách bảo mậ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: onboarding-flow-tutorial --- --- title: "Xây dựng onboarding cá nhân hóa" description: "Hướng dẫn toàn bộ quá trình xây dựng onboarding nhiều màn hình — thiết kế màn hình, nội dung, điều hướng và phân nhánh có điều kiện — thông qua 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 bằng các hành động điều hướng. Flow có thể chạy tuyến tính, hoặc phân nhánh dựa trên dữ liệu đầu vào của người dùng thu thập ở màn hình trước. Hướng dẫn này đi qua toàn bộ quy trình — tạo màn hình, xây dựng nội dung, kết nối điều hướng và thêm phân nhánh có điều kiện — sử dụng onboarding bốn màn hình làm ví dụ minh họa. Ví dụ sử dụng: - **Nhập tên** để lưu tên người dùng thành biến dùng cho cá nhân hóa. - **Quiz chọn một đáp á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** với nội dung được tùy chỉnh riêng cho từng nhóm đối tượng. - **Paywall** là màn hình cuối cùng. Mẫu này áp dụng cho bất kỳ flow nào cá nhân hóa nội dung dựa trên dữ liệu người dùng nhập vào. Bạn thích xem video? Hướng dẫn quickstart này đi qua cùng một 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 style dùng lại \{#1-set-up-reusable-styles\} Style dùng lại cho phé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ó biến thể Light và Dark, để flow tự động hỗ trợ cả hai giao diện. Để xem hướng dẫn đầy đủ, xem [Tùy chỉnh element — Style dùng lại](builder-styling#reusable-styles). Để thiết lập style: 1. Trong bảng bên trái, mở bảng **Styles** Styles. 2. Ở tab **Colors**, nhấp **Plus Create style** và thêm các màu bạn sẽ dùng lại. Với mỗi màu, chọn giá trị Light, chuyển sang tab Dark và chọn giá trị Dark. 3. Ở tab **Text**, nhấp vào một style hiện có để chỉnh font, độ đậm và kích thước, hoặc nhấp **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. Cấu hình màn hình đầu tiên với nền tảng chung — bố cục, nền và safe area — sau đó nhân bản nó cho các màn hình còn lại. Như vậy, mọi màn hình đều có cùng 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à Layer — 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ấp 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 element căn theo cạnh có thể kéo dài đến mép màn hình. 3. Trong **Fill**, chọn loại nền và cấu hình nó — ví dụ: **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 cách 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 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 có bốn màn hình — nhánh thứ hai sẽ được thêm sau bằng cách nhân bản từ nhánh đầu tiên. 6. Đổi tên mỗi màn hình theo vai trò của nó — trong ví dụ: `Welcome`, `Quiz`, `Rock path` và `Paywall`. ## 3. Xây dựng màn hình giới thiệu \{#3-build-the-introduction-screen\} Màn hình đầu tiên thường tạo ấn tượng ban đầu — tiêu đề, danh sách tính năng và lời kêu gọi hành động dẫn vào phần còn lại của flow. Trong ví dụ, đây là màn hình Welcome. Nhấp màn hình **Welcome** trong bảng **Screens**, sau đó thêm các element: 1. Thêm ảnh chính. Nhấp **+** > **Media** > **Image**, tải ảnh lên và điều chỉnh margin nếu cần. 2. Thêm tiêu đề: nhấp **+** > **Text**, chọn style heading từ các text style đã lưu, và chỉnh 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 mỗi card. 4. Thêm nút điều hướng chính ở phía dưới. Nút này sẽ được gán hành động ở bước điều hướng. ## 4. Xây dựng màn hình nhập liệu và quiz \{#4-build-the-input-and-quiz-screen\} Màn hình thứ hai thu thập dữ liệu từ người dùng. Trong ví dụ, nó hỏi tên và một câu trả lời chọn một quyết định người dùng sẽ thấy nhánh nào tiếp theo. Để tìm hiểu thêm về nhập liệu và quiz, xem [Nhập liệu và form](builder-inputs-and-forms) và [Khảo sát và quiz](onboarding-quizzes). Nhấp màn hình **Quiz** trong bảng **Screens**, sau đó thêm các element. Mỗi nhóm trên màn hình — intro, câu hỏi + nhập liệu, câu hỏi + quiz — nằm trong Vertical Container riêng để các element liên quan gắn kết trực quan. 1. Thêm tiêu đề và nội dung intro. Nhấp **+** > **Text** > **H1** cho tiêu đề và **+** > **Text** > **Body** cho phần nội dung hỗ trợ. 2. Nhóm phần intro. Nhấp **+** > **Layout** > **Vertical Container**, kéo container mới lên đầu cây layer, sau đó 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ấp **+** > **Text** cho caption câu hỏi, sau đó nhấp **+** > **Inputs** > **Text** cho trường nhập. 4. Đặt **Element ID** của trường nhập trong tab **Design** — trong ví dụ là `name`. Điều này giúp giá trị được lưu thành biến mà các màn hình khác có thể tham chiếu. 5. Nhóm caption và trường nhập trong Vertical Container tương tự như phần intro. 6. Thêm câu hỏi thứ hai và quiz. Nhấp **+** > **Text** cho caption, sau đó nhấp **+** > **Quiz** và chọn preset bố cục như Icon Options. Cấu hình các lựa chọn — trong ví dụ là `Rock` và `Hip hop`. 7. Nhóm caption và quiz trong Vertical Container tương tự. 8. Đặt ID cho các lựa chọn. Chọn từng lựa chọn quiz, mở tab **Interactions**, và đặt **Element ID** của nó. Các ID này được tham chiếu trong điều hướng có điều kiện sau này. 9. Chuyển quiz sang chọn một: nhấp vùng trống trên canvas để mở **Screen settings**, kéo xuống **Selectable Groups**, nhấp tên nhóm quiz, và đặt loại thành **Single choice**. 10. Thêm nút chính ở phía dưới — đây là nút Next kích hoạt phân nhánh. ## 5. Xây dựng nhánh đầu tiên \{#5-build-the-first-branching-path\} Mỗi màn hình nhánh tùy chỉnh nội dung cho một nhóm đối tượng. Trong ví dụ, nhánh Rock có nội dung tập trung vào rock — playlist, nghệ sĩ và 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 màn hình **Rock path**. 2. Thêm tiêu đề. Đặt con trỏ vào trường **Content** ở vị trí muốn cá nhân hóa, nhấp icon biến Variable icon, và mở tab **Elements**. Chọn màn hình chứa trường nhập — trong ví dụ là **Quiz** — sau đó chọn biến giá trị của trường nhập. Bộ chọn sẽ tự động phân giải thành `<elementId>.value` — trong ví dụ là `name.value`. Khi chạy, tiêu đề sẽ hiển thị nội dung người dùng đã nhập. 3. Thêm nội dung body dưới dạng các element text bổ sung, được điều chỉnh cho nhóm đối tượng của nhánh này. 4. Thêm nút chính ở phía dưới. ## 6. Xây dựng nhánh thứ hai \{#6-build-the-second-branching-path\} Các màn hình nhánh thường có cùng bố cục — chỉ thay đổi nội dung. Nhân bản màn hình nhánh đầu tiên và cập nhật nội dung. Để nhân bản và cập nhật: 1. Trong bảng **Screens**, chọn màn hình nhánh đầu tiên và nhấn ⌘D / Ctrl+D để nhân bản. Bản sao xuất hiện ở cuối danh sách màn hình. 2. Đổi tên bản sao — trong ví dụ là `Hip hop path` — và kéo nó đến đúng vị trí trong danh sách màn hình, để nó nằm cạnh màn hình gốc. 3. Cập nhật nội dung body cho nhóm đối tượng kia. Tiêu đề cá nhân hóa vẫn hoạt động — biến được giữ nguyên. ## 7. Xây dựng paywall \{#7-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 hướng dẫn đó. Nhấp màn hình **Paywall** trong bảng **Screens**, sau đó thêm các element: 1. Thêm **Horizontal Container** ở trên cùng và thêm nút **Close** vào bên trong. Preset Close đã được cấu hình sẵn. 2. Thêm ảnh chính, tiêu đề (dùng cùng biến cá nhân hóa như trên các màn hình nhánh), và subheading làm nội dung hỗ trợ. 3. Thêm sản phẩm: nhấp **+** > **Products** và chọn **Vertical List**. Gán từng card một sản phẩm từ dropdown trong tab **Design**. 4. Nhấp card của sản phẩm mặc định và bật **Set as default product** để nó được chọn sẵn khi màn hình tải. 5. Thêm nút mua. Nhấp **+** > **Buttons** và chọn preset. Trong tab **Interactions**, nhấp **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. Nó bao gồm ba link ở footer: Restore Purchases, Terms of Use và Privacy Policy. Link Restore đã được cấu hình sẵn. Để cấu hình các link còn lại, chọn element nút, mở tab **Interactions**, và đặt đích cho hành động **Open URL**. ## 8. Kết nối điều hướng giữa các màn hình \{#8-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à hành động **Navigate to** để kết nối nút chính của mỗi 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 sẽ dùng **Conditional action** thay thế. Để tìm hiểu thêm về điều hướng và hành động có điều kiện, xem [Điều hướng và tương tác](onboarding-navigation-branching) và [Hành động — Hành động có điều kiện](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, và 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ụ 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 lựa chọn — trong ví dụ là lựa chọn Rock. - **IF** điều kiện khớp, kích hoạt **Navigate to** và chọn màn hình nhánh đầu tiên. - **ELSE**, kích hoạt **Navigate to** và chọn màn hình nhánh thứ hai. 3. **Điều hướng tĩnh từ mỗi nhánh đến paywall.** Lặp lại mẫu 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](builder-save-publish). - [Thêm flow vào placement](create-placement) để bắt đầu hiển thị cho người dùng. - Để có flow riêng cho từng đối tượng (thay vì phân nhánh trong flow), hãy tạo phân khúc đối tượng và gán các flow khác nhau trên trang Placement. --- # 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 flow Adapty duy nhất — 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 app cũ." --- Trong Adapty, một *flow* kết hợp onboarding và paywall thành một thực thể duy nhất dưới một placement. Flow thay thế onboarding và paywall riêng lẻ mà bạn đang 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 flows 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 app cũ hơn. :::important Flows hiện chỉ được hỗ trợ trên iOS SDK v4 trở lên. Hỗ trợ cho các nền tảng và framework khác sẽ sớm ra mắt. ::: ## Flows so với onboardings và paywalls \{#flows-vs-onboardings-and-paywalls\} Với onboardings và paywalls riêng lẻ, bạn phải duy trì hai builder và hai placement. Bạn cũng phải tự xử lý việc chuyển người dùng từ onboarding sang paywall trong code của mình. Một flow thay thế cả hai bằng một trải nghiệm duy nhất — màn hình giới thiệu, bài 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. Bảng dưới đây so sánh những gì mỗi tùy chọn cung cấp: | | Flow | Paywall Builder paywall | Onboarding | |---|---|---|---| | Nhiều màn hình | Có | Không — màn hình đơn | 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 của riêng nó — để 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 migrate \{#how-to-migrate\} Quá trình migration gồm bốn bước. Phần lớn công việc là nâng cấp SDK một lần — 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 một flow trong editor không cần code; không cần developer. 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 app Adapty dành cho di động; không cần build app. 3. **[Tạo một placement mới cho flow của bạn](#create-a-new-placement-for-your-flow)**: Tạo một flow placement mới với ID duy nhất riêng, và quyết định cách nó cùng tồn tại với các placement hiện tại của bạn. 4. **[Cập nhật SDK](#update-the-sdk)**: Nâng cấp lên iOS SDK v4, lấy flow từ placement của nó và xác minh một giao dịch mua sandbox. Đây là nhiệm vụ chính của developer. ### Xây dựng flow của bạn \{#build-your-flow\} Trên trang **Flows**, nhấp **Create flow** để bắt đầu xây dựng, tái tạo onboarding và paywall của bạn thành một trải nghiệm duy nhất. Để 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 công thức flow phổ biến](flow-builder-recipes)**: Hướng dẫn từng bước cho các màn hình phổ biến nhất. - **Hỏi AI**: Sử dụng chat trên bất kỳ trang tài liệu nào khi bạn gặp khó khăn. :::note Việc xây dựng flow từ template flow có sẵn hoặc tạo flow bằng AI chưa khả dụng — cả hai sẽ sớm ra mắt. Hiện tại, mọi flow mới đều 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ực mà không cần chỉnh sửa app. Tải xuống [app Adapty](https://apps.apple.com/us/app/adapty/id6739359219) từ App Store. Sau đó, trong flow builder, nhấp **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 hiển thị các màn hình thực tế, phân nhánh, nội dung và thiết kế. :::note Ở chế độ xem trước, Adapty không thể kết nối với sản phẩm của bạn trong các cửa hàng, vì vậy giá hiển thị trong bản xem trước không phải là 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 — flow, paywall hoặc onboarding. Bạn không thể chuyển đổi một placement onboarding hoặc paywall hiện có thành flow placement (xem [loại placement](create-placement)). Một flow cần placement mới của riêng nó. **Đặt cho flow placement 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 hoặc onboarding nào hiện có. :::warning Giữ các placement cũ hoạt động trong thời gian chuyển tiếp Người dùng trên các phiên bản app cũ hơn đã có placement ID của onboarding và paywall được biên dịch vào app. Họ tiếp tục gọi các phương thức onboarding và paywall và xem onboarding và paywall hiện tại của bạn cho đến khi họ cập nhật. Chỉ ngừng sử dụng các placement cũ khi tỷ lệ áp dụng SDK v4 của bạn đủ cao. ::: Bạn không cần phải chuyển tất cả các vị trí sang flows cùng một lúc. Trong iOS SDK v4, phương thức `getFlow` lấy dữ liệu từ cả flow placement và paywall placement, vì vậy app của bạn gọi cùng một phương thức ở mọi nơi. Giữ các paywall Paywall Builder trong các placement mà bạn muốn, và sử dụng flows ở phần còn lại. Trong thời gian chuyển tiếp, mỗi loại placement theo dõi chỉ số của riêng mình. Trong khi cả phiên bản app cũ và mới đều đang hoạt động, dữ liệu của bạn sẽ phân tán trên hai bộ placement. Các placement onboarding và paywall cũ phủ các phiên bản cũ hơn; flow placement mới phủ SDK v4+. So sánh chúng như các cohort riêng biệt, và dự kiến tỷ lệ của flow placement sẽ tăng khi người dùng cập nhật. Bạn có thể tiếp tục A/B test với flows: chạy [A/B test thông thường](ab-tests) trên các biến thể flow trong một flow placement. A/B test đa placement hiện chỉ khả dụng cho paywalls, vì vậy bạn chưa thể chạy loại này trên các flow placement. Việc so sánh một flow mới với paywall cũ của bạn là so sánh cohort, không phải một bài test duy nhất — chúng thuộc các loại placement khác nhau. ### Cập nhật SDK \{#update-the-sdk\} Khi flow placement của bạn đã sẵn sàng, hãy trỏ app vào nó. Flows chỉ hiển thị trên Adapty SDK v4 trở lên. Nâng cấp SDK và lấy flow từ placement mới của bạn bằng `getFlow(placementId:)`. Xem [hướng dẫn migration iOS SDK v4](migration-to-ios-sdk-v4) để biết các bước nâng cấp cụ thể. Sau khi flow đã được kết nối, hãy xác minh nó như bất kỳ flow mua hàng nào khác: chạy nó trên thiết bị hoặc simulator và thực hiện [giao dịch mua sandbox](ios-test) để 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 sẽ thấy flows chỉ sau khi họ cài đặt app được xây dựng với SDK v4+. Bất kỳ ai đang dùng phiên bản app cũ hơn vẫn tiếp tục nhận onboarding và paywall hiện tại của bạn, đó là lý do tại sao các placement cũ phải tiếp tục hoạt động trong thời gian chuyển tiếp. Điều tương tự áp dụng cho các nền tảng chưa hỗ trợ flows. ::: --- # File: paywall-layout-and-products --- --- title: Màn hình và layer description: "Quản lý màn hình và phân cấp phần tử trong từng màn hình trong Flow Builder." --- Một flow 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 bài quiz, hoặc một slide giới thiệu sản phẩm. Các phần tử trên mỗi màn hình được tổ chức theo phân cấp layer. Để quản lý màn hình, layer và các phần tử, hãy mở giao diện mặc định **Screens and Layers**. Giao diện này hiển thị 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ủa bảng bên trái liệt kê tất cả màn hình trong flow. Mỗi mục hiển thị nhãn có số thứ tự 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 hiển thị màn hình được chọn, và phần Layers bên dưới cập nhật để hiển thị phân cấp layer của màn hình đó. * **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. * **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 sử dụng, bạn sẽ không thể xuất bản. Hãy xóa tất cả màn hình nháp trước khi xuất bản. ::: ### Thao tác 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. | Thao tác | 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ột 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 | :::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 đầy đủ: [Vị trí phần tử](manage-paywall-ui-elements) ::: Phần **Layout** xác định cách màn hình phân bổ các phần tử con. Các thuộc tính này có sẵn trên bất kỳ phần tử container nào. * **Free**: Các phần tử con được định vị độc lập. * **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. 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í phần tử theo trục chéo. * **Gap**: Khoảng cách giữa các phần tử liền kề. * **Distribution**: Phân bổ khoảng trắng giữa và xung quanh các phần tử con. #### Bố cục RTL \{#rtl-layout\} Bật checkbox **Mirror for RTL** để đảo ngược bố cục cho các hệ thống chữ viết từ phải sang trái. Thứ tự các phần tử trong container ngang sẽ bị lật. ### 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, gradient, hình ảnh, hoặc video. Nền lấp đầy toàn bộ viewport của thiết bị, kể cả vùng phía sau notch và thanh hệ thống — ngay cả khi **Safe area** đã được bật. #### Lặp video nền \{#loop-background-video\} Bật toggle **Loop** để phát video nền theo vòng lặp liên tục. #### Gán ID media tùy chỉnh \{#assign-a-custom-media-id\} Cũ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 ID media tùy chỉnh để 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: [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), [trial toggle](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ấp 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 nó. ## 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ư 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 *khi* 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ấp 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 phần tử bạn thêm qua giao diện [Add element](builder-elements) đều xuất hiện dưới dạng layer mới trong cây. * **Chọn layer**: Nhấp vào một layer để chọn nó. Trình chỉnh sửa trực quan tô sáng phần tử tương ứng trên canvas, và bảng bên phải hiển thị 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 layer**: Kéo và thả layer trong cây để thay đổi thứ tự của chúng 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 thị hoặc ẩn layer**: Di chuột qua một layer để hiện biểu tượng mắt Eye ở bên phải. Nhấp vào đó để bật/tắt hiển thị layer. Các layer ẩn vẫn còn trong cây nhưng không hiển thị trong trình chỉnh sửa trực quan hay trên thiết bị. Để kiểm soát hiển thị bằng logic lúc runtime, hãy dùng [điều kiện hiển thị](onboarding-element-visibility). * **Thu gọn tất 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. ### Thao tác với layer \{#layer-actions\} Nhấp vào biểu tượng ba chấm Context để mở menu ngữ cảnh. | Thao tác | Phím tắt | Mô tả | |--------|----------|-------------| | **Copy** | ⌘C / Ctrl+C | Sao chép layer vào clipboard | | **Paste here** | ⌘V / Ctrl+V | Dán một layer đã sao chép trước đó làm layer 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 dùng nội dung hoặc loại thành phần làm tên | | **Delete** | ⌘⌫ / Ctrl+Del | Xóa layer và tất cả layer 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** | | Xóa container bọc ngoài và chuyển các layer 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: 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 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. ## Thêm sản phẩm \{#add-products\} Phần tử sản phẩm là một thẻ hiển thị sản phẩm trên canvas. Để thêm phần tử sản phẩm: 1. Trên canvas, nhấn **+** trên màn hình đích. 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 cho nó trong dropdown ở bảng **Design**. :::important Phần tử sản phẩm chưa được gán sản phẩm sẽ [chặn tính năng 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. ::: :::note Bạn cũng có thể gắn hành động **Purchase** trực tiếp vào tương tác **On tap** của thẻ sản phẩm. Khi nhấn vào thẻ, thao tác mua hàng sẽ được kích hoạt mà không cần thêm nút mua hàng riêng. ::: :::important Nếu bạn xóa một nhóm sản phẩm và thay bằng nhóm mới, hãy kiểm tra lại tất cả các hành động và biến để đảm bảo chúng trỏ đúng đến nhóm mới. Các tham chiếu còn trỏ đến nhóm đã xóa sẽ [chặn tính năng 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 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 **+** trên màn hình. 2. Chọn **Button** và chọn kiểu nút. 3. Khi 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**, sau đó đặt **Product** thành một trong các tùy chọn: - `products.selectedProduct`: Mua sản phẩm mà người dùng đang chọn 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ể người dùng chọn gì 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 đang được chọn vào nhãn nút, sử dụng biến: 1. Khi nút được chọn, 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ấn 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ụ: `Đăng ký với giá {prod_price}`. Nhãn sẽ cập nhật 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\} Để cho phép người dùng 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 phần tử khôi phục giao dịch mua: 1. Trên canvas, nhấn **+** trên màn hình. 2. Chọn **Button**, rồi chọn **Links** cho liên kết văn bản hoặc kiểu nút khác cho nút có kiểu dáng. 3. Khi phần tử được 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. Từ 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ó sản phẩm, bạn có thể hiển thị hoặc ẩn các phần tử khác tùy theo sản phẩ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ấn **Add trigger**. 3. Chọn **On tap** và nhấn **Add action**. 4. Từ dropdown **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: manage-paywall-ui-elements --- --- title: "Vị trí phần tử" description: "Sắp xếp các phần tử trên màn hình với layout, 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 phần tử đến 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 (dọc hay 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ự điều chỉnh kích thước và margin, hoặc — khi cần — thoát khỏi flow bằng vị trí tuyệt đối (absolute) hoặc cố định (fixed). :::link Về các thuộc tính hiển thị như fill, đường viền và hiệu ứng, xem [Tạo kiểu phần tử](builder-styling). ::: ## Layout \{#layout\} Layout 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 quy tắc — hướng, căn chỉnh và khoảng cách. Các phần tử layout có trong builder 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 overlay trượt lên, hiển thị thêm nội dung khi người dùng nhấn vào nút Container là các khối xây dựng chính của một màn hình. Bạn có thể lồng chúng vào nhau để tạo ra các layout phức tạp. Mỗi container có phần **Layout** trong bảng bên phải để điều khiển 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 [thao tác 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 Về chi tiết cấu trúc màn hình và layer, xem [Màn hình và Layer](paywall-layout-and-products). ::: ### Hướng \{#direction\} * Free **Free**: Không có layout tự động. Các phần tử con được định vị độc lập (hữu ích khi dùng vị trí tuyệt đối) * Vertical **Vertical**: Các phần tử con xếp 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 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 hiển thị theo thứ tự xuất hiện trong bảng **Layers**. Trong container dọc, mục ở trên cùng trong danh sách xuất hiện ở đầu màn hình. Trong container ngang, mục ở trên cùng xuất hiện 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 dùng [thao tác layer](paywall-layout-and-products#layer-actions) **Move Up** và **Move Down**. ### Căn chỉnh \{#alignment\} Lưới căn chỉnh kiểm soát vị trí của các phần tử con dọc theo trục cross của container. Trong container dọc, căn chỉnh điều khiển vị trí ngang của các phần tử con (trái, giữa, hoặc phải). Trong container ngang, nó điều khiển vị trí dọc (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): Giá trị pixel cố định giữa các phần tử con liền kề * **Space Between**: Các phần tử con trải ra đến các cạnh; khoảng cách bằng nhau xuất hiện giữa chúng * **Space Around**: Khoảng cách bằng nhau bao quanh mỗi phần tử con, với khoảng cách nửa kích thước ở các cạnh * **Space Evenly**: Khoảng cách 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 để cho phép tràn nội dung (ví dụ: một badge cố tình vượt ra ngoài cạnh 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 layout của container chứa nó. Nút bật/tắt **Position** cho phép bạn thoát khỏi flow thông thường và định vị thủ công. ### Relative (mặc định) \{#relative-default\} Phần tử ở trong flow layout thông thường. Vị trí của nó được xác định tự động theo quy tắc layout của container cha — bạn không thể kéo tự do. Dùng **Margin** để điều chỉnh khoảng cách xung quanh phần tử relative. Dùng vị trí relative cho phần lớn nội dung: khối văn bản, hình ảnh, card, nút và mục danh sách. ### Absolute \{#absolute\} Phần tử thoát khỏi flow thông thường và phủ lên các nội dung khác. Nó không còn ảnh hưởng đến layout 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 mỗi 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): 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ử anh em. Giá trị cao hơn sẽ hiển thị ở trên cùng Dùng vị trí absolute cho các overlay trang trí, badge, nút đóng và icon đặt trên hình ảnh. :::tip Để kéo giãn một phần tử absolute theo toàn bộ chiều rộng của container cha, đặt anchor ngang thành **Left**, sau đó thêm offset **Right** bằng 0. Phần tử sẽ ghim vào cả hai cạnh. ::: ### Fixed \{#fixed\} Phần tử bỏ qua hoàn toàn container cha và ghim vào viewport của thiết bị. Nó vẫn hiển thị khi người dùng cuộn — nội dung trang di chuyển bên dưới nó. Vị trí fixed dùng các điều khiển tương tự Absolute (offset, lưới anchor, Z-index). Tất cả offset đều tính từ cạnh màn hình thay vì từ container cha. Dùng vị trí fixed cho các nút sticky ở dưới cùng, thanh điều hướng nổi và header cố định. ## Kích thước \{#sizing\} Mỗi phần tử có điều khiển **Width** và **Height**. Nhấp vào dropdown để chọn chế độ kích thước: * **Fill**: Phần tử kéo giãn để chiếm toàn bộ không gian có sẵn trong container cha. Giá trị pixel hiển thị là kết quả được tính toán. * **Hug**: Phần tử co lại để vừa với nội dung của nó. Giá trị pixel hiển thị là kết quả được tính toán. * **Fixed**: Phần tử sử dụng chính xác giá trị pixel bạn chỉ định, bất kể kích thước cha hay nội dung. Chế độ duy nhất có thể dùng cho các phần tử với vị trí absolute hoặc fixed. ## Khoảng cách \{#spacing\} Đặt giá trị khoảng cách độc lập cho mỗi 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 phần tử và nội dung của nó. Phần tử văn bản chỉ có margin. Màn hình chỉ có padding. Cả hai đều có sẵn cho 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ự. Sự chồng lấp chỉ xảy ra khi một phần tử thoát khỏi flow 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ử anh em xuất hiện sau trong bảng **Layers** sẽ hiển thị trên các phần tử xuất hiện trước — kể cả khi phần tử sau là relative và 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ẽ thắng. Các phần tử relative không có Z-index — chỉ thứ tự layer xác định vị trí xếp chồng của chúng. Dùng [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: "Định dạng phần tử" 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à các style toàn dự án." --- Tab **Design** ở bảng bên phải kiểm soát giao diện 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ó các tùy chọn định dạng chung. ## Kích thước và khoảng cách \{#size-and-space\} :::link Bài viết chính: [Vị trí phần tử](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 bị ẩn. Dùng tính năng này để tạm thời loại bỏ phần tử khỏi flow mà không cần xóa nó. ### Kích thước \{#sizing\} Mỗi phần tử có các điều khiển **Width** và **Height** với ba chế độ: * **Fill**: Kéo giãn để chiếm toàn bộ không gian có sẵn trong phần tử cha * **Hug**: Thu nhỏ để vừa khít nội dung của phần tử * **Fixed**: Đặt giá trị pixel chính xác cho kích thước ### Khoảng trống \{#spacing\} Mục **Spacing** hiển thị mô hình hộp với hai lớp: * **Margin**: Khoảng cách giữa phần tử và các phần tử lân cận. Không mở rộng ra ngoài ranh giới của vùng chứa cha, bất kể giá trị là bao nhiêu. * **Padding**: Khoảng cách giữa viền của phần tử và nội dung bên trong. Đặt giá trị cho từng cạnh riêng lẻ. :::note Phần tử văn bản chỉ có margin. Màn hình chỉ có padding. Cả hai đều có sẵn cho các vùng chứa và các phần tử khác có nội dung con. ::: ## Màu nền \{#fill\} Mục **Fill** kiểm soát nền của phần tử. Có bốn loại fill: màu đơn sắc, gradient, hình ảnh và video. Dùng thuộc tính này để đặt hình ảnh / video hero 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 gán một [color style 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 fill gradient với hai hoặc nhiều điểm màu. Kéo các điểm để điều chỉnh chuyển màu, và thay đổi 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ử. ## Viền \{#border\} Viền mặc định bị tắt. Nhấp vào Plus bên cạnh **Border** trong bảng bên phải để thêm viền. Để xóa viền, nhấp vào Close bên cạnh tiêu đề **Border**. Khi có viền, hãy cấu hình: * **Color**: Dùng bộ chọn màu, nhập giá trị hex, hoặc gán một [color style toàn dự án](#color-styles). Điều chỉnh **opacity** để làm viền trong suốt một phần. * **Width**: Độ dày 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ấp vào nút dấu cộng Plus bên cạnh **Effects** để thêm một hoặc nhiều hiệu ứng trực quan: * **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 * **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ử. Bật/tắt hiển thị Show để tạm thời vô hiệu hóa một hiệu ứng. ## Hoạt ảnh \{#animation\} Nhấp vào nút Plus bên cạnh **Animation** để thêm hiệu ứng hoạt ảnh. Hiện tại, **Pulse** là hoạt ảnh duy nhất có sẵn — phần tử sẽ nhịp nhàng phóng to và thu nhỏ để thu hút sự chú ý. Cấu hình hoạt ảnh Pulse với các tham số sau: | Tham số | Mô tả | |-----------|-------------| | Scale amount (%) | Phần tử phóng to bao nhiêu so với kích thước gốc | | Duration (ms) | Độ dài của một chu kỳ hoạt ảnh | | 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 đổ khi pulse | | Shadow size (px) | Kích thước của bóng đổ khi pulse | ### 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: ### Font chữ \{#font\} :::link Xem thêm: [Font chữ tùy chỉnh](using-custom-fonts-in-paywall-builder) ::: Nhấp vào dropdown font chữ Font select để mở bộ chọn font. Nó có hai tab: * **Styles**: Liệt kê các [text style](#text-styles) đã lưu của dự án bạn. Chọn một style để áp dụng toàn bộ cài đặt kiểu chữ của nó cùng một lúc. * **Fonts**: Liệt kê tất cả các họ font có sẵn. Tìm kiếm hoặc cuộn để tìm font bạn cần. ### Cỡ chữ và độ đậm \{#size-and-weight\} * **Weight**: Chọn độ đậm font từ dropdown * **Size**: Chọn kích thước từ dropdown 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 cung cấp 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ư fill, màu viền và màu chữ để làm nổi bật lựa chọn đang hoạt động Để định dạng phần tử có thể chọn khi nó không tương tá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 **Set conditions** set conditions để xác định khi nào phần tử bị vô hiệu hóa trong thời gian chạy, ví dụ khi một trường bắt buộc còn 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. ### Text style \{#text-styles\} :::link Bài viết chính: [Nội dung văn bản](onboarding-text) ::: Text style lưu trữ một bộ cài đặt kiểu chữ đầy đủ — họ font, độ đậm, cỡ chữ, chiều cao dòng, căn chỉnh và trang trí. Mỗi template flow đều bao gồm các preset mặc định, và bạn có thể tạo style tùy chỉnh. Để tạo text style: 1. Mở bảng **Styles** Styles và chọn tab **Text**. 2. Nhấp **Plus Create style**. 3. Nhập tên và cấu hình cài đặt kiểu chữ. 4. Nhấp **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**. ### Color style \{#color-styles\} Color style là các màu được đặt tên mà bạn có thể tham chiếu trong toàn bộ flow. Mỗi color style có tên (như "Primary text" hoặc "Brand"), giá trị hex, và số lượng sử dụng cho biết bao nhiêu phần tử đang tham chiếu đến nó. Để tạo color style: 1. Mở bảng **Styles** Styles và chọn tab **Colors**. 2. Nhấp **Plus Create style**. 3. Nhập tên và chọn màu. Khi bạn cập nhật một color style, tất cả các phần tử tham chiếu đến nó sẽ tự động cập nhật theo. ### Dark mode \{#dark-mode\} :::link Bài viết chính: [Dark mode](paywall-dark-mode) ::: Nếu cần, bạn có thể thêm hai biến thể cho mỗi color style — 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 giao diện màu hiện tại của thiết bị. Để xem trước dark mode trong builder, dùng **theme toggle** Dark mode trong [thanh công cụ phía dưới](builder-ui#view-controls-bottom-toolbar). --- # File: flow-selectable-elements --- --- title: "Selectable elements and groups" description: "Make elements selectable, organize them into groups, and use their state in conditions across the flow." --- Selectable elements are flow elements that users can tap to select or deselect. Their state can drive navigation, visibility, and other logic across the flow. Here's what you can do: - [Use default selectable elements](#default-selectable-elements) — quiz options, products, tabs, and trial toggles are selectable out of the box - [Make any element selectable](#make-an-element-selectable) — turn any element into a selectable one and assign it to a group - [Create and manage groups](#create-a-group) — organize selectable elements into single-choice, multi-choice, or toggle groups - [Use selected state in conditions](#use-selectable-state-in-conditions) — reference group values in conditions on any screen in the flow ## Default selectable elements Some element types are selectable by default — they already belong to auto-created groups and don't need additional setup: - **Quiz options**: Each quiz answer is a selectable element within the quiz group. See [Quizzes](onboarding-quizzes). - **Products**: Product cards in a product group. See [Product block](paywall-product-block). - **Tabs**: Tab items within a tab group. See [Tabs](builder-tabs). - **Trial toggles**: A container that belongs to a group and gets a selected state. See [Toggles](builder-toggles). ## Make an element selectable In some cases, you might want to make additional elements selectable. For example, you can add a **Don't ask me again** checkbox that operates as an element inside a quiz group. To make an element selectable: 1. Select the element on the screen or in the **Layers** panel. 2. On the right, switch to the **Interactions** panel. 3. Select **Turn into selectable element**. 4. In the **Group** dropdown, select an existing group or [create a new one](#create-a-group). 5. Set the **Element ID** — a unique identifier for this element within the group. 6. If you want this element to be selected by default, select the **Set as default in group** checkbox. ## Create a group Groups organize selectable elements on a screen and define how selection works — whether users can pick one option, multiple options, or toggle. To create a group: 1. Select an element and [make it selectable](#make-an-element-selectable). 2. In the **Group** dropdown, select **Create group**. 3. Enter a **Group name**. 4. Select the [group type](#group-types). The group is now available in the **Group** dropdown for other selectable elements on the same screen. ## Group types :::important Most [quiz presets](onboarding-quizzes) are **multi-choice** by default. Change the [group type](#manage-groups) to allow only one answer. ::: - **Single choice**: Only one element in the group can be selected at a time. Selecting a new element deselects the previous one. - **Multi-choice**: Multiple elements can be selected at the same time. - **Toggle**: Each element switches between selected and deselected on each tap, independently of other elements. ## Manage groups To view and edit groups, open the **Screen settings** panel and find the **Selectable groups** section. It lists all groups on the current screen. Click a group ID to: - Change the group ID - Change the [group type](#group-types) - See how group elements are referenced in conditions ## Use selectable state in conditions You can reference a group's selected state in conditions on any screen in the flow — not just the screen where the group is defined. For example: `IF quiz.photo is selected, THEN navigate to the Photo screen`. :::important All elements in a group must be on the same screen. You cannot add elements from different screens to one group. However, you can reference group values in conditions on any screen in the flow. ::: Use selectable state with: - **[Conditional actions](onboarding-actions#conditional-actions)**: Route users to different screens or trigger different actions based on selected elements. - **[Dynamic navigation](onboarding-navigation-branching)**: Branch the flow based on quiz answers, toggle states, or other selections. - **[Conditional visibility](onboarding-element-visibility)**: Show or hide elements based on what users selected on previous screens. --- # File: builder-element-states --- --- title: "Trạng thái của element" description: "Tùy chỉnh giao diện element theo từng trạng thái, và dùng điều kiện để vô hiệu hóa element khi chạy thực tế." --- Các element tương tác trong flow thay đổi giao diện theo hành động của người dùng: một lựa chọn quiz được nhấn sẽ chuyển sang **Selected**, một input đang được focus sẽ chuyển sang **Active**. Một số trạng thái được kích hoạt theo điều kiện — ví dụ: bạn có thể **vô hiệu hóa** một nút. Hãy tùy chỉnh giao diện riêng cho từng trạng thái để người dùng nhận được phản hồi trực quan mà không cần viết code trong ứ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 element \{#available-states-by-element-kind\} | Loại element | Trạng thái mặc định | Trạng thái có thể thêm | |---|---|---| | [Element selectable](#selectable-element-states) | **Default**, **Selected** | **Disabled** | | [Input](#input-states) | **Default**, **Active**, **Invalid** | **Disabled** | | [Mọi element có tương tác nhấn](#condition-driven-disabled-state) — nút, ảnh, icon, stack, v.v. | **Default** | **Disabled** | | [Các bước của progress indicator](#step-states-for-progress-indicators) | **Completed**, **Current**, **Upcoming** | — | Các trạng thái **có thể thêm** không xuất hiện mặc định — mở **States settings** Settings để thêm chúng. Chúng được [**kích hoạt theo đ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ùy chỉnh giao diện theo trạng thái \{#how-to-style-a-state\} 1. Chọn một element. Phần **States** trong panel bên phải liệt kê các trạng thái mà element đó hỗ trợ. 2. Trong phần **States**, kích hoạt trạng thái cần chỉnh. 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, nội dung văn bản, v.v. Thay đổi đó chỉ áp dụng cho trạng thái đó. Các element con cũng trở nên có trạng thái cùng với element cha. Mọi thay đổi với element con — màu sắc, bố cục, nội dung văn bản — đều được giới hạn trong trạng thái đang kích hoạt của element cha. 4. Builder sẽ áp dụng style tương ứng khi chạy thực tế. :::tip Một trạng thái có thể thay đổi *nội dung* của một element văn bản, không chỉ thay đổi giao diện của nó. Builder xem nội dung văn bản là một thuộc tính, cùng danh mục với fill hay border width. ::: ## Trạng thái của element selectable \{#selectable-element-states\} Các element selectable — lựa chọn quiz, sản phẩm, tab, toggle dùng thử, và bất kỳ [element selectable 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 element. - **Selected**: Áp dụng khi người dùng nhấn vào element. Builder sẽ trở về Default khi người dùng bỏ chọn element. Trong nhóm chọn một (single-choice), việc chọn một element sẽ bỏ chọn các element còn lại. Nhóm đa chọn (multi-choice) cho phép nhiều element ở trạng thái Selected cùng lúc. Toggle hoạt động độc lập — việc chọn một toggle không ảnh hưởng đến các toggle khác. Xem thêm [loại nhóm](flow-selectable-elements#group-types). :::tip Muốn tùy chỉnh cùng một trạng thái cho nhiều element (ví dụ: các lựa chọn quiz)? Hãy tùy chỉnh một element 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 element 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 element. Khác với Default, Selected, Active hay Invalid, trạng thái Disabled không tự kích hoạt — nó yêu cầu một điều kiện kích hoạt do người dùng định nghĩa. Disabled có thể áp dụng cho: - **Input**: Bất kỳ [trường input](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ờ. - **Element selectable**: Lựa chọn quiz, sản phẩm, tab, toggle dùng thử, và bất kỳ [element selectable tùy chỉnh](flow-selectable-elements#make-an-element-selectable) nào. - **Mọi element có tương tác nhấn**: Ví dụ: nút, ảnh hoặc icon kích hoạt mộ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 element cần chỉnh. 2. Trong phần **States**, nhấn **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ấn **Edit conditional state** Edit conditional state. 5. Thêm điều kiện. Nếu muốn vô hiệu hóa nút **submit** khi input chưa vượt qua validation, 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 để truyền đạt trực quan sự hạn chế đó (ví dụ: giảm độ mờ/opacity). { } Adapty SDK sẽ đánh giá điều kiện khi chạy thực tế và áp dụng trạng thái Disabled khi phù hợp — không cần 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: "Các 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 nhóm các phần tử khác lại với nhau và kiểm soát cách chúng được sắp xếp trên màn hình. Flow Builder có bốn loại phần tử bố cục: - **Container**: sắp xếp các phần tử con theo một trục — dọc hoặc ngang - **Carousel**: container có thể vuốt, hiển thị một slide tại một thời điểm - **Bottom Sheet**: một panel trượt lên từ phía dưới màn hình và hiển thị phía trên nội dung bên dưới - **Divider**: 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ó 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 container 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 để xem slide tiếp theo, hoặc carousel tự động chuyển slide theo bộ đếm thời gian. Carousel chứa một tập hợp các layer **Slide**. Khi slide đang hoạt động, các phần tử trên layer đó sẽ xuất hiện trên màn hình. Khác với Tabs, slide đang hoạt động của carousel không được hiển thị như một [selectable group](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. Dùng Carousel để xoay vòng hiển thị, không dùng cho phân nhánh do người dùng điều khiển. ### 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 \{#properties\} #### Auto-scroll \{#auto-scroll\} Auto-scroll tự động chuyển qua các slide — người dùng không cần vuốt để xem toàn bộ nội dung. Hai điều khiển thời gian xác định hành vi của tính năng 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 riê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 liền kề hiển thị một phần. #### Dots \{#dots\} Chỉ báo trang ở phía dưới carousel. Nó cho người dùng biết có bao nhiêu slide và slide nào đang hiển thị. Tắt toggle **Show dots** để ẩn chỉ báo slide. Khi dots hiển thị, các thuộc tính sau điều khiển giao diện của chúng: - **Color** — màu của dot không hoạt động. - **Active Color** — màu của dot cho slide đang hiển thị. - **Size** — đường kính của mỗi dot, tính bằng pixel. - **Gap** — khoảng cách giữa các dot liền kề. - **Padding** — khoảng cách giữa hàng dot 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. ## 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 xây dựng flow, bạn có thể muốn sử 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 bảng **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 trên toàn cục. ::: ## Font tích hợp sẵn \{#built-in-fonts\} Khi tạo 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, nhưng có thể khác nhau tùy 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 này đi kèm với một vài tùy chọn kiểu dáng. Các font này không được cung cấp như một phần của SDK và chỉ 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 tất cả thiết bị. Tuy nhiên, qua thử nghiệm, những font này thường được hầu hết các thiết bị nhận dạng mà không cần thêm bất kỳ thao tác nào từ phía bạn. Bạn cũng có thể [xem các font có sẵn theo mặc định trên iOS](https://developer.apple.com/fonts/system-fonts/). ## Thêm font tùy chỉnh \{#add-a-custom-font\} Nếu bạn cần nhiều hơn những gì được cung cấp mặc định, bạn có thể thêm font tùy chỉnh. Để thêm font tùy chỉnh: 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: - **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 font trên toàn Builder. - **iOS font name**: Nhập tên PostScript của font. Bạn có thể tìm thấy trong Font Book → PostScript name, hoặc qua [`UIFont` API](https://developer.apple.com/documentation/uikit/uifont). - **Android font name**: Nhập tên file từ `res/font/`. Chỉ sử dụng chữ thường, số và dấu gạch dưới. - **Font file**: Kéo và thả file font hoặc nhấn **Select files**. Các định dạng được hỗ trợ: `.ttf`, `.otf`, `.woff`, `.woff2`. 3. Nhấn **Save font**. :::warning File font bạn tải lên không được gửi đến thiết bị; nó chỉ dùng để xem trước. SDK của chúng tôi chỉ nhận các chuỗi tham chiếu đến font cần sử dụng khi hiển thị paywall. Do đó, bạn phải đưa cùng file font đó vào bundle ứng dụng và cung cấp tên font đúng theo từng nền tảng để mọi thứ hoạt động trơn tru. Đừng lo, việc này sẽ không mất nhiều thời gian. ::: Bằng cách tải file font 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. ## Thêm file font vào bundle ứ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 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. Đọc cách thực hiệ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) :::important Khi tải xuống các gói font, bạn sẽ nhận được tất cả các biến thể font trong một file nén. Chỉ thêm những file font cụ thể mà paywall của bạn sử dụng vào bundle ứng dụng để giảm thiểu kích thước ứng dụng. Ví dụ: nếu bạn chỉ dùng `OpenSans-Regular.ttf` trong paywall, đừng đưa `OpenSans-Bold.ttf` vào bundle ứng dụng. ::: --- # 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à hoán đổi media lúc chạy bằng custom media ID." --- Flow Builder bao gồm ba loại phần tử media thuộc danh mục **Media**: Image, Video và Icon. ## 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. - **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ị lúc chạy, video phát ở chế độ tắt tiếng theo mặc định. Khi bật Loop, video lặp lại vô thời 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. ## Custom media ID \{#custom-media-id\} :::important Bạn cũng có thể đặt custom media ID 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 bằng custom media ID để hoán đổi 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. Hình ảnh hoặc video bạn tải lên trong Flow Builder sẽ đóng vai trò là nội dung dự phòng. Nếu code của bạn không cung cấp media cho ID đó lúc chạy, nội dung dự phòng sẽ được hiển thị thay thế. Để bật custom media ID trên phần tử Image 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 một media ID. 3. Tải lên hình ả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 API của SDK. --- # File: paywall-buttons --- --- title: "Các nút trong Flow Builder" description: "Thêm và cấu hình các nút thao tác trong Flow Builder." --- 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 vào. Dùng chúng cho: - CTA mua hàng — kết nối với sản phẩm và xử lý giao dịch tự động - Điều hướng — 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 giao dịch, Điều khoản dịch vụ và Chính sách quyền riêng tư :::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. ::: ## Thêm nút \{#add-buttons\} Để thêm bất kỳ nút nào: 1. Nhấp **+** 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 đã được cấu hình sẵn hành động. Với liên kết, hãy [cấu hình URL để điều hướng người dùng](#links). Với các loại nút khác, vào bảng **Interactions**. Tại đó, trong phần **Button triggers**, 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](manage-paywall-ui-elements) trong bảng **Design**. ## Các loại nút \{#button-types\} ### Nút mua hàng \{#purchase-buttons\} :::link Để các 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 khởi động in-app purchase cho sản phẩm mà người dùng đang 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ý mua hàng trong code ứng dụng. Để thêm nút mua hàng: 1. Nhấp **+** và chọn **Button**, sau đó chọn một preset nút. 2. Với nút đang được chọn, mở tab **Interactions** ở bảng bên phải. 3. Nhấp **Add trigger** > **On tap**, rồi nhấp **Add action**. 4. Đặt **Action** thành **Purchase** và **Product** thành `products.selectedProduct`. Biến `products.selectedProduct` luôn trỏ đến sản phẩm hiện đang được chọn trên màn hình. :::tip Bạn có thể thu hút thêm sự chú ý vào nút mua hàng bằng cách thêm hiệu ứng chuyển độ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**. Đặt URL đích tại đây. URL trống trong Open URL 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 Để thêm liên kết: 1. Nhấp **+** và chọn **Button > Links**. Thao tác này sẽ thêm một hàng nút nội tuyến với các hành động được định sẵn: khôi phục giao dịch hoặc mở URL. Nếu bạn không cần tất cả các nút, 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ờ, 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 sẵn rồi. - Với mỗi liên kết còn lại: 1. Nhấp vào nút để chọn và chuyển sang tab **Interactions** ở bên phải. 2. Dán URL vào ô nhập liệu. 3. Mặc định, URL sẽ mở trong trình duyệt trong ứng dụng để trải nghiệm liền mạch. Nếu 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** đóng flow tự động. Để thêm nút đóng, nhấp **+** 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 dùng [hành động](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 về các cài đặt giao diện — bố cục, style và hiển thị — xem [Định dạng phần tử](builder-styling). ::: Với tất 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 trống. - **State:** Định nghĩa giao diện 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 giao diện 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 | |----------------------------|---------------------------| | Clear button | Text, Email | | Validate email format | Email | | Show password icon | Password | | Edit password requirements | Password | | Number format | Number | | Date/time format | Date, Time, Date and time | | Min and max date | Date, Date and time | ## Sử dụng giá trị input \{#use-input-values\} Mỗi input tự động có sẵn dưới dạng biến — không cần thiết lập thêm hay dùng hành động **On Submit**. Giá trị được tham chiếu thông qua **Element ID** của input, được đặt trong **Input Settings**. Để sử dụng giá trị input ở 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 giá trị input đã lưu: - [Điều hướng có điều kiện](onboarding-navigation-branching) - [Biến](onboarding-variables) ::: ## Xác thực input \{#input-validation\} Hành vi xác thực phụ thuộc vào loại input. Mỗi input đề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 input hay 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 Next cho đến khi định dạng email hợp lệ. :::note - Biến `isValid` là chỉ đọc — bạn không thể đặt giá trị cho nó. - Trường input trống luôn được coi là hợp lệ. - Các input text không có quy tắc xác thực. `textInput.isValid` luôn trả về `True`. ::: | Loại input | Hành vi xác thực | |---|---| | Text | Không có quy tắc xác thực 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á lúc runtime. | | 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 giao diện](builder-styling#input-states) **Invalid** được kích hoạt khi người dùng submit form — ví dụ: nhấn Enter hoặc Done trên bàn phím. Cho đến lúc đó, input hiển thị trạng thái **Active** hoặc **Default**. ### Yêu cầu mật khẩu \{#password-requirements\} Input password hỗ trợ các quy tắc xác thực có thể cấu hì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 hiển thị dưới dạng danh sách kiểm tra trực tiếp bên dưới input — mỗi mục được đánh dấu ngay khi quy tắc tương ứng được thỏa mãn. Các quy tắc có sẵn: - **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: "Actions" description: "Định nghĩa các hành động được kích hoạt bởi tương tác của người dùng trong builder." --- Bảng **Interactions** cho phép bạn định nghĩa cách các phần tử trong flow phản hồi với các sự kiện — như thao tác chạm, phần tử xuất hiện, 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 phần tử, mở URL, đặt biến, và nhiều hơn nữa. 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 gồm ba phần liên kết: 1. **Element**: Thành phần màn hình bắt đầ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 cứ thứ gì khác. 2. **Trigger**: Sự kiện kích hoạt logic, chẳng hạn như thao tác chạm, phần tử xuất hiện, hoặ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 bảng **Layers**. 2. Ở phía phải, chuyển sang bảng **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, và chọn [loại action](#action-types) từ dropdown trong cửa sổ **Edit action**. 5. Cấu hình thuộc tính action dựa trên [loại action](#action-types) bạn đã chọn. 6. Nếu cần, nhấn **Add action** để thêm nhiều action cho cùng một trigger. ## Loại trigger \{#trigger-types\} Trigger kích hoạt để phản hồi hành vi người dùng, thay đổi trạng thái phần tử, hoặc khi màn hình tải. **On screen appear** là trigger chung; các trigger 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ả phần tử | | **On tap** | Người dùng chạm 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 (nhập liệu, chọn ngày hoặc giờ) | Tất cả [phần tử input](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** | Phần tử [Countdown](flow-timer) đạt về 0 | [Countdown](flow-timer) | | **On playback finished** | [Video](custom-media) phát đến cuối | [Video](custom-media) | 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 khả dụng. ## Loại action \{#action-types\} :::important **Bất kỳ action điều hướng nào** chuyển người dùng sang màn hình khác phải luôn là action cuối cùng trong danh sách. Các action đặt sau nó (như "Set Variable") có thể không được thực thi vì ứng dụng đã chuyển màn hình rồi. ::: ### Navigate to screen \{#navigate-to-screen\} Đây là action chính để di chuyển người dùng giữa các màn hình. Nó đưa người dùng đến màn hình đích được chỉ định. Với action này, bạn chỉ cần thiết lập màn hình đích. Nếu muốn bật điều hướng động, xem [Navigation and branching](onboarding-navigation-branching) hoặc phần [Conditional actions](#conditional-actions). ### Navigate next \{#navigate-next\} Đưa người dùng đến màn hình tiếp theo trong thứ tự màn hình của flow. Dùng cho các flow tuyến tính, nơi thứ tự màn hình trong editor khớp với thứ tự bạn muốn người dùng xem. ### Navigate back \{#navigate-back\} Quay người dùng về màn hình trước đó trong lịch sử điều hướng của họ, thay vì màn hình trước đó trong chuỗi. ### Open URL \{#open-url\} :::tip Dùng [inline links](onboarding-text#inline-link) để chèn liên kết vào văn bản đang chảy. ::: Mở một địa chỉ web cụ thể. Dùng để đưa người dùng đến các trang web, bài viết, hoặc hồ sơ mạng xã hội bên ngoài màn hình native của ứng dụng. Với action này, bạn có thể cấu hình hai cài đặt: - **URL address**: Đặt địa chỉ URL. Ngoài ra, bạn có thể làm cho 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ọ đã gửi. Để làm điều này, nhấn Variable icon và chọn biến bạn muốn dùng. - **Open in external browser**: Xác định nơi bạn muốn mở liên kết ngoài. Theo mặc định, chúng mở trong trình duyệt trong ứng dụng để giữ người dùng trong app. Chọn checkbox **Open in external browser** nếu bạn muốn mở liên kết trong trình duyệt bên ngoài. ### Close flow \{#close-flow\} Đóng flow hiện tại. ### Show/hide elements \{#showhide-elements\} Hiển thị hoặc ẩn một phần tử cụ thể trên màn hình. Action này ghi đè trạng thái ban đầu được đặt trong **Visibility** ở bảng **Design**. Nếu **Visibility** được đặt thành **Hide**, action **Show** sẽ làm cho nó xuất hiện. :::important Action **Show** hoặc **Hide** không có phần tử đích [chặn xem trước và xuất bản](builder-save-publish#troubleshooting). Chọn đích hoặc xóa action. ::: ### Show alert \{#show-alert\} Hiển thị cửa sổ pop-up hệ thống native. Người dùng phải nhấn **Ok** để tiếp tục. Với alert, bạn phải thiết lập **Title** và **Message**. Trong cả hai, bạn có thể dùng biến để làm nội dung động. Để làm điều này, nhấn Variable icon và chọn biến bạn muốn dùng. :::important Action **Show alert** với cấu hình trống hoặc chưa hoàn chỉnh [chặn xem trước và xuất bản](builder-save-publish#troubleshooting). Điền vào cả hai trường hoặc xóa action. ::: ### Set variable \{#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 biến trong bảng **Variables** ở bên trái (xem [Variables](onboarding-variables)). Nhấn **Add variable** và đặt bao nhiêu biến cùng giá trị của chúng tùy ý. :::important Action **Set variable** không có phép gán [chặn xem trước và xuất bản](builder-save-publish#troubleshooting). Cấu hình ít nhất một phép gán hoặc xóa action. ::: ### Purchase \{#purchase\} Kích hoạt luồng mua hàng trực tiếp từ một nút hoặc tương tác trong onboarding. Dùng để cho phép người dùng đăng ký hoặc mua một 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 tạo một giao dịch mua native. Đặt **Product** thành một sản phẩm cụ thể, hoặc thành `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**: Đưa người dùng đến một [web paywall](web-paywall) thay vì kích hoạt giao dịch mua native. Dùng khi bạn muốn xử lý giao dịch bên ngoài ứng dụng, chẳng hạn cho các ưu đãi đăng ký trên web. :::important Action **Purchase** không có **Product** hoặc **Web Paywall URL** đích [chặn xem trước và xuất bản](builder-save-publish#troubleshooting). Gán đích hoặc xóa action. ::: ### Restore purchases \{#restore-purchases\} Kích hoạt luồng khôi phục giao dịch mua trên thiết bị. Người dùng nhấn vào đây khi họ đã mua gói đăng ký trước đó trên thiết bị khác hoặc sau khi cài lại ứng dụng, và cần khôi phục quyền truy cập vào các quyền lợi của họ. 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 luồng cửa hàng native. Action **Restore purchases** cũng được cấu hình sẵn trên liên kết **Restore** trong preset nút **Links** (xem [Set up purchases](paywall-product-block#restore-purchases)). ## Custom actions \{#custom-actions\} Một custom action kích hoạ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 action tích hợp sẵn không đáp ứng được nhu cầu của bạn. Adapty cung cấp trigger; ứng dụng của bạn thực hiện hành vi: 1. Trong builder, bạn gán **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 truyền ID cho ứ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 custom action \{#set-up-a-custom-action\} 1. Trong cửa sổ **Edit action**, gán **Action ID** — một chuỗi mà ứng dụng của bạn sẽ nhận ra (ví dụ: `show_discount`). 2. Trong code ứng dụng của bạn, triển khai một handler cho Action ID này. Xem [Handle paywall actions](handle-paywall-actions) để biết chi tiết triển khai và ví dụ code. :::important Action **Custom** không có **Action ID** [chặn xem trước và xuất bản](builder-save-publish#troubleshooting). Gán Action ID hoặc xóa action. ::: ### Bạn có thể làm gì với custom actions \{#what-you-can-do-with-custom-actions\} Một custom action tự nó không làm gì cả. Bạn đặt một Action ID tĩnh trong builder, và code ứng dụng của bạn xử lý điều xảy ra khi nhận được ID đó. Mọi trường hợp sử dụng dưới đây đề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 sự kiện trong ứng dụng**: Kích hoạt ID như `viewed_special_offer`, sau đó ghi lại sự kiện vào analytics của bạn khi ứng dụng nhận được nó. - **Yêu cầu quyền hệ thống**: Kích hoạt ID như `request_location`, sau đó gọi lời nhắc quyền OS từ ứng dụng của bạn. Adapty không hiển thị lời nhắc — ứng dụng của bạn làm điều đó. - **Bắt đầu xác thực native**: Kích hoạt ID như `login_google`, sau đó hiển thị màn hình đăng nhập của bạn. Flow không thể đăng nhập người dùng. - **Áp dụng logic nghiệp vụ**: Kích hoạt ID như `apply_discount`, sau đó mở khóa nội dung hoặc thay đổi trạng thái ứng dụng ở phía bạn. - **Truyền câu trả lời quiz cho ứng dụng**: Gán Action ID khác nhau cho mỗi tùy chọn (ví dụ: `goal_weight_loss` và `goal_muscle`), sau đó đọc ID trong code của bạn. Dùng ID để đặt [custom user attribute](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 để báo cáo lựa chọn — flow không thể gửi giá trị đã chọn. :::important Custom action 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 cũng kích hoạt Action ID mới. Ứng dụng của bạn sau đó nhận cả hai theo thứ tự — ví dụ: `goal_weight_loss`, rồi `goal_muscle`. Hãy làm cho handler của bạn idempotent để tín hiệu mới nhất thắng. ::: ### Custom actions không thể làm gì \{#what-custom-actions-cant-do\} Custom action là tĩnh. Action ID được cố định khi bạn build flow — nó không thể đọc [biến](onboarding-variables) hoặc [input người dùng](builder-inputs-and-forms). Khi action 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, hoặc input khác mà người dùng đã nhập. Các trường input ở lại trong flow dưới dạng biến để phân nhánh và cá nhân hóa. Để dùng các giá trị đó trong ứng dụng của bạn, hãy thu thập chúng thông qua UI hoặc API của riêng bạn. ## Conditional actions \{#conditional-actions\} Dùng conditional actions để phân chia flow thành các đường dẫn 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: - Bạn có một 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, thêm conditional action 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 khác nhau 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 nhất định cho những người dùng đã hoàn thành hướng dẫn trong phiên ứng dụng trước đó. Conditional actions 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 ở kết quả khớp đầu tiên: 1. **IF**: Flow kiểm tra điều kiện chính. - Đúng? Flow thực thi ngay các action THEN và dừng. - Sai? Flow bỏ qua đến phần tiếp theo. 2. **ELSE IF**: Bạn có thể thêm các kiểm tra bổ sung ở đây (ví dụ: "Nếu không phải Premium, người dùng có đang dùng Trial không?"). 3. **ELSE** (Fallback): Nếu không có quy tắc nào khớp ở trên, flow thực thi các action trong phần cuối này. :::important - Nếu một quy tắc được thêm vào nhưng không có action được gán, việc khớp điều kiện dẫn đến không làm gì. - Một quy tắc chưa hoàn chỉnh (không có toán tử hoặc giá trị) [chặn 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 action để chạy. Bạn có thể đặt nhiều hơn một action cho mỗi quy tắc. :::important Flow chỉ thực thi một quy tắc — quy tắc đầu tiên nó khớp. Nếu bạn cần thực thi cả **IF** và **ELSE IF** cùng lúc, hãy thêm cả hai action 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 để dùng trong điều kiện, xem [Selectable elements and groups](flow-selectable-elements). ## Khắc phục sự cố \{#troubleshooting\} Bất kỳ action nào thiếu các trường bắt buộc đều chặn xem trước và xuất bản. Xem [Save & publish flows](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: "Điều kiện hiển thị" description: "Hiển thị hoặc ẩn các phần tử dựa trên điều kiện." --- 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 [hành động](onboarding-actions) **Show** hoặc **Hide**, hành động đó sẽ ghi đè điều kiện **Visibility** đã đặt trên phần tử đó. Dùng điều kiện **Visibility** cho các phần tử cần hiển thị hoặc ẩn dựa trên tiêu chí cố định. Dùng hành động khi khả năng hiển thị cần thay đổi theo 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 câu hỏi khảo sát. ::: Để thêm điều kiện vào 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ạ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ư trạng thái bật/tắt của toggle dùng thử. 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 nhiều điều kiện hơn. Dùng bộ chọn để yêu cầu tất cả điều kiện đều 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 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 với Remote Config](customize-paywall-with-remote-config). ::: Remote Config cho phép bạn lưu trữ một payload JSON tùy chỉnh mà SDK đọc tại runtime. Dùng nó để thiết lập các giá trị như tiêu đề, hình ảnh, font chữ, màu sắc, hoặc 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 cho 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 **publish** flow. Xem [Lưu & publish flow](builder-save-publish#troubleshooting) để biết danh sách đầy đủ các vấn đề cản trở việc xem trước và publish. ::: Bạn có thể truy cập dữ liệu này từ SDK thông qua mảng `remoteConfigs` trên `AdaptyFlow`. Adapty lưu trữ một entry `AdaptyRemoteConfig` cho mỗi ngôn ngữ; chọn entry 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 tại runtime. 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="Tiêu đề" default> ```json showLineNumbers { "screen_title": "Today only: Subscribe, and get 7 days for free!" } # Test titles or other texts ``` </TabItem> <TabItem value="Images" label="Hình ảnh"> ```json showLineNumbers { "background_image": "https://adapty.io/media/paywalls/bg1.webp" } # Test images on your flow ``` </TabItem> <TabItem value="Fonts" label="Font chữ"> ```json showLineNumbers { "font_family": "San Francisco", "font_size": 16 } # Test fonts ``` </TabItem> <TabItem value="Color" label="Màu sắc"> ```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="Bản dịch"> ```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 nội dung, bố cục, hoặc hành vi thay thế. Tiếp theo, [tạo một placement](create-placement) và thêm flow vào đó. Sau đó [hiển thị flow trong app iOS của bạn](present-remote-config-paywalls). ## 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. ## Import và export JSON \{#import-and-export-json\} Sử dụng menu **Import/Export** phía trên trình chỉnh sửa để sao lưu, chia sẻ, hoặc chỉnh sửa hàng loạt Remote Config của bạn trên tất cả các ngôn ngữ cùng một lúc. - **Export JSON**: Tải xuống một file JSON duy nhất chứa tất cả 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ị: ```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 block ngôn ngữ tuân theo cùng cấu trúc JSON mà bạn nhập trực tiếp vào cột ngôn ngữ. --- # 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\} --- no_index: true --- 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 flow \{#publish-a-flow\} Xuất bản sẽ đưa phiên bản hiện tại của flow đến người dùng thông qua SDK. Sau khi xuất bản, phiên bản mới sẽ thay thế bất kỳ phiên bản đã xuất bản nào trước đó của cùng flow đó. Để xuất bản flow, nhấp **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 vào một placement hay chưa: - **Flow đã có trong placement**: Người dùng sẽ thấy phiên bản mới trong lần yêu cầu tiếp theo đến placement đó. - **Flow chưa có trong placement**: 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. ::: ## 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 vị trí của flow 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, nên người dùng chưa thấy. | | **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 cuối 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\} --- no_index: true --- 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ệ completion duy nhất \{#unique-completions-rate\} Số completion duy nhất chia cho số lượt xem duy nhất. Dùng chỉ số này để hiểu người dùng tiến qua flow như thế nào và xác định nơi họ rời bỏ. ### 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: "Fallback flows" description: "Thiết lập fallback flows cục bộ trong Adapty để giữ cho flow hiển thị khi thiết bị offline." --- Để đảm bảo 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 yêu cầu một flow, Adapty SDK sẽ liên hệ với máy chủ của chúng tôi để lấy cấu hình. Nếu thiết bị không thể kết nối Adapty (lỗi mạng, sự cố máy chủ), SDK sẽ chuyển sang dùng dữ liệu cục bộ: - Nếu người dùng đã xem flow đó trước đây, SDK sẽ phục vụ bản đã lưu trong bộ nhớ cache. - Nếu không có cache, SDK sẽ tải tệp cấu hình dự phòng được đóng gói sẵn trong ứng dụng. Adapty tự động tạo các tệp dự phòng này. Gói fallback cho flow được dùng chung với paywall — một tệp 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 sẽ đọc phần nào cần thiết. :::important Fallback flow đượ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, tệp 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 phụ thuộc vào fallback flow. ::: ## 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 tệp fallback \{#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ừ menu thả xuống. 4. Chọn phiên bản SDK khớp với phiên bản đang dùng trong ứng dụng. Chọn **Adapty SDK v4.0.0 and higher** (hoặc tùy chọn mới hơn) để nhận gói có chứa flow. Trình duyệt sẽ tải xuống một tệp JSON cho mỗi nền tảng — ví dụ: `ios_4_0_0_fallback.json`. <details> <summary>Ví dụ một 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": "" } } } ``` Cấu trúc chính xác có thể thay đổi giữa các phiên bản SDK. Hãy luôn dùng tệp do Adapty tạo ra cho phiên bản SDK của bạn thay vì tự tạo thủ công. </details> ## Sau khi tải xuống \{#after-the-download\} Thêm tệp vào mã nguồn ứng dụng, sau đó làm theo hướng dẫn thiết lập dành riêng cho từng nền tảng. Các API dùng để tải paywall fallback cũng sẽ tải flow fallback khi ứng dụng đang chạy phiên bản SDK hỗ trợ flow: - [iOS](ios-use-fallback-paywalls) ## Giới hạn \{#limitations\} Fallback flow được mã hóa cứng và lưu trữ cục bộ, nên không có đầy đủ các tính năng động như 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 (đối tượng khác nhau, các biến thể A/B test), tệp fallback 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.** A/B test trên flow trực tiếp được xử lý trên máy chủ; fallback luôn phục vụ một biến thể duy nhất đã được chọn sẵn. - **Không cập nhật từ xa.** Để cập nhật fallback, bạn cần phát hành phiên bản ứng dụng mới. Những thay đổi bạn thường thực hiện qua Remote Config hãy đưa vào flow trực tiếp thay thế. - **Chỉ dùng ngôn ngữ mặc định.** Fallback dùng locale `en`; các biến thể đã 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: 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 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\} --- no_index: true --- 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 chỉ số \{#metrics-definitions\} Khai thác sức mạnh của chỉ số placement với các định nghĩa toàn diện. Từ doanh thu đến tỷ lệ chuyển đổi, thu thập thông tin chi tiết có giá trị giúp tăng cường chiến lược kiếm tiền và thúc đẩy thành công cho ứng dụng của bạn. <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 */ }} /> #### 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: "Use fallback paywalls to ensure seamless user experience in Adapty." --- To maintain a fluid user experience, it is important that you set up **fallback versions** for your [paywalls](paywalls) and [onboardings](onboardings). When your application loads a paywall, the Adapty SDK requests paywall configuration data from our servers. But what if the device cannot connect to Adapty due to network issues or server outages? * If the user accessed the paywall before, and the device cached its data, the application loads paywall data **from cache**. * If the device did not cache the paywall, the application looks for a locally stored configuration file. It allows the application to display the paywall without an error. Adapty automatically generates fallback configuration files for you to download and use. Each file contains platform-specific configurations for *all* your placements. ## Get started 1. [Download the fallback configuration file](/local-fallback-paywalls) from Adapty. 2. Use the Adapty SDK to configure your fallback paywalls: * [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) ## Limitations Fallback paywalls are hard-coded and locally stored, so they lack the dynamic capabilities of regular Adapty paywalls. * Fallback paywalls don't support [internationalization](paywall-localization). When Adapty generates the configuration file, it uses the default `en` locale. * Each placement can only have one fallback paywall. If your setup inlcudes different paywall configurations for different [audiences](audience), Adapty uses the configuration intended for "All users". * Fallback paywalls don't support [A/B testing](ab-tests). If a paywall participates in an A/B test, its fallback configuration file will include the variation with the highest weight. * Fallback paywalls cannot be [managed remotely](customize-paywall-with-remote-config). If you want to update the configuration file, you need to release a new version of the app on 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\} --- no_index: true --- 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 chỉ số \{#metrics-definitions\} #### Revenue \{#revenue\} Chỉ số này biểu thị tổng số tiền (tính bằng USD) thu được từ các giao dịch mua và gia hạn. Lưu ý rằng tính toán doanh thu không bao gồm hoa hồng của App Store / Play Store và được tính trước khi trừ bất kỳ khoản phí nào. #### Proceeds \{#proceeds\} Chỉ số này biểu thị số tiền thực tế (tính bằng USD) mà chủ ứng dụng nhận được từ các giao dịch mua và gia hạn sau khi trừ 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: "Manage user profiles and CRM data in Adapty to enhance audience segmentation." --- Profiles is a CRM for your users. With Profiles, you can: 1. Find specific users by profile ID, customer user ID, email, or transaction ID. 2. View the user's event timeline, including billing issues, grace periods, and other [events](events). 3. Analyze user's properties such as subscription state, total revenue/proceeds, and more. 4. Grant the user a subscription. --- no_index: true --- import Callout from '../../../components/Callout.astro'; :::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 To understand how Adapty creates and links user profiles, see [How profiles work](how-profiles-work). ::: ## Finding users In the Profiles list, you can search for a specific user by: - **Profile ID**: Adapty's internal identifier for the user (also called Adapty ID). - **Customer user ID**: Your app's identifier for the user, if you've set one. - **Email**: The user's email, if sent as a custom attribute. - **Transaction ID**: The store transaction ID from a purchase. Click any row to open the user's full profile. ## Subscription state In the Profiles list, you can filter and sort users by subscription state. The state values are: | **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. | ## 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' }} /> You can send additional user properties to Adapty using the SDK. By default, Adapty sets: | Property | Description | | ---------------- | ------------------------------------------------------------ | | Customer user ID | An identifier of your end user in your system. | | Adapty ID | Internal Adapty identifier of your end user, called Profile ID. | | IDFA | The Identifier for Advertisers, assigned by Apple to a user's device. Requires App Tracking Transparency (ATT) permission on iOS 14+. Not available on Android. | | Country | Country of your end user. | | OS | The operating system used by the end user. | | Device | The end-user-visible device model name. | | Install date | The date when the user was first recorded in Adapty: <ul><li>The date the user was created. </li><li>If the user installed your app before you integrated Adapty, the install date reflects the date of their first transaction.</li><li>If applicable, the date provided during a historical data import.</li></ul> | | Created at | The date the user was created. | Send at least your internal user ID or user email. This lets you find users by these identifiers in the Profiles list. After you install the SDK, Adapty automatically collects user events from the payment queue and displays them in the user profile. The attributes in the table above are collected automatically — you do not need to send them. ### Custom attributes In the **Attributes** section of a profile, you can see custom attributes set via the SDK or API. You can also assign attributes manually using the **Add attribute** button. <img src="/assets/shared/img/378c1fb-add_attribute.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> ## Granting a subscription In a profile, you can extend an active subscription or grant a user lifetime access to an access level — without requiring them to make a purchase. <img src="/assets/shared/img/b1d74fd-edit_paid_access_level.webp" style={{ border: '1px solid #727272', width: '700px', display: 'block', margin: '0 auto' }} /> This is most useful for: - Compensating a user after a billing or support issue. - Running manual promotions or beta programs. - Testing subscription flows without a real purchase. To grant access, open the user's profile, go to the **Access levels** section, and click **Edit**. Set the expiration date and save. The expiration date must be in the future and cannot be decreased once set. Adjusting it for active subscriptions does not affect ongoing payments. :::note Granting access does not create App Store or Google Play purchase events. The user's event feed and analytics will differ from a real purchase flow. ::: You can also grant access programmatically using the [Grant access level](api-adapty/operations/grantAccessLevel) API method. ## Sharing paid access between user accounts :::link Main article: [Sharing paid access between user accounts](sharing-paid-access-between-user-accounts) ::: ### Access sharing history When access levels are shared or transferred, the user’s profile shows a link to the connected profile — the profile that shared access, or the profile that received it. To view the connected profile, in the user’s **Profile**, click the link next to the access level. <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 */ }} /> ## Next steps - To understand how Adapty creates and links profiles, see [How profiles work](how-profiles-work). - To configure the access sharing policy, see [Sharing paid access between user accounts](sharing-paid-access-between-user-accounts). - To grant access programmatically, see the [Grant access level](api-adapty/operations/grantAccessLevel) API method. --- # 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). --- no_index: true --- **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. --- no_index: true --- import Callout from '../../../components/Callout.astro'; :::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 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 kế hoạch A/B test khả thi mà không cần tự nghiên cứu. [Growth Autopilot](autopilot) kiểm tra paywall của bạn, so sánh với đối thủ cạnh tranh và tạo ra các gợi ý từ dữ liệu ẩn danh của 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 cái nào có tỷ lệ chuyển đổi tốt nhất — không cần thay đổi code. Ví dụ, bạn có thể thử nghiệm: - Giá các 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ị. - **Đối với flow**: Ít nhất hai [flow](adapty-flow-builder). - **Đối với paywall**: Ít nhất hai [paywall](paywalls). - **Đối 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 không thể tính lượt xem paywall trong bài test và số liệu thống kê 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 \{#ab-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 */ }} /> **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\} --- no_index: true --- 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: "Growth Autopilot: Cách hoạt động" description: "Hiểu rõ logic đằng sau Growth Autopilot và tin tưởng chúng tôi giúp tăng trưởng doanh thu của bạn." --- [Growth Autopilot](autopilot) giúp bạn xác định những thử nghiệm 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ì phải đoán mò điều gì có thể hiệu quả, bạn nhận được các đề xuất cụ thể cho những bài test 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 Autopilot vận hành — dữ liệu nó sử dụng, cách đánh giá các cơ hội, và lý do 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. ## Autopilot thực sự làm gì \{#what-autopilot-actually-does\} Autopilot 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 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 - **Xu hướng thị trường**: cách các ứng dụng tương tự cấu trúc ưu đãi và mức giá họ đặt ra - **Lịch sử thử nghiệm của bạn**: những thử nghiệm bạn đã chạy và những gì chúng tiết lộ - **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 Autopilot sử dụng AI để đánh giá tổng hợp các yếu tố này và chuyển chúng thành các A/B test bạn có thể khởi chạy ngay. Bạn nhận được một kế hoạch sẵn sàng mà không cần nghiên cứu đối thủ cạnh tranh hay đoán xem nên test gì tiếp theo. ## Dữ liệu đằng sau Autopilot \{#the-data-behind-autopilot\} Mỗi đề xuất được xây dựng từ ba nguồn dữ liệu chính phối hợp với nhau. #### Dữ liệu của chính ứng dụng bạn \{#your-apps-own-data\} Autopilot xem xét hiệu suất hiện tại của ứng dụng bạn: - 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 cung cấp cho Autopilot một nền tảng để làm việc trước khi đề xuất bất kỳ thay đổi nào. :::note Chúng tôi không sử dụng dữ liệu hiệu suất của ứng dụng bạn để huấn luyện đề xuất cho các ứng dụng khác. Dữ liệu của bạn được giữ bí mật. ::: #### Phân tích paywall \{#paywall-analysis\} Autopilot 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 thiết lập bởi các ứng dụng có hiệu suất cao nhất trong danh mục của bạn. Nó đánh giá lựa chọn bố cục, nội dung, cách trình bày gói đăng ký, và các yếu tố hướng chuyển đổi như huy hiệu tiết kiệm hoặc phần đánh giá. Phân tích này tạo ra hai loại đề xuất: - **Đề xuất dựa trên chuẩn** dựa trên những gì các ứng dụng có hiệu suất cao làm khác biệt — mỗi đề xuất được hỗ trợ bởi một số liệu cụ thể (ví dụ: "Được sử dụng bởi 72% ứng dụng Giáo dục có hiệu suất cao"). - **Đề xuất phân tích trực quan** được AI tạo ra từ ảnh chụp màn hình của bạn, bao gồm cải thiện 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ể [khởi chạy thành A/B test](autopilot-execute-plan). #### Dữ liệu đối thủ cạnh tranh \{#competitor-data\} Autopilot so sánh thiết lập 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ẫu phổ biến trong danh mục của bạn. Những so sánh này theo từng quốc gia, vì giá cả và cấu trúc của đối thủ cạnh tranh khác nhau giữa các thị trường. Giá của đối thủ cạnh tranh đến từ các nguồn bên thứ ba và công khai như App Store — khác với dữ liệu mạng Adapty ẩn danh được sử dụng bởi phân tích chỉ số. Theo cách này, bạn đang test những chiến lược đã hiệu quả với các ứng dụng giống bạn, không chỉ là các ý tưởng ngẫu nhiên. Khi xem phân tích, bạn có thể so sánh chuẩn và giá đối thủ cạnh tranh song song. Nếu các ứng dụng tương tự đang làm tốt hơn với mức giá hoặc 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 Autopilot tự động chọn đối thủ cạnh tranh phù hợp dựa trên những gì bạn có thể cạnh tranh thực tế. Chúng tôi thường khuyến nghị giữ nguyên những đề xuất này thay vì thêm các ứng dụng quá vượt trội hoặc quá tụt hậu. Nếu ứng dụng của bạn thuộc nhiều danh mục, bạn có thể muốn điều chỉnh danh sách để tập trung vào phân khúc thị trường phù hợp nhất. ::: #### Chuẩn ngành \{#industry-benchmarks\} Autopilot 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 thấy bạn so sánh như thế nào 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 và không bao giờ gắn với một ứng dụng cụ thể. 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 danh mục và quốc gia của bạn. Điều này giúp bạn thấy liệu bạn đang hoạt động kém hơn, khoảng trung bình, hay đã vượt trước. #### Dữ liệu thị trường địa lý \{#geographic-market-data\} Autopilot phân tích từng thị trường địa lý riêng lẻ — dựa trên các mẫu trên mạng 20.000 ứng dụng Adapty — để xác định nơi điều chỉnh giá theo khu vực có thể mở khóa thêm doanh thu. Với mỗi quốc gia, nó đánh giá: - **Tỷ lệ chuyển đổi**: Mức độ tỷ lệ từ cài đặt đến trả tiền so sánh với mức trung bình toàn cầu. Tỷ lệ cao hơn có thể cho thấy có chỗ để tăng giá; tỷ lệ thấp hơn có thể báo hiệu độ 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), cho biết sức mua của người dân quốc gia đó. 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ừ [đề xuất giá theo địa lý](autopilot-growth-plan#geo-pricing-hypotheses) trong kế hoạch tăng trưởng của bạn. ## Cách Autopilot quyết định đề xuất gì \{#how-autopilot-decides-what-to-recommend\} Autopilot tạo ra một tập hợp các đề xuất để cải thiện tỷ lệ chuyển đổi paywall của bạn. Những đề xuất này được thiết kế để thử nghiệm từng cái một nhằm đo lường đáng tin cậy tác động của từng thay đổi. Đây là cách Autopilot đưa ra đề xuất: 1. **Tìm cơ hội lớn nhất** Autopilot xem xét giá, sản phẩm và hiệu suất phễu của bạn, sau đó so sánh với các mẫu 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 — vì vậy các đề xuất về giá phù hợp với mức giá người đăng ký của bạn thực sự trả. Nó tìm kiếm nơi bạn có nhiều cơ hội cải thiện nhất, dù đó là điều chỉnh giá, thêm 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ử test hiện có của bạn. Autopilot biết những thử nghiệm bạn đã chạy, thử nghiệm nào thắng, và những hướng nào vẫn còn đáng khám phá. Đề xuất tiếp theo xây dựng dựa trên những gì thử nghiệm trước tiết lộ thay vì theo một trình tự cố định. 3. **Chạy test người thắng so với người thách thức** Sau mỗi thử nghiệm, người thắng trở thành nền tảng mới của bạn. Kết quả đó định hình đề xuất tiếp theo trong kế hoạch tăng trưởng của bạn — Autopilot giữ lại những gì đã hiệu quả, loại bỏ những gì không hiệu quả, và chọn test tiếp theo từ đó. 4. **Giữ nó thực tế** Autopilot chỉ đề xuất các test bạn có thể khởi chạy với sản phẩm và thiết lập hiện có, hoặc với những thay đổi nhỏ như tạo sản phẩm mới hoặc điều chỉnh giá. Mục tiêu là giữ cho việc test nhanh và dễ quản lý. 5. **Hiển thị lý do** Với mỗi đề xuất, Autopilot cung cấp một giả thuyết rõ ràng giải thích chính xác lý do test này đáng chạy. Bạn sẽ thấy các chỉ số hiện tại của bạn so sánh với đối thủ cạnh tranh và mức trung bình ngành như thế nào, cơ hội là gì, và những chỉ số chính 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 test dạy bạn điều gì đó và đưa bạn đến gần hơn với 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 đề xuất không bao giờ cạn kiệt. Mỗi test đã hoàn thành trở thành cơ sở cho các thử nghiệm tiếp theo. Miễn là bạn tiếp tục test, Autopilot tiếp tục đề xuất những gì nên thử tiếp theo. Để làm mới dữ liệu thị trường bên dưới, 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ẽ kéo vào giá đối thủ cạnh tranh được cập nhật, chuẩn chuyển đổi và xu hướng danh mục, đồng thời đóng góp bất kỳ giả thuyết mới được xác định nào vào kế hoạch tăng trưởng của bạn mà không làm xáo trộn những gì đã có. Các giả thuyết do AI tạo ra, giả thuyết tùy chỉnh, và A/B test đang tiến hành của bạn được bảo toàn qua các lần chạy lại. Sau khi bạn đã tối ưu hóa nền tảng của mình, bạn cũng có thể chọn cạnh tranh với các đối thủ tiên tiến hơn. Cách tiếp cận lặp đi lặp lại này giúp bạn tiếp tục 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ử? Khởi chạy [Growth Autopilot](autopilot-analysis) để phân tích paywall của bạn và tạo kế hoạch tăng trưởng với các A/B test. Sử dụng [trình hướng dẫn tích hợp](autopilot-execute-plan) để khởi chạy các test phức tạp một cách liền mạch: nó sẽ hướng dẫn bạn qua quá trình 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 Growth Autopilot 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 **Growth Autopilot** và nhấn 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. Growth Autopilot cần ả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ấn **Confirm & Analyze** để tiếp tục. Adapty 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 paywall và tải lên ảnh chụp màn hình, Adapty phân tích paywall của bạn dựa trên các mẫu thiết kế đã được kiểm chứng, làm nổi bật cả những điểm tốt lẫn 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 việc bạn sử dụng các mẫu đã được kiểm chứng giúp tối đa hóa tỷ lệ chuyển đổi. Ví dụ: huy hiệu tiết kiệm dễ thấy, phần đánh giá của người dùng nổi bật, hoặc bảng phân tích gói đăng ký rõ ràng. #### Những gì cần cải thiện trên paywall \{#what-to-fix-on-your-paywall\} Adapty phân loại các đề xuất thành hai nhóm: - **Đề xuất dựa trên benchmark**: Gợi ý dựa trên dữ liệu từ các ứng dụng hoạt động tốt nhất trong danh mục của bạn. Mỗi đề xuất bao gồm một số liệu benchmark (ví dụ: "Được dùng bởi 72% ứng dụng Giáo dục hoạt động tốt nhất") và mô tả những gì cần thay đổi. - **Đề xuất từ phân tích hình ảnh**: 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 đề xuất benchmark. Bạn có thể thêm các gợi ý từ phân tích hình ảnh vào kế hoạch theo cách thủ công. ::: Nhấn **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 \{#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 với mức trung bình của danh mục. Sử dụng biểu đồ này kết hợp với [phễu chuyển đổi](#conversion-funnel): - 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 các ưu đãi đang được định giá thấp hơn mức cần thiết. 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 qua và chia doanh thu họ tạo ra cho số lượng của họ. #### 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 ở nơi khác trong dashboard — mỗi chỉ số đo lường một điều 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ố cao hơn Activation ARPU nhiều lần. - **[Biểu đồ doanh thu](revenue), bộ lọc Period đặ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 được thực hiện bởi cohort trong khoảng thời gian 90 ngày. - **[Doanh thu cohort](analytics-cohorts) (90 ngày)**: 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" 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 trình bày kế hoạch tăng trưởng của bạn — 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 bằng 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 để làm mới 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 kế hoạch tăng trưởng để lọc giả thuyết theo loại: - **Top priority** bao gồm các giả thuyết có tác động cao nhất đáng được chú ý. 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ả các giả thuyết trong kế hoạch đang hoạt động của bạn. - Giả thuyết **Pricing** khám phá các 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 thông tin thị trường. - Giả thuyết **Visual** là các gợi ý cải tiế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ố trực quan 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à các gợi ý 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. Kiểm tra các giả thuyết này từng cái một, theo bất kỳ thứ tự nào. Các bài kiểm tra geo-pricing là ngoại lệ — đối tượng của chúng không trùng lặp nên có thể chạy song song. ### Giả thuyết Geo-pricing \{#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 khu vực. ::: Mở tab **Geo-pricing** để xem danh sách các khuyến nghị geo-pricing. Mỗi khuyến nghị nhắm mục tiêu một quốc gia với một thay đổi giá duy nhất và chạy dưới dạng 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 khuyến nghị 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 ý geo-pricing \{#how-adapty-makes-geo-pricing-suggestions\} - Các khuyến nghị về giá dựa trên dữ liệu App Store. A/B test kết quả có thể chạy trên cả App Store và Google Play. - Tỷ lệ phần trăm thay đổi giá là như nhau trên 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á được hiển thị bằng đơ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á được hiển thị bằng USD. ### Nhãn trạng thái giả thuyết \{#hypothesis-status-badges\} 1. **Lightning bolt** — chỉ ra các gợi ý ưu tiên hàng đầu. 2. **Trạng thái đồng bộ sản phẩm** — xuất hiện khi cần thực hiện 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 chấp thuận và sẵn sàng để kiểm tra. - **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** — bài kiểm tra đã được tạo nháp nhưng chưa chạy. - **Running** — bài kiểm tra đang chạy. - **Completed** — bài kiểm tra đã hoàn thành. - **Archived test** — bài kiểm tra đã được lưu trữ mà không có kết luận. ## Nhận giả thuyết mới được tạo bởi AI \{#get-new-ai-generated-hypotheses\} Sau mỗi bài kiểm tra, 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ạnh tranh mới nhất, chuẩn mực chuyển đổi và xu hướng danh mục, nhấp vào **Update** Refresh trong tiêu đề kế hoạch tăng trưởng — hoặc **Update Analysis** trên trang chủ Autopilot. Nhấp vào **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 phân tích paywall và nghiên cứu đối thủ cạnh tranh, Adapty sẽ xác định các giả thuyết mới từ kết quả. Chọn những giả thuyết bạn muốn thêm, hoặc nhấp vào **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, 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 quá trình cập nhật trước khi hoàn tất, bạn có thể **tiếp tục** để tiếp tục, hoặc **hủy** để bắt đầu lại. ## 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\} Để xem các gợi ý được tạo ra từ các phân tích trước, nhấp vào **Clock** Clock trong tiêu đề kế hoạch tăng trưởng. Modal **Version history** liệt kê mọi lần chạy trước cho placement — ngày tháng, paywall và số lượng giả thuyết bạn đã chấp nhận lúc đó. Nhấp vào một lần chạy trước để xem các giả thuyết nó đã tạo ra. Bạn có thể thêm bất kỳ giả thuyết nào vào kế hoạch đang hoạt động với **Add to Plan** Plus — hữu ích để xem lại các gợi ý bạn chưa chọn lần đầu. --- # 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 test theo bất kỳ thứ tự nào, nhưng cần chạy **từng test một**. Vì đối tượng của các test định giá theo khu vực địa lý không trùng nhau, chúng có thể chạy song song với nhau. Khi một test kết thúc, hãy đưa chiến lược chiến thắng lên vòng tiếp theo. Mỗi vòng sẽ thu hẹp dần để tìm ra cấu hình hiệu quả hơn cho ứng dụng của bạn. Theo ước tính của chúng tôi, việc chạy đầy đủ bộ test Autopilot có thể giúp tăng doanh thu của bạn **lên tới 80%**. :::important Mỗi gợi ý đều bao gồm thời gian 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 giả thuyết và nhấn **Set Up & Run Test** để khởi chạy 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 riêng 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 như các nguồn bổ sung — nền tảng marketing và mạng quảng cáo. Adapty SDK theo dõi các tương tác của người dùng với 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 thời gian thực](#data-processing). - **Chỉ số nâng cao**: Các cửa hàng ứng dụng hiển thị các chỉ số cơ bản như số lượt tải xuống, 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, 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 riêng biệt phân tích các vấ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) để biết danh sách đầy đủ. - **Dự đoán**: Adapty sử dụng các thuật toán machine learning 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 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 những sự kiện này thành [biểu đồ analytics](analytics) và chuyển tiếp chúng theo thời gian thực đến [webhook](webhook), [event feed](event-feed), và [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ần 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ý flow mua hàng của bạn, SDK sẽ chia sẻ thông tin trực tiếp về các sự kiện mua hàng. Nếu bạn sử 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 sử 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 lưu lượng truy cập và chiến dịch quảng cáo. Nếu bạn thiết lập [Adapty User Acquisition](adapty-user-acquisition), Adapty có thể tự xử lý dữ liệu chiến dịch quảng cáo của bạn mà không cần 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 [Xử lý 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 analytics gần thời gian thực, cho phép người dùng phản ứng nhanh với các thay đổi trong các chỉ số quan trọng. - **Biểu đồ analytics**: dữ liệu xuất hiện với **độ trễ 15–30 phút** sau 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ế, và tổng hợp dữ liệu. - **[Event feed](event-feed)**: cập nhật theo thời gian thực, ngay khi cửa hàng gửi một sự kiện. - **[Webhook](webhook) và 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ễ analytics 15–30 phút không áp dụng ở đây. Dịch vụ nhận có thể có thời gian xử lý riêng của nó. Mỗi bề mặt có thời gian 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ự khác biệt 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 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 hoa hồng: * [Chương trình Doanh nghiệp Nhỏ App Store](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 yếu tố khác có làm giảm hoa hồng giao dịch của bạn hay không: * [Gia hạn gói đăng ký App Store kéo dài hơn 1 năm](https://developer.apple.com/app-store/subscriptions/) — hoa hồng 15% * Mức giá theo 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 toán thuế.** Apple và Google 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 hiển thị giá trị đó như nguyên gốc. 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 có hiệu lực tại đó. - **Quy tắc xử lý thuế của cửa hàng**. Ở một số khu vực tài phán, cửa hàng thu và nộp thuế thay cho nhà phát triển; ở những nơi khác, nhà phát triển 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 các mức khác nhau tùy thuộc vào quy định địa phương. Mức thuế có thể khác nhau đáng kể giữa các ứng dụng — 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ửa hàng, và (đối với App Store) danh mục thuế được gán. Để 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ử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: Mức thuế 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) ::: * Nguồn dữ liệu bị cấu hình sai hoặc 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 các tích hợp với cửa hàng và nền tảng bên thứ ba của bạn đã được cấu hình và hoạt động. * Nếu bạn so sánh biểu đồ Adapty với các nền tảng analytics khác, bạn có thể nhận thấy sự khác biệt. Đây là hành vi bình thường có thể xuất phát từ sự khác biệt trong cách xử lý dữ liệu. Đọc bài viết [hướng dẫn về sự khác biệt](discrepancies-and-troubleshooting) để tìm hiểu về các nguyên nhân phổ biến gây ra sự khác biệt 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ố analytics của Adapty, được tổ chức theo danh mục." --- Đây là tổng quan về các chỉ số có sẵn trong Adapty Analytics. Dùng bài này để hiểu từng chỉ số đo lường điều gì và khác nhau như thế nào so với các chỉ số liên quan. Để hiểu sâu hơn về cách Adapty xử lý dữ liệu analytics, xem [Cách Adapty analytics hoạt động](how-adapty-analytics-works). :::note Bài viết này không bao gồm các chỉ số của [Adapty User Acquisition](adapty-user-acquisition). Đọc [UA analytics](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.v.). ::: ## 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 chính | |--------|-------------|----------------| | [Revenue](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ỉnh 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 như MRR nhưng theo quy mô hàng năm. Hữu ích để dự đoán doanh thu 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 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 đã mua hàng trong kỳ được chọn, bao gồm cả giao dịch đã hoàn tiền. Luôn cao hơn ARPU | | [LTV (lifetime value)](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 kỳ đơn lẻ), LTV thể hiện tổng doanh thu trong suốt vòng đời khách hàng. Có thể xem theo số lần gia hạn hoặc theo ngày dương lịch | | [Predicted LTV](predicted-ltv-and-revenue) | Ước tính lifetime value 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 tế, dự báo giá trị tương lai từ dữ liệu lịch sử retention của cohort. Có sẵn cho 3, 6, 9, 12, 18 và 24 tháng | | [Predicted revenue](predicted-ltv-and-revenue) | Ước tính tổng doanh thu mà một cohort sẽ tạo ra | Ước tính hướng về tương lai. Khác với Revenue thực tế, dự đoán tổng số một cohort sẽ tạo ra trong khung thời gian được 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. | | [Refund events](refund-events) | Số lần hoàn tiền cho giao dịch hoặc gói đăng ký | Gán cho ngày hoàn tiền, không phải ngày mua ban đầu. | | [Refund money](refund-money) | Tổng số tiền đã hoàn trong kỳ được chọn | Tác động tài chính của việc hoàn tiền. Tính trước khi trừ phí cửa hàng. Khác với Refund events (số lượt), chỉ số này hiển thị số tiền thực tế | ### Người đăng ký và tỷ lệ 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 phễu. | Chỉ số | Mô tả | Điểm khác biệt chính | |--------|-------------|----------------| | [Installs](installs) | Số lần cài đặt ứng dụng trong kỳ | Tính theo một trong các định nghĩa sau tùy theo [định nghĩa install](general#4-installs-definition-for-analytics): <br /> • Lượt cài đặt trên thiết bị (người dùng cài lại ứng dụng được tính thêm một lần) <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 trừ hoàn toàn — nếu không có người dùng nào được xác định, kết quả là 0) | | [New trials](new-trials) | Số trial được kích hoạt trong kỳ | Đếm mọi lần bắt đầu trial, kể cả khi trial đã hết hạn hoặc đã chuyển sang trả tiền tại thời điểm bạn xem biểu đồ | | [Active trials](active-trials) | Số trial chưa hết hạn | Chỉ tính các trial đang hoạt động vào cuối kỳ | | [New subscriptions](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 trial lẫn chuyển từ trial sang trả tiền | Không bao gồm gia hạn và tái kích hoạt. Không giống sự kiện tích hợp `subscription_started`, vốn chỉ tính lần mua đầu tiên không qua trial — chuyển đổi từ trial sẽ kích hoạt `trial_converted` thay thế | | [Active subscriptions](active-subscriptions) | Số gói đăng ký trả tiền chưa hết hạn | Không bao gồm trial và gói đăng ký đã hủy gia hạn | | [Install to trial](analytics-conversion#install---trial) | Tỷ lệ phần trăm người dùng cài đặt đã bắt đầu trial | 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 Paywall view to trial. Hai chỉ số này cũng có thể khác 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 trial từ [promoted in-app purchase](https://developer.apple.com/documentation/storekit/supporting-promoted-in-app-purchases-in-your-app). | | [Paywall view to trial](analytics-conversion#paywall-view---trial) | Tỷ lệ phần trăm người xem paywall đã bắt đầu trial | Chỉ tính người dùng đã xem paywall, nên tỷ lệ có thể cao hơn Install to trial | | [Trial to paid](analytics-conversion#trial---paid) | Tỷ lệ phần trăm người dùng trial đã mua gói đăng ký | Đo lường chất lượng trial và hiệu quả chuyển đổi. Khác với Install to paid, chỉ tập trung vào người dùng đã hoàn thành trial | | [Install to paid](analytics-conversion#install---paid) | Tỷ lệ phần trăm người cài đặt đã mua gói đăng ký lần đầu | 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 Paywall view to paid. Bao gồm cả mua trực tiếp lẫn chuyển từ trial sang trả tiền | | [Paywall view to paid](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 Install to paid. Bao gồm người dùng đã hoàn thành trial trước | ### Retention và gia hạn gói đăng ký \{#retention-and-subscription-renewal\} Các chỉ số này theo dõi mức độ ứng dụng giữ chân người đăng ký trả tiền theo thời gian. | Chỉ số | Mô tả | Điểm khác biệt chính | |--------|-------------|----------------| | [Retention](analytics-retention) | Tỷ lệ người đăng ký ban đầu vẫn còn sau mỗi kỳ thanh toán — lần gia hạn thứ 1, thứ 2, v.v. | Theo dõi người đăng ký từ lần thanh toán đầu tiên trở đi. Khác với các chỉ số kỳ-sang-kỳ bên dưới, luôn so sánh với nhóm ban đầu, giúp bạn thấy toàn cảnh ngay lập tức | | [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 quá trình 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ỳ 2 sang kỳ 3 | Cho thấy sự ổn định retention 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ỳ 3 sang kỳ 4 | Chỉ số retention trung hạn | | [4th to 5th period](analytics-conversion#4th-period---5th-period) | Tỷ lệ phần trăm gia hạn từ kỳ 4 sang kỳ 5 | Chỉ số 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 còn đăng ký sau 6 tháng | Đo theo thời gian dương lịch, không theo số lần gia hạn. Người đăng ký hàng năm được tính là còn giữ ở 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 còn đăng ký sau 12 tháng | Mốc retention 1 năm | | [2 Years+](analytics-conversion#2-years-) | Tỷ lệ phần trăm người đăng ký lần đầu vẫn còn đăng ký sau 24 tháng | Mốc retention dài hạn | ### Churn \{#churn\} Các chỉ số này đo lường số người đăng ký và người dùng trial 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) | Trial mà người dùng đã tắt tự động gia hạn | Người dùng vẫn có quyền truy cập trial cho đến khi hết hạn nhưng sẽ không tự động chuyển sang trả tiền. Khác với Subscriptions renewal cancelled, áp dụng cho người dùng trial chưa trả tiền | | [Expired (churned) trials](expired-churned-trials) | Trial đã hết hạn — người dùng mất quyền truy cập tính năng premium | Người dùng đã mất quyền truy cập. Gán cho ngày hết hạn, kể cả khi người dùng đã hủy gia hạn từ kỳ trước. Có thể nhóm theo lý do (tự nguyện hay thanh toán) | | [Subscriptions renewal cancelled](cancelled-subscriptions) | 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ỳ kết thúc. Báo hiệu nguy cơ churn, chứ không phải churn thực tế — 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) | Gói đăng ký đã hết hạn — người dùng mất quyền truy cập tính năng premium | Churn thực tế. Người dùng đã mất quyền truy cập. Gán cho ngày hết hạn, kể cả khi người dùng đã hủy gia hạn từ kỳ trước. Có thể nhóm theo lý do (tự nguyện hay thanh toán) | ### Vấn đề thanh toán và khôi phục 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 khôi phục doanh thu bị mất do vấn đề thanh toán. | Chỉ số | Mô tả | Điểm khác biệt chính | |--------|-------------|----------------| | [Grace period](grace-period) | Gói đăng ký đã vào thời gian ân hạn do lỗi thanh toán | Bao gồm người dùng đã vượt quá thời gian ân hạn và mất quyền truy cập | | [Grace period to paid](analytics-conversion#grace-period---paid) | Tỷ lệ phần trăm người dùng trong thời gian ân hạn đã gia hạn trước khi hết thời gian ân hạn | Một 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?" | | [Grace period converted](grace-period-converted) | Số lượng tuyệt đối 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 Grace period to paid, nhưng hiển thị dưới dạng số lượng thay vì tỷ lệ phần trăm | | [Grace period converted revenue](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 | | [Billing issue](billing-issue) | Gói đăng ký đã vào trạng thái lỗi thanh toán | Bắt đầu sau khi thời gian ân hạn hết. Khác với Grace period, chỉ tính người dùng đã mất quyền truy cập premium | | [Billing issue to paid](analytics-conversion#billing-issue---paid) | Tỷ lệ phần trăm người dùng gặp lỗi thanh toán đã gia hạn trước khi kết thúc chu kỳ thanh toán | Một 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?" | | [Billing issue converted](billing-issue-converted) | Số lượng tuyệt đối 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 Billing issue to paid, nhưng hiển thị dưới dạng số lượng thay vì tỷ lệ phần trăm | | [Billing issue converted revenue](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 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 **Chỉ số toàn cục liên quan** hiển thị chỉ số tương ứng trong phần analytics toàn cục. | Chỉ số | Mô tả | Điểm khác biệt chính | 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ả tiền cho paywall hoặc placement này | Cùng cách tính với ARPPU toàn cục nhưng giới hạn trong một paywall hoặc placement duy nhất | [ARPPU](arppu) | | [ARPAS](paywall-metrics#arpas) | Doanh thu chia cho số người đăng ký đang hoạt động (trial và trả tiền) | Tính cả người dùng trial. Khác với ARPPU, 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 paywall hoặc placement được hiển thị | Đếm mọi lần hiển thị. Một người dùng xem cùng một paywall hai lần được tính là 2 lượt xem | — | | [Unique views](paywall-metrics#unique-views) | Số người dùng duy nhất đã xem paywall hoặc placement | Mỗi người dùng chỉ được tính một lần bất kể đã xem bao nhiêu lần. Khác với Views, đ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ần mua chia cho tổng số lượt xem | Dùng tổng 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ần mua chia cho số lượt xem duy nhất | 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ố trial bắt đầu chia cho tổng số lượt xem | Đo lường mức độ hiệu quả của paywall trong việc chuyển lượt xem thành trial | [Paywall view to trial](analytics-conversion#paywall-view---trial) | | [Unique CR to trials](paywall-metrics#unique-cr-to-trials) | Số trial bắt đầu 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 cho paywall này: mua mới, chuyển đổi từ trial, nâng cấp, hạ cấp và gói đăng ký quay lại | Không bao gồm gia hạn. | [Revenue](revenue) | | [Trials](paywall-metrics#trials) | Tổng số trial được kích hoạt qua paywall này | Chỉ giới hạn trong paywall này | [New trials](new-trials) | | [Trials canceled](paywall-metrics#trials-canceled) | Số trial mà người dùng đã tắt tự động gia hạn | Chỉ giới hạn trong các trial của paywall này | [Trials renewal cancelled](trials-renewal-cancelled) | | [Refund rate](paywall-metrics#refund-rate) | Số lần hoàn tiền chia cho số lần mua lần đầu (không tính gia hạn) | Một tỷ lệ (%), không phải số lượng. Chuẩn hóa số lần hoàn tiền theo số lần 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 flow onboarding từ màn hình đầu đến cuối | Chỉ dành cho placement và onboarding. Đếm mọi lần hoàn thành, kể cả lần hoàn thành lặp lại của cùng một người dùng | — | | Unique completions | Số người dùng duy nhất đã hoàn thành flow onboarding | Chỉ dành cho placement và onboarding. Mỗi người dùng chỉ được tính một lần. Khác với Completions, đo lường bao nhiêu cá nhân đã hoàn thành flow | — | | Unique completions rate | Số lần hoàn thành duy nhất chia cho số lượt xem duy nhất | Chỉ dành cho placement và onboarding. Đo lường hiệu quả onboarding: bao nhiêu phần trăm người dùng đã bắt đầu và 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\} --- no_index: true --- 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, doanh thu dự đoán, tiền hoàn trả và các con số doanh thu trong báo cáo cohort và A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty quy đổi từng giao dịch sang USD bằng 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 đơn vị tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price` và `currency_code` trong webhook - Các cột `price` và `currency_code` trong các bản xuất S3, GCS và BigQuery - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo đơn vị 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 xuất và tự tổng hợp lại. ## Giá gia hạn \{#renewal-pricing\} --- no_index: true --- 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\} --- no_index: true --- 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, doanh thu dự đoán, tiền hoàn trả và các con số doanh thu trong báo cáo cohort và A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty quy đổi từng giao dịch sang USD bằng 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 đơn vị tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price` và `currency_code` trong webhook - Các cột `price` và `currency_code` trong các bản xuất S3, GCS và BigQuery - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo đơn vị 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 xuất và tự tổng hợp lại. ## Giá gia hạn \{#renewal-pricing\} --- no_index: true --- 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\} --- no_index: true --- 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, doanh thu dự đoán, tiền hoàn trả và các con số doanh thu trong báo cáo cohort và A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty quy đổi từng giao dịch sang USD bằng 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 đơn vị tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price` và `currency_code` trong webhook - Các cột `price` và `currency_code` trong các bản xuất S3, GCS và BigQuery - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo đơn vị 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 xuất và tự tổng hợp lại. ## Định giá gia hạn \{#renewal-pricing\} --- no_index: true --- 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\} --- no_index: true --- 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, doanh thu dự đoán, tiền hoàn trả và các con số doanh thu trong báo cáo cohort và A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty quy đổi từng giao dịch sang USD bằng 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 đơn vị tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price` và `currency_code` trong webhook - Các cột `price` và `currency_code` trong các bản xuất S3, GCS và BigQuery - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo đơn vị 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 xuất và tự tổng hợp lạ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 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\} --- no_index: true --- 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, doanh thu dự đoán, tiền hoàn trả và các con số doanh thu trong báo cáo cohort và A/B test. Không có tùy chọn nào để hiển thị theo đơn vị tiền tệ khác. Adapty quy đổi từng giao dịch sang USD bằng 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 đơn vị tiền tệ địa phương có thể xem theo từng giao dịch tại: - Các trường `price` và `currency_code` trong webhook - Các cột `price` và `currency_code` trong các bản xuất S3, GCS và BigQuery - Trang hồ sơ người dùng (xem theo từng giao dịch) Để báo cáo tài chính theo đơn vị 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 xuất và tự tổng hợp lạ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, Đố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 cách khác nhau. Cùng một sự kiện hoàn tiền có thể làm giảm ngay một biểu đồ, làm giảm biểu đồ khác theo kiểu hồi tố (thay đổi giá trị kỳ trước), hoặc hoàn toàn không ảnh hưởng đến biểu đồ thứ ba. Bảng dưới đây mô tả quy tắc cho từng chỉ số. | Chỉ số | Áp dụng hoàn tiền? | 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 quá 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ó còn hoạt động | Không | Giá trị kỳ trước có thể giảm sau khi hoàn tiền. | | [ARR](arr) | Có, hồi tố | Tương tự MRR | Không | Giá trị 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 đã được hoàn tiền vẫn được tính vào mẫu số người dùng đã thanh toá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ố lượng bao gồm các gói đăng ký bị hoàn tiền sau đó. So sánh với [Refund events](refund-events) để biết tác động thực tế. | | [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 so với [Active subscriptions](active-subscriptions) hoặc [Revenue](revenue) cho cùng một cohort. | | [Cohort revenue](analytics-cohorts) | Có, tích lũy | Ngày hoàn tiền | Không (các khoản khấu trừ tích lũy không đẩy doanh thu cohort xuống âm) | Hoàn tiền trừ vào doanh thu cohort khi phát sinh. | | Chỉ số [Paywall](paywall-metrics) / chỉ số [A/B test](results-and-metrics) (số lượng) | **Không** | — | Không | Số lượng Subscribers, Paying Subscribers và ARPPU trên các trang này không khấu trừ hoàn tiền. | | GCS / S3 exports | Hoàn tiền như một hàng sự kiện riêng | `event_datetime` = thời điểm hoàn tiền | Các cột net 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\} --- no_index: true --- 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 trong analytics của Adapty để theo dõi mức độ tương tác của người dùng và xu hướng gói đăng ký." --- Cohort trong Adapty được thiết kế để trả lời một số câu hỏi quan trọng: 1. Cohort sẽ hoàn vốn vào ngày nào? 2. Ứng dụng thu được bao nhiêu tiền từ một cohort cụ thể? 3. Tôi có thể chi 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 với 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 cần bất kỳ cấu hình bổ sung nào từ phía bạn. ## Cohort theo 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 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à kéo theo cách tiếp cận phân tích cũng thay đổi theo. 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 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ý như consumable hoặc sản phẩm mua một lần. Trong 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 cho phép 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ế độ theo lần gia hạn, chúng bị nhóm 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ì hoãn góp phần tạo ra mô hình phân phối này, vì 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** thể hiện tỷ lệ giữ chân và rời bỏ của cohort từ lần thanh toán này sang lần thanh toán khác mà không quan tâm đến ngày cụ thể. Những người dùng trễ đã thanh toán với bất kỳ độ trễ nào (có thể là nhiều tháng) đều được tính vào chu kỳ gói đăng ký của họ. Cách tiếp cận này không phản ánh tình hình doanh thu theo lịch nhưng tiện lợi hơn để phân tích tỷ lệ giữ chân và rời bỏ của cohort, cũng như rút ra insights từ hành vi của họ. Hãy chọn chế độ phù hợp hoặc dùng cả hai để có thêm nhiều kết luận và ý tưởng. ## Cách Adapty xây dựng cohort \{#how-adapty-builds-cohorts\} Hãy xem ví dụ về cohort theo lần gia hạn để hiểu cách bảng được tạo 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 (lượt mua). Mỗi hàng của 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 trọn đời/không phải gói đăng ký. Mỗi cột tiếp theo trong hàng hiển thị số người dùng đã gia hạn gói đăng ký đến chu kỳ đó. M3 là tháng thứ 3, nghĩa là người đăng ký đã gia hạn liên tiếp 3 lần đến thời điểm này; W7 là tuần thứ 7; Y2 là năm thứ 2. Đôi khi bạn có thể thấy P2 trong cohort — P là viết tắt của Period (chu kỳ 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 chu kỳ gia hạn khác nhau trong cùng một cohort. Chúng tôi dùng màu gradient để làm nổi bật sự khác biệt giữa các giá trị cohort. Số càng lớn thì màu càng đậm. 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 (dấu #1). 2. Không loại trừ proceeds và hiển thị doanh thu dưới dạng giá trị tuyệt đối (dấu #2). 3. Khoảng thời gian đang xét là 6 tháng gần nhất, và độ dài cohort là 1 tháng (dấu #3). 4. Hàng **Total** (dấu #4) hiển thị giá trị lũy kế cho mỗi chu kỳ. $442K trong ô đầu tiên của hàng **Total** là tổng doanh thu chu kỳ đầu (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ị tổng số 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 Nov 2023 (dấu #5) hiển thị doanh thu $37,7K từ chu kỳ đầu (kích hoạt gói đăng ký) của những khách hàng đã cài đặt ứng dụng vào tháng 11 năm 2023. Số khách hàng đã cài đặt ứng dụng vào 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 Nov 2023 hiển thị doanh thu $8,77K từ tuần thứ 2 (các gói đăng ký đã gia hạn đến tuần thứ 2) của 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 (dấu #6). Bạn có thể đọc thêm về chúng ở phần dưới 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 trường thả xuống **Columns** (dấu #7). 8. Phía trên bảng bên phải (dấu #8), có một trường thả xuống để tính phí hoa hồng của cửa hàng và 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 và thuế của cửa hàng trong [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ừ trường thả xuống, dữ liệu doanh thu sẽ được tính toán lại dựa trên đó. 9. Ở phần 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) (dấ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 khoảng 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 lên bất kỳ ô nào trong cohort để xem các chỉ số chi tiết cho chu kỳ đó. Các ô có đường kẻ chéo nền là các chu kỳ chưa kết thúc, nên giá trị trong đó có thể còn tăng thêm. ## Độ dài cohort và khung thời gian \{#cohort-length-and-time-frame\} --- no_index: true --- 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 cohort sử dụng dữ liệu từ tất cả các lượt 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ó nút để xuất dữ liệu cohort ra CSV. Bạn có thể mở file này 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 thay đổi tương đối so với thời điểm bắt đầu cohort. ## Subscriptions, payers, tổng doanh thu, ARPU, ARPPU và ARPAS \{#subscriptions-payers-total-revenue-arpu-arppu-and-arpas\} **Subscriptions** là tổng số lượng 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 khung thời gian đã chọn. Theo dõi chỉ số này giúp bạn hiểu hành vi khách hàng và hiệu quả của các sản phẩm đang cung cấp. Thông tin này cho phép bạn tinh chỉnh chiến lược sản phẩm, điều chỉnh nỗ lực marketing và tối ưu hóa luồng 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. Đối với các ứng dụng có lượng lớn sản phẩm mua một lần, chỉ số này có thể làm nổi bật phạm vi thực sự của các sản phẩm bạn cung cấp — cho thấy liệu một lượng lớn người dùng đang mua hàng hay doanh thu đến từ một nhóm nhỏ người mua thường xuyên. Hiểu được số lượng payers giúp đánh giá mức độ tương tác 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. **Total revenue** được tích lũy cho một cohort trong khung thời gian đã chọn (ví dụ: 25/11/2022 — 24/5/2023). Chỉ số này giúp bạn hiểu bạn thu được bao nhiêu tiền từ người dùng trong một cohort cụ thể và tính ROAS. Ví dụ: nếu chi phí quảng cáo tháng 9/2022 là $10.000 và tổng proceeds của cohort tháng 9/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. Hữu ích khi so sánh giá trị này với chi phí cài đặt (CPI) để đánh giá hiệu quả các chiến dịch marketing. **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. Giúp bạn hiểu trung bình mỗi khách hàng trả phí mang lại bao nhiêu tiền. **ARPAS** là doanh thu trung bình trên mỗi người đăng ký đang hoạt động. Được tính bằng tổng doanh thu / số người đăng ký đang hoạt động. Người đăng ký ở đâ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 đăng ký = $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 và thuế của cửa hàng (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 analytics cohort. Để biết thêm chi tiết về cách Adapty tính thuế và hoa hồng trong analytics, vui lòng tham khảo [tài liệu](controls-filters-grouping-compare-proceeds#display-gross-or-net-revenue) của chúng tôi. ## Revenue và Proceeds \{#revenue-vs-proceeds\} Cả Revenue và Proceeds đều là chỉ số tiền. Bạn có thể hiểu Revenue là doanh thu gộp và Proceeds là doanh thu ròng. Revenue không tính phí của App Store / Play Store, trong khi Proceeds thì có. Do đó Proceeds luôn nhỏ hơn Revenue. Mức hoa hồng thực tế bị khấu trừ thay đổi dựa trên nhiều yếu tố, bao gồm khả năng đủ điều kiện tham gia chương trình như [Small Business Program](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 hoa hồng áp dụng cho mọi giao dịch của khách hàng và tính Proceeds dựa trên đó. Để biết thêm thông tin về cách xác định mức hoa hồng, xem tài liệu [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\} Hoàn tiền được trừ khỏi doanh thu cohort theo cách lũy kế khi phát sinh — vào ngày hoàn tiền, không phải ngày mua ban đầu. Để 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). ## Dự đoán: Revenue và LTV \{#prediction-revenue-and-ltv\} **Predicted revenue** là tổng doanh thu ước tính mà một cohort người đăng ký trả phí dự kiến sẽ tạo ra trong khoảng thời gian đã chọn sau khi cohort được tạo. Đượ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à có 100 người dùng trả phí trong một cohort, thì Predicted Revenue sẽ là $5.000. **Predicted LTV** là giá trị trọn đời ước tính trên mỗi người đăng ký trả phí, đại diện cho doanh thu trung bình mà mỗi người đăng ký trả phí dự kiến sẽ 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 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ùng trung bình nhiều ứng dụng trong trường hợp còn lại. Để xem tài liệu chi tiết về mô hình dự đoán của Adapty, vui lòng tham khảo [tài liệu Dự đoán](predicted-ltv-and-revenue) của chúng tôi. Cohort trong Adapty cung cấp thông tin chi tiết về hành vi người dùng và hiệu suất tài chính trong ứng dụng của bạn. Bằng cách phân tích cohort theo lần gia hạn hoặc theo ngày, bạn có thể xác định thời điểm cohort sinh lời, theo dõi doanh thu, tính doanh thu trung bình trên mỗi người dùng và hiểu thời gian cần để 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 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 và kiếm tiền để đạt được thành công tối đa cho ứng dụng. --- # 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 các câu hỏi sau: 1. Lifetime value (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 lợi nhuận dự đoán? Với Dự đoán của Adapty, 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 cách doanh thu, số lượng người dùng trả phí và LTV trung bình sẽ thay đổi theo thời gian. Điều này giúp bạn đưa ra 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 LTV dự đoán 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 3, 6, 9, 12, 18 và 24 tháng sau khi cohort được tạo. Với các ứng dụng có lịch sử rất hạn chế, mô hình sẽ dùng mức trung bình từ nhiều ứng dụng, vì vậy dự đoán cho các ứng dụng mới có thể chưa phản ánh đúng hành vi người dùng cụ thể của chúng. ## Cách mô hình hoạt động \{#how-the-model-works\} Mô hình dự đoán của Adapty sử dụng các mô hình giữ chân từ dữ liệu cohort lịch sử để dự báo doanh thu và LTV trong tương lai. Với mỗi sự kết hợp giữa ứng dụng và loại gói đăng ký, mô hình đo lường cách người dùng trả phí và tổng doanh thu thay đổi từ một kỳ gia hạn sang kỳ tiếp theo. Mô hình tính toán hai tỷ lệ giữ chân — một cho người dùng, một cho doanh thu — dựa trên các cohort lịch sử 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 sự tăng trưởng trong 3, 6, 9, 12, 18 và 24 tháng sau khi cohort được tạo. Dữ liệu được sử dụng hoàn toàn ẩn danh. Mô hình tạo ra hai giá trị cho mỗi cohort: - **Doanh thu dự đoán**: Tổng doanh thu mà một cohort được dự kiến sẽ tạo ra trong khoảng thời gian đã chọn. - **LTV dự đoán**: Doanh thu dự đoán chia cho số lượng người dùng trả phí dự đoá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ử dụng trọng số giữ chân được học từ các cohort lịch sử của chính ứ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 không có đủ lịch sử cho một khoảng dự đoán cụ thể, Adapty sẽ dùng trọng số giữ chân được tính trung bình từ tất cả các ứng dụng cùng loại gói đăng ký. Ví dụ: dự báo 12 tháng cho một ứng dụng mới chỉ hoạt động được 6 tháng sẽ sử dụng trọng số chung. Phương án dự phòng này được áp dụng độc lập theo từng khoảng thời gian, vì vậy cùng một cohort có thể sử dụng trọng số của ứng dụng cho dự đoán 3 tháng và trọng số chung 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 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 Phân tích Cohort hiển thị cột Doanh thu Dự đoán và LTV Dự đoán" 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 sẽ tạo ra trong khoảng thời gian đã chọn sau 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ô hình giữ chân cohort lịch sử của ứng dụng. Cột **Predicted LTV** hiển thị lifetime value ướ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 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. - **Kích thước cohort nhỏ**: Quá ít người dùng trả phí để tạo ra dự báo đá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ô hình mà mô hình kỳ vọng. Chờ thêm vài tuần có thể giải quyết vấn đề này khi có thêm dữ liệu tích lũy. - **Vượt quá khoảng thời gian**: Cohort đã cũ hơn khoảng 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 sau mười hai 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. :::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: "Apple Ads Manager" description: "Get realtime analytics from Apple Ads and manage and optimize your campaigns" --- **Apple Ads Manager** is an all-in-one platform designed to help you manage, optimize, and scale your Apple Ads campaigns more efficiently. It connects your Apple Search Ads performance with key revenue metrics such as installs, trials, subscriptions, and lifetime value without requiring an MMP. With real-time analytics, AI-driven forecasts, and smart automation, Apple Ads Manager eliminates tedious manual bid changes, spreadsheets, and guesswork, and replaces them with clear insights and tools that help you take action faster. With Apple Ads Manager, you get: - **[Overview](ads-manager-overview)**: All key metrics at a glance — spend, revenue, ROAS, CPA, and more — each with a daily trend chart - **Real-time performance data**: Across campaigns, ad groups, and keywords - **End-to-end revenue tracking**: From search → install → trial → subscription → LTV - **AI predictions & recommendations**: For profitable scaling - **Bulk management**: Of bids, budgets, statuses, and structures - **Rule-based automations**: Manage the full keyword lifecycle - **[Market Intelligence](ads-manager-market-intelligence)**: Competitor keyword strategies across 50+ countries - **[CPP A/B Tests](ads-manager-cpp-ab-tests)**: Compare custom product pages head-to-head and find the best performer <CustomDocCardList ids={['adapty-ads-manager-get-started', 'ads-manager-overview', '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']} /> ## Why Apple Ads Manager? Because we give you the **most accurate data in the market.** Unlike the native Apple Ads console or MMPs, our data is **real-time, lossless, and fully connected** to trials, subscriptions, and LTV — without delays or attribution gaps. With its easy implementation and smooth user experience, you can manage everything in one place without having to switch between multiple tools. ## Get started To get started with Apple Ads Manager, follow the [guide](adapty-ads-manager-get-started), and you're all set to explore --- # File: adapty-ads-manager-get-started --- --- title: "Bắt đầu với Apple 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" --- [Apple Ads Manager](adapty-ads-manager) là nền tảng tối ưu hóa và phân tích của bạn dành cho Apple Ads. Trong hướng dẫn này, bạn sẽ học cách bắt đầu làm việc với Apple 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 Apple Ads Manager với tài khoản Apple Ads để 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 Apple Ads Manager không sử dụng [tích hợp Apple Ads](apple-search-ads) từ **App settings**. Để sử dụng Apple Ads Manager, bạn chỉ cần hoàn thành 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ý hoặc analytics của bạn không được xử 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, thiết lập tối thiểu là cài đặt Adapty 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 của bạn: 1. Bước đầu tiên phụ thuộc vào việc bạn đã triển khai in-app purchase 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 Adapty 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 mua 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. ::: Bây giờ, bạn cần kết nối tài khoản Apple Ads Manager với tài khoản Apple Ads của mình: 1. Trên Adapty dashboard, chuyển sang Apple Ads Manager ở góc trên bên trái. 2. Nhấn **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 Apple Ads Manager: - **Read and Write**: Cấp quyền truy cập cho 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 cho những nhóm đó. 5. Nhấn **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 cho đến khi tất cả 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 Apple Ads Manager" description: "Xem tất cả các chỉ số Apple Ads quan trọng ở một nơi, mỗi chỉ số kèm theo biểu đồ xu hướng." --- Trang **Overview** hiển thị tất cả các chỉ số Apple Ads quan trọng ở một nơi, mỗi chỉ số kèm theo biểu đồ xu hướng. Mặc định, trang này hiển thị dữ liệu cho tất cả các ứng dụng đã kết nối. Để xem một ứng dụng cụ thể, hãy chọn ứng dụng đó từ menu dropdown trong thanh tiêu đề. Để mở trang này, vào **Overview** trong thanh bên trái của Apple Ads Manager. ## 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ố đó. ## Các điều khiển \{#controls\} Sử dụng các điều khiển ở phía trên để đ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 thời gian tùy chỉnh. Tất cả biểu đồ và giá trị tổng hợp sẽ được 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à hình 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ừ. - **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: 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 một nghìn lượt hiển thị quảng cáo. Avg CPM = Spend / (Impressions / 1000) Lưu ý: đối với các chiến dịch Kết quả tìm kiếm App Store với mô hình định giá CPT, đây là CPM hiệu quả. | | Taps | Số lần quảng cáo được người dùng nhấn và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. Avg CPT = Spend / Taps | | TTR | Số lần quảng cáo được nhấn chia cho tổng số lượt hiển thị. TTR = Taps / Impressions * 100% | | Downloads (Total) | Tổng số lượt tải xuống mới và tải lại từ quảng cáo qua tap và view trong khoảng thời gian báo cáo. | | Downloads (View-Through) | Số lượt tải xuống và tải lại từ người dùng đã xem quảng cáo trong cửa sổ 24 giờ nhưng không nhấn vào. | | Downloads (Tap-Through) | Tổng số lượt tải xuống mới và tải lại từ người dùng đã nhấn vào quảng cáo trong cửa sổ 30 ngày. | | Avg CPA (Total) | Chi phí trung bình mỗi lần chuyển đổi (CPA) tổng là tổng chi tiêu chiến dịch chia cho tổng số lượt tải xuống từ lượt 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 mỗi lần chuyển đổi (CPA) tap-through là tổng chi tiêu chiến dịch chia cho số lượt tải xuống tap-through trong khoảng thời gian báo cáo. | | Download Rate (Total) | Tổng số lượt tải xuống từ lượt 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 0% | | Download Rate (Tap-Through) | Tổng số lượt tải xuống từ lần 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 0% | | DPM (Total) | Downloads per Mille (DPM) là số lượt tải xuống trên một nghìn lượt hiển thị. Công thức: (Total Downloads / Impressions) × 1000 nếu Impressions > 0, ngược lại 0 | ## Chuyển đổi \{#conversions\} :::note Revenue, 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 khoảng thời gian 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 0% | | Cost per Conversion | Cost per Conversion là tổng chi tiêu của bạn chia cho số lần chuyển đổi. Công thức: Spend / Conversions nếu Conversions > 0, ngược lại 0 | | Revenue | Revenue là tổng số tiền tạo ra từ các giao dịch mua, gia hạn hoặc các chuyển đổi có doanh thu 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 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, tính theo phần trăm. Công thức: (Revenue / Spend) × 100% nếu Spend > 0, ngược lại 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 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ụ: $60.000 doanh thu / 5.000 người dùng = $12 ARPU. Hữu ích khi so sánh với chi phí mỗi lượt cài đặt (CPI) để đánh giá hiệu quả 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ả phí. Được tính bằng tổng doanh thu chia cho số người dùng trả phí duy nhất. Ví dụ: $60.000 doanh thu / 1.000 người dùng trả phí = $60 ARPPU. Giúp bạn hiểu một khách hàng trả phí tạo ra bao nhiêu doanh thu trung bình. | | 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 tổng doanh thu / số người đăng ký đang hoạt động. Người đăng ký ở đây là những người đã kích hoạt thời gian dùng thử hoặc gói đăng ký. Ví dụ: $60.000 doanh thu / 1.500 người đăng ký = $40 ARPAS. | | Installs | Installs là tổng số người dùng đã cài đặt ứng dụng lần đầu, cũng như các lượt 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ượt tải xuống chưa hoàn chỉnh hoặc bị hủy trước khi hoàn tất sẽ không được tính. | | Installs CR | Installs CR (Tỷ lệ chuyển đổi) là tỷ lệ phần trăm người dùng từ 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 0% | | CPI | CPI (Cost per Install) là chi phí mỗi lượt cài đặt được ghi nhận bởi Adapty. Công thức: Spend / Installs nếu Installs > 0, ngược lại 0 | | Trials | Trials là số lượng gói đăng ký dùng thử mới được khởi động trong khoảng thời gian 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 0% | | Cost per Trial | Cost per Trial là tổng chi tiêu của bạn chia cho số lần bắt đầu dùng thử mới. Công thức: Spend / Trials nếu Trials > 0, ngược lại 0 | | Trials converted | Trials converted là số lượng gói đăng ký dùng thử đã chuyển đổi thành công sang gói đăng ký trả phí trong khoảng thời gian 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ý trả phí. Công thức: (Trials converted / Trials) × 100% nếu Trials > 0, ngược lại 0% | | Cost per Trial converted | Cost per Trial converted là tổng chi tiêu của bạn chia cho số lần dùng thử đã chuyển đổi trong cùng khoảng thời gian. Công thức: Spend / Trials converted nếu Trials converted > 0, ngược lại 0 | | Subscriptions | Subscriptions là tổng số lượng đăng ký mới (không bao gồm dùng thử) trong khoảng thời gian 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 đã bắt đầu gói đăng ký 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 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 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 phải dạng 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 giao dịch mua non-subscription 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 0% | | Cost per Non-subscription | Cost per Non-subscription là tổng chi tiêu của bạn chia cho số lượng giao dịch mua non-subscription. Công thức: Spend / Non-subscriptions started nếu Non-subscriptions started > 0, ngược lại 0 | ## Tải xuống nâng cao \{#advanced-downloads\} | Chỉ số | Mô tả | |--------|--------| | New Downloads (Total) | Tổng số lượt tải xuống mới qua tap và view trong khoảng thời gian báo cáo. | | New Downloads (View-Through) | Lượt tải xuống mới từ người dùng đã xem quảng cáo nhưng không nhấn vào và chưa từng tải ứng dụng trước đây, được tính trong cửa sổ 24 giờ. | | New Downloads (Tap-Through) | Lượt tải xuống mới từ người dùng đã nhấn vào quảng cáo và chưa từng tải ứng dụng trước đây. Được tính trong cửa sổ attribution 30 ngày. | | New Download Share (Tap-Through) | Cho thấy tỷ lệ phần trăm tổng số lượt tải xuống từ lần nhấn là lượt tải xuống mới (từ người dùng đã nhấn vào quảng cáo trong cửa sổ attribution 30 ngày). | | Redownloads (Total) | Tổng số lượt tải lại qua tap và view trong khoảng thời gian báo cáo. | | Redownloads (View-Through) | 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 ứng dụng, xóa đi rồi tải lại trên cùng thiết bị hoặc thiết bị khác sau khi xem quảng cáo. | | Redownloads (Tap-Through) | 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 ứng dụng, xóa đi rồi tải lại trên cùng thiết bị hoặc thiết bị khác sau khi nhấn vào quảng cáo. | | Redownloads Share (Tap-Through) | Cho thấy tỷ lệ phần trăm tổng số lượt tải xuống từ lần nhấn là lượt tải lại. | ## Insights \{#insights\} | Chỉ số | Mô tả | |--------|--------| | Impression Share | Impression Share là tỷ lệ phần trăm lượt hiển thị quảng cáo của bạn 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 bạn tính theo impression share 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à lượng tìm kiếm cao nhất. | --- # File: ads-manager-create-campaign --- --- title: "Quản lý chiến dịch trong Apple Ads Manager" description: "Tạo và chỉnh sửa các chiến dịch Apple Ads trong Apple Ads Manager." --- Apple 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 như theo thời gian thực và có thể tạo, chỉnh sửa các chiến dịch trực tiếp từ Adapty Dashboard một cách tiện lợi hơn nhiều so với giao diện gốc. Nếu bạn tạo chiến dịch trong dashboard Apple Ads gốc, nó sẽ tự động xuất hiện trong Apple Ads Manager trong vòng 24 giờ. Ngoài việc [khám phá 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 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 ## Chiến dịch là gì \{#what-is-campaign\} Một chiến dịch tập trung vào một ứng dụng duy nhất và chạy quảng cáo trong một placement trên App Store. Mỗi chiến dịch bao gồm ngân sách hàng ngày và các [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. Chiến dịch sẽ tiếp tục tiêu tiêu theo 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 độ nơi bạn đặt giá thầu mặc định, đối tượng và từ khóa. Nếu không có nhóm quảng cáo, một chiến dịch sẽ không có mục tiêu hay giá thầu nào và sẽ không phục vụ. 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 Apple Ads mới: 1. Vào **Ads Manager** từ menu sidebar. Trên bất kỳ tab nào, nhấp vào **+** phía trên bảng và chọn **Create campaign**. 2. Chọn ứng dụng bạn muốn khởi chạy chiến dịch. 3. Chọn nhóm chiến dịch cho chiến dịch. 4. Trong phần **Campaign Settings**, cấu hình: - **Campaign name**: Nhãn bạn đặt để nhận diện và tìm kiếm chiến dịch của mình trong dashboard. - **Ad placement**: Bề mặt App Store cụ thể nơi quảng cáo của bạn xuất hiện: - **Search tab**: Quảng bá ứng dụng của bạn ở đầu danh sách ứng dụng được đề xuất khi người dùng truy cập tab Tìm kiếm trong App Store. - **Search results**: Quảng bá ứng dụng của bạn ở đầu kết quả khi người dùng tìm kiếm các ứng dụng liên quan trong App Store. - **Product pages**: Hiển thị quảng cáo của bạn ở đầu danh sách **You might also like** cho những người dùng đã cuộn xuống các trang sản phẩm liên quan. - **Countries or regions**: Các vị trí địa lý nơi chiến dịch của bạn đủ điều kiện phục vụ và chi tiêu ngân sách. - **Ad scheduling**: Ngày và giờ bắt đầu quảng cáo. Tùy chọn, bạn cũng có thể đặt ngày và giờ kết thúc. 5. Trong phần **Bid Strategy**, chọn cách quản lý giá thầu: - **Manage Bids**: Bạn đặt và điều chỉnh giá thầu thủ công ở cấp nhóm quảng cáo và từ khóa. Nhập **Daily budget**. - **Maximize Conversions**: Hệ thống đặt giá thầu tự động của Apple tối đa hóa lượt tải xuống trong phạm vi ngân sách của bạn. Nhập **Daily budget** và **Target CPA** để hướng dẫn thuật toán đến một mục tiêu chi phí-mỗi-lượt-tải-xuống cụ thể. [Tìm hiểu thêm](https://ads.apple.com/app-store/best-practices/maximize-conversions) 6. Nhấp vào **Create**. 7. Tiến hành [tạo một nhóm quảng cáo](ads-manager-create-ad-group) để kích hoạt chiến dịch. ## Chỉnh sửa chiến dịch \{#edit-campaigns\} Để chỉnh sửa bất kỳ chiến dịch đã tạo nào: 1. Mở cài đặt chiến dịch bằng một trong hai cách: - 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. Lưu ý rằng bạn chỉ có thể chỉnh sửa tên, quốc gia và ngân sách hàng ngày. Nếu muốn thay đổi vị trí quảng cáo hoặc lịch trình, hãy tạo một chiến dịch mới. 3. Nhấp vào **Save changes**. :::note Các chỉnh sửa đối với chiến dịch được thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ với Apple Ads Manager, nhưng có thể mất một thời gian để hiển thị trong Apple Ads Manager. ::: ## Xuất 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 tất cả các trang vào một tệp duy nhất. Một cửa sổ 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. 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 hàng chiến dịch theo quốc gia. Bảng được xuất 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ừ Apple Ads Manager: 1. Vào **Ads Manager > Campaigns**. 2. Bật hoặc tắt nút gạt bên cạnh tên chiến dịch trong cột **Status**. --- # File: ads-manager-create-ad-group --- --- title: "Quản lý nhóm quảng cáo trong Apple Ads Manager" description: "Tạo và chỉnh sửa nhóm quảng cáo Apple Ads trong Apple Ads Manager." --- Apple 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 — thuận tiện 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, nó sẽ tự động xuất hiện trong Apple 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 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 ## Nhóm quảng cáo là gì \{#what-is-ad-group\} Nhóm quảng cáo thuộc một [chiến dịch](ads-manager-create-campaign) và là nơi bạn cấu hình tiêu chí nhắm mục tiêu cũng như chiến lược đặt giá thầu cho quảng cáo. Mỗi nhóm quảng cáo 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) quyết định thời điểm và đối tượng xem quảng cáo của bạn. Nhóm quảng cáo giúp bạn tổ chức chiến lược quảng cáo trong một chiến dịch 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 — nếu không có ít nhất một nhóm quảng cáo, chiến dịch sẽ không có nhắm mục tiêu hay đặt giá thầu và sẽ không phân phối được. 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 một 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 nhóm quảng cáo vào. 3. Chọn chiến dịch bạn muốn thêm nhóm quảng cáo vào. 4. Cấu hình cài đặt nhóm quảng cáo: - **Ad group name**: Nhãn bạn đặt để nhận diện và tìm kiếm nhóm quảng cáo trên dashboard. - **Default max CPT bid**: Số tiền tối đa bạn sẵn sàng trả cho mỗi lần nhấp vào quảng cáo. Giá thầu này áp dụng cho tất cả từ khóa trong nhóm quảng cáo, trừ khi bạn thiết lập giá thầu 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ần chuyển đổi qua nhấp (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 giá thầu cho tất cả từ khóa trong nhóm quảng cáo. Mức trầ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 qua nhấp: `CPA Cap × CR (Tap-Through)`. Nếu giá thầu CPT tối đa của từ khóa thấp hơn giá trị này, 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 qua nhấp là 65%, giá thầu tối đa áp dụng cho tất cả từ khóa trong nhóm quảng cáo sẽ là $3.25. Nếu CPT tối đa được đặt ở $4, giá thầu tối đa áp dụng vẫn sẽ là $3.25. - **Search Match**: Bật/tắ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 được 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 nhìn thấy quảng cáo của bạn. - **All eligible users**: Hiển thị quảng cáo cho tất cả người dùng đủ điều kiện trong chiến dịch. - **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 theo độ tuổi cụ thể hoặc tất cả người dùng. - **Ad scheduling** (Tùy chọn): Thiết lập 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 bắt đầu phân phối. - **End date** (Tùy chọn): Thời điểm nhóm quảng cáo ngừng phân phối. 5. Nhấp **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ân phối quảng cáo. Với các loại vị trí khác, bạn đã hoàn tất. ## Chỉnh sửa nhóm quảng cáo \{#edit-ad-groups\} Để chỉnh sửa một nhóm quảng cáo đã tạo: 1. Mở cài đặt chiến dịch bằng một trong hai cách: - Nhấp vào tên chiến dịch trong **Ads Manager > Ad groups**. Sau đó nhấp **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 **Actions > Edit ad group settings**. 2. Điều chỉnh cài đặt nhóm quảng cáo. Bạn có thể chỉnh sửa tất cả các cài đặt, ngoại trừ ứng dụng và chiến dịch. 3. Nhấp **Save changes**. :::note Các chỉnh sửa nhóm quảng cáo được thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ sang Apple Ads Manager, nhưng có thể mất một thời gian để hiển thị trong Apple 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 tất cả trang thành một file duy nhất. Một cửa sổ 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. 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 được chỉ định. - **Group by country**: Phân tách từng hàng nhóm quảng cáo theo quốc gia. Bảng được xuất theo đúng cách 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 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ý keywords trong Apple Ads Manager" description: "Thêm và quản lý keywords, negative keywords và SKAG keywords trong Apple Ads Manager." --- Apple 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 như theo thời gian thực, đồng thời có thể tạo và chỉnh sửa keywords 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 một keyword trong Apple Ads dashboard gốc, nó sẽ tự động xuất hiện trong Apple Ads Manager trong vòng 24 giờ. Ngoài việc [xem analytics toàn diện](adapty-ads-manager-analytics), bạn có thể quản lý tất cả các cài đặt keyword: - Thêm keywords vào ad group - Thêm negative keywords - Thêm keywords dạng SKAG (Single Keyword Ad Group) - Chỉnh sửa keywords trực tiếp trong bảng - Thực hiện hành động hàng loạt trên nhiều keywords - Bật & tắt keywords ## 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 keywords \{#add-keywords\} Để thêm keywords vào một ad group: 1. Vào **Ads Manager** từ menu sidebar. Trên bất kỳ tab nào, nhấp **+** phía trên bảng, rồi chọn **Add keywords** từ dropdown. 2. Trong hộp thoại, chọn các chiến dịch và ad group bạn muốn thêm keywords. Sau khi chọn ad group trong một chiến dịch, bạn có thể chọn chiến dịch khác và thêm nhiều ad group hơn vào danh sách. 3. Nhấp **Select** để tiếp tục. 4. Trong hộp thoại **Add keywords**, nhập keywords vào trường **Keywords list**. Nếu bạn có file chứa keywords phân cách bằng dấu phẩy, bạn có thể dán nội dung để Apple Ads Manager tải lên tất cả keywords cùng lúc. 5. Đối với mỗi keyword trong bảng, cấu hình: - **Match type**: Chọn kiểu khớp **Exact** hoặc **Broad** - **CPT bid**: Đặt mức giá thầu tối đa cost per tap cho keyword này, hoặc để trống để sử dụng mức CPT bid mặc định của ad group 6. Xem lại các keywords và nhấp **Add X keywords** (X là số lượng keywords bạn đang thêm). :::important Sau khi lưu keyword, match type không thể thay đổi. Nếu cần thay đổi match type, hãy xóa keyword đó và thêm lại với match type mong muốn. ::: ## Thêm negative keywords \{#add-negative-keywords\} Để thêm negative keywords: 1. Vào **Ads Manager** từ menu sidebar. Trên bất kỳ tab nào, nhấp **+** phía trên bảng, rồi chọn **Add negative keywords** từ dropdown. 2. Trong hộp thoại **Add negative keywords to**, chọn cấp độ bạn muốn thêm negative keywords: - **Selected campaigns**: Thêm negative keywords ở cấp chiến dịch. - **Selected ad groups**: Thêm negative keywords ở cấp ad group. - **All ad groups in selected campaigns**: Thêm negative keywords ở cấp ad group cho tất cả các ad group trong các chiến dịch đã chọn. :::note Lưu ý: - Negative keywords ở cấp ad group có độ ưu tiên cao hơn negative keywords ở cấp chiến dịch. - Nếu bạn thêm negative keywords cho tất cả ad group trong các chiến dịch đã chọn, bạn sẽ cần thêm thủ công nếu sau này thêm ad group mới vào các chiến dịch đó. ::: 3. Nhập negative keywords vào trường **Keywords list**. Nếu bạn có file chứa keywords phân cách bằng dấu phẩy, bạn có thể dán nội dung để Apple Ads Manager tải lên tất cả keywords cùng lúc. 4. Đối với mỗi keyword trong bảng, chọn **Match type**: - **Exact**: Chỉ loại trừ đúng keyword đó hoặc các biến thể gần giống. - **Broad**: Loại trừ keyword và các cụm từ tìm kiếm liên quan. Hoặc chọn checkbox bên cạnh chúng và thay đổi match type hàng loạt. 5. Xem lại các negative keywords và nhấp **Add X keywords** (X là số lượng keywords bạn đang thêm). :::note Negative keywords 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 khỏi nhiều chiến dịch cùng lúc, tiết kiệm thời gian và đảm bảo tính nhất quán trong chiến lược quảng cáo. ::: ## Thêm keywords dạng SKAG \{#add-keywords-as-skag\} Để thêm keywords dạng SKAG (Single Keyword Ad Group): 1. Vào **Ads Manager** từ menu sidebar. Trên bất kỳ tab nào, nhấp **+** 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 nơi bạn muốn tạo SKAG ad group. Bạn có thể chọn nhiều chiến dịch. 3. Theo mặc định, các ad group mới sẽ được tạo với cài đặt mặc định nhắm mục tiêu 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ác 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ấp **Tag** để tự động thêm keyword, 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ả keywords cùng lúc, hoặc chọn **Set CPT bid and CPA cap for each word manually** để đặt riêng cho từng keyword. 5. Nhập keywords vào trường **Keywords list**. Nếu bạn có file chứa keywords phân cách bằng dấu phẩy, bạn có thể dán nội dung để Apple Ads Manager tải lên tất cả keywords cùng lúc. 6. Đối với mỗi keyword trong bảng, chọn **Match type**: - **Exact**: Chỉ khớp đúng keyword đó hoặc các biến thể gần giống - **Broad**: Khớp keyword và các cụm từ tìm kiếm liên quan Hoặc chọn checkbox bên cạnh chúng và thay đổi match type hàng loạt. 7. Chọn **Check for duplicates in target campaign** để đảm bảo không có keywords trùng lặp trong các chiến dịch mục tiêu. 8. Nhấp **Create** để tạo các SKAG ad group. Mỗi keyword sẽ được đặt trong ad group riêng của nó trong mỗi chiến dịch đã chọn, cho phé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 keywords \{#edit-keywords\} Để chỉnh sửa các keywords hiện có: 1. Vào **Ads Manager > Keywords** hoặc **Ads Manager > Negative keywords** và tìm keyword bạn muốn chỉnh sửa trong bảng, hoặc điều hướng đến trang chiến dịch, rồi đến trang ad group và tìm keyword. 2. Chỉnh sửa giá trị trực tiếp trong bảng: - **CPT bid**: Nhấp vào giá trị giá thầu và nhập mức giá thầu cost per tap tối đa mới - **Status**: Dùng công tắc toggle để tạm dừng hoặc kích hoạt keyword :::note Các chỉnh sửa keywords được thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ với Apple Ads Manager, nhưng có thể mất một lúc để hiển thị trong Apple 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 keywords để tiết kiệm thời gian và quản lý keywords hiệu quả hơn. Để thực hiện hành động hàng loạt: 1. Vào tab **Ads Manager > Keywords** hoặc **Ads Manager > Negative keywords**. 2. Chọn nhiều keywords bằng cách đánh dấu vào ô bên cạnh các keywords bạn muốn quản lý. 3. Nhấp dropdown **Actions** và chọn một trong các tùy chọn sau: - **Add as keywords**: Thêm các keywords đã chọn làm standard keywords - **Add as negative keywords**: Thêm các keywords đã chọn làm negative keywords - **Add as SKAG**: Tạo Single Keyword Ad Group cho các keywords đã chọn - **Activate**: Kích hoạt các keywords đã chọn - **Pause**: Tạm dừng các keywords đã chọn - **Edit CPT bids**: Chỉnh sửa CPT bid cho các keywords đã chọn. Bạn có thể chỉnh sửa theo nhiều cách: - **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 lượng 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 trên để 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) để căn chỉnh giá thầu theo đó. Đặt hệ số nhân. Ví dụ: đặt hệ số nhân là 0,9 khi hiệu suất dưới kỳ vọng, hoặc 1,1 khi hiệu suất vượt trội. - **Set to average CPA**: Sử dụng chỉ số CPA (cost-per-acquisition) để căn chỉnh giá thầu theo đó. Đặt hệ số nhân. :::tip Hành động hàng loạt đặc biệt hữu ích cho: - Chuyển đổi keywords giữa các loại khác nhau (standard, negative, SKAG) - Nhanh chóng thêm keywords với match type khác cho nhiều keywords - Lọc các keywords hoạt động tốt nhất và điều chỉnh giá thầu của chúng - Xác định các keywords hoạt động kém và tạm dừng chúng ::: ## Xuất keywords \{#export-keywords\} Để xuất bảng keywords 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ả keywords trên tất cả các trang vào một file duy nhất. Một hộp thoại 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. Có hai bộ lọc tùy chọn: - **Enabled only**: Chỉ bao gồm các keywords đang hoạt động. - **With spend ≥**: Chỉ bao gồm các keywords có chi tiêu vượt ngưỡng đã chỉ định. - **Group by country**: Phân tách từng hàng keyword theo quốc gia. Bảng được xuất theo cách hiển thị trên dashboard của bạn, với các cột bạn đã chọn hiển thị. ## Xem biểu đồ theo keyword \{#explore-keyword-level-charts\} Bạn có thể mở biểu đồ cho bất kỳ keyword 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 chính xác theo từng ngày cho mỗi keyword riêng lẻ. Để hiển thị biểu đồ, nhấp vào biểu tượng biểu đồ bên cạnh keyword trong bảng. Theo mặc định, biểu đồ sẽ hiển thị chỉ số **Spend** cho keyword đã chọn. Bạn có thể hiển thị nhiều chỉ số cùng lúc để phát hiện mối tương quan và thay đổi theo thời gian. Nhấp **+** để thêm chỉ số mới. Nhấp **Reset** để bắt đầu lại hoặc chỉ cần bỏ chọn checkbox chỉ số để ẩn chúng. ## Lịch sử giá thầu \{#bid-history\} Để xem lịch sử giá thầu của một keyword, nhấp vào nút **Bid History** bên cạnh nó trong bảng. Một panel mở ra với hai tab. - Tab **Metrics** hiển thị biểu đồ với các chỉ số theo thời gian. Bạn có thể thêm và xóa chỉ số theo cách tương tự như trong biểu đồ theo keyword — nhấp **+** để thêm, hoặc bỏ chọn checkbox để ẩn. Một điểm đánh dấu xuất hiện tại mỗi vị trí giá thầu được thay đổi — di chuột qua điểm đánh dấu để xem mức giá thầu chính xác vào ngày đó. Sử dụng tính năng này để tương quan các thay đổi giá thầu với sự biến động hiệu suất: nếu một chỉ số giảm hoặc tăng đột biến sau khi thay đổi, điểm đánh dấu cho biết thời điểm đó xảy ra. - Tab **Bid History** hiển thị bảng tất cả các thay đổi giá thầu theo thứ tự thời gian đảo ngược, với giá trị giá thầu trước và sau, ngày thay đổi và nguyên nhân kích hoạt. --- # File: ads-manager-manage-ads --- --- title: "Quản lý quảng cáo trong Apple Ads Manager" description: "Tạo và chỉnh sửa quảng cáo Apple Ads trong Apple Ads Manager." --- Apple Ads Manager có 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 tiện lợi 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 Apple 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: - **Nhóm quảng cáo**. Bạn có thể [tạo ngay trong dashboard Apple Ads Manager](ads-manager-create-ad-group). - **Trang sản phẩm tùy chỉnh (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ó một quảng cáo đang hoạt động trong nhóm quảng cáo đã 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 sidebar. Ở bất kỳ tab nào, nhấp **+** 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 nhóm quảng cáo cho quảng cáo đó. 4. Nhập tên quảng cáo. 5. Chọn trạng thái quảng cáo. Bỏ chọn hộp kiểm **Status** nếu bạn muốn chạy quảng cáo sau. 6. Nhấp **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 **Create ad**. ## Chỉnh sửa quảng cáo \{#edit-ads\} :::note Sau khi tạo quảng cáo, bạn chỉ có thể chỉnh sửa tên của nó. Bạn không thể thay đổi CPP hoặc chuyển quảng cáo 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 biểu tượng dấu tích bên cạnh. - Chọn hộp kiểm bên cạnh tên quảng cáo và nhấp **Actions > Edit ad**. Thay đổi tên quảng cáo và nhấp **Save changes**. :::note Các chỉnh sửa quảng cáo thực hiện trực tiếp trong Apple Ads sẽ tự động đồng bộ sang Apple Ads Manager, nhưng có thể mất một chút thời gian để hiển thị trong Apple Ads Manager. ::: ## Xuất danh sách 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 tất cả quảng cáo trên 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 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 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 được xuất theo đúng cách hiển thị trên dashboard của bạn, với các cột bạn đã chọn hiển thị. ## Chạy & tạm dừng quảng cáo \{#launch--pause-ads\} Để chạy hoặc tạm dừng bất kỳ quảng cáo nào từ Apple Ads Manager, sử dụng một trong các tùy chọn sau: - Chọn hoặc bỏ chọn hộp kiểm trong cột **Status** tại **Ads Manager > Ads**. - Chọn hộp kiểm bên cạnh tên quảng cáo và nhấp **Actions > Edit ad**. Thay đổi tên quảng cáo và nhấp **Save changes**. --- # File: ads-manager-create-segments --- --- title: "Tạo phân khúc dựa trên attribution Apple Ads trong Apple Ads Manager" description: "Tạo phân khúc từ các chiến dịch, nhóm quảng cáo và từ khóa chỉ với vài cú nhấp trong Apple Ads Manager." --- Bạn có thể tạo [phân khúc](segments) người dùng trực tiếp từ [Apple 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 và chuyển chúng thành phân khúc chỉ với vài cú nhấp. Cách này giúp bạn dễ dàng cá nhân hóa paywall và ưu đãi dựa trên nguồn thu hút người dùng, mà không cần tự thiết lập điều kiện phân khúc thủ công. 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 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 mức giá đặc biệt cho người dùng từ các chiến dịch Apple Ads được chọn, trong khi giữ ưu đãi tiêu chuẩn cho người dùng khác. - **Nhất quán từ quảng cáo đến paywall**. Chuyển hướng người dùng 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 theo giá đầy đủ ưu tiên sản phẩm cao cấp cho người dùng từ các chiến dịch liên tục 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ừ Apple 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 thực thể bạn muốn sử dụng. Lưu ý rằng nếu bạn chọn nhiều thực thể, chúng sẽ được dùng để tạo một phân khúc cho tất cả chứ không phải mỗi thực thể một phân khúc riêng. 2. Nhấp vào **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**: Ứng dụng 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ừ chiến dịch hoặc nhóm quảng cáo, bạn có thể điều chỉnh các chiến dịch hoặc nhóm quảng cáo đã chọn ở bước này. - **Segment name** - **Segment description** 4. Nhấp **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 sẽ 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 Apple Ads Manager" description: "Tự động quản lý vòng đời từ khóa — điều chỉnh giá thầu, kích hoạ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 xử lý 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 dùng thử, gói đăng ký và doanh thu. Bạn định nghĩa đ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 sẽ được thực hiện khi điều kiện đó được đáp ứng. Quy tắc chạy theo lịch bạn thiết lập và tự động phản hồ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 dùng ngay cho các kịch bản tối ưu hóa phổ biến. Một số mẫu thông dụng bao gồm: - **Cut waste on non-converting keywords**: Giảm giá thầu khi Spend > X và Installs hoặc Trials = 0. - **Scale winning keywords**: Tăng giá thầu khi ROAS > mục tiêu hoặc CPA < mục tiêu. Để tạo quy tắc từ mẫu: 1. Ở thanh sidebar bên trái, vào **Automations** và nhấp **Templates**. 2. Chọn mẫu và nhấp **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 tên theo tên mẫu và ngày hiện tại (ví dụ: "Scale Winning Keywords - [2025-11-12]"). - **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à 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 cài sẵn nếu cần. - **Schedule**: Đặt tần suất chạy quy tắc. 4. Nhấp **Save** để kích hoạt quy tắc. ### Tạo thủ công \{#manually\} Để tạo quy tắc từ khóa tùy chỉnh từ đầu: 1. Ở thanh sidebar bên trái, vào **Automations**, nhấp **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 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 **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ệ tài khoản của bạn. Dữ liệu được cập nhật gần thời gian thực, nên 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 sẽ xảy ra khi điều kiện được đáp ứng: **Change bid** - **Action type**: Chọn **Increase by**, **Decrease by** hoặc **Set to**. - **Value type**: Chuyển đổi giữa **$** (giá trị tuyệt đối) và **%** (tương đối so với giá thầu hiện tại tại thời điểm quy tắc chạy). - **Upper bid limit** (tùy chọn): Mức giá thầu tối đa để tránh đặt giá quá cao nếu quy tắc kích hoạt nhiều lần do tín hiệu mạnh. **Enable keyword** - Không cần cấu hình thêm. Quy tắc sẽ kích hoạt lại các từ khóa đang tạm dừng đáp ứng điều kiện. **Pause keyword** - Không cần cấu hình thêm. Quy tắc sẽ tạm dừng các từ khóa đang hoạt động đáp ứng điều kiện. **Add as keyword to…** - **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 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. **Add as negative keyword to…** - **Scope**: Chọn nhóm quảng cáo hoặc chiến dịch nơi từ khóa phủ định sẽ đượ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.v. - Chọn thời gian chạy (tất cả thời gian theo UTC). Quy tắc chạy vào thời gian đã lên lịch theo UTC. Quá trình thực thi thường hoàn thành 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ấp **Save** để tạo quy tắc. ## Các thực hành tốt nhất \{#best-practices\} - **Bắt đầu với phạm vi hẹp**: Áp dụng 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 trước khi mở rộng. - **Dùng cửa sổ nhìn lại ngắn cho 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à 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 giá thầu cho quy tắc Change bid**: Giới hạn giá thầu trên giúp ngăn giá thầu tăng không kiểm soát khi tín hiệu mạnh kích hoạt quy tắc nhiều lần. - **Dùng Enable keyword kết hợp với dữ liệu cohort**: Từ khóa bị tạm dừng sớm do 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. Đặt điều kiện dựa trên ROAS cohort và tự động kích hoạt lại khi đạt mục tiêu. - **Dùng Add as keyword to… cho quy trình từ thử nghiệm sang mở rộng**: Khi một từ khóa 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 mở rộng. - **Dùng Add as negative keyword to… để giữ chiến dịch Discovery sạch**: Khi một từ khóa được xác nhận là từ khóa đối sánh chính xác, hãy phủ định nó trong các chiến dịch Discovery hoặc Search Match để tránh cạnh tranh cho cùng một truy vấn. - **Chờ trước khi quy tắc giá thầu xử lý từ khóa mới được thăng cấp**: Nếu bạn dùng [tự động hóa cụm từ tìm kiếm](ads-manager-automations-search-terms) để chuyển các cụm từ vào chiến dịch từ khóa, hãy cho những từ khóa đó một hoặc hai ngày để tích lũy dữ liệu trước. --- # File: ads-manager-automations-search-terms --- --- title: "Automation từ khóa tìm kiếm trong Apple Ads Manager" description: "Tự động đưa các từ khóa tìm kiếm hiệu quả vào danh sách keyword và loại trừ chúng tại nguồn để mở rộng traffic khám phá mà không cần thao tác thủ công" --- Các chiến dịch Discovery và Search Match tạo ra dữ liệu từ khóa tìm kiếm. Để chuyển dữ liệu đó thành danh sách keyword có cấu trúc, bạn phải tải báo cáo, lọc các từ khóa rồi thêm thủ công vào từng ad group. Automation từ khóa tìm kiếm thực hiện điều này một cách tự động: khi một từ khóa đáp ứng điều kiện của bạn, quy tắc sẽ xử lý nó theo hành động bạn cấu hình. Có hai loại hành động cho quy tắc từ khóa tìm kiếm: - **Add as keyword**: Đưa từ khóa vào một ad group đích dưới dạng exact-match keyword, và tùy chọn loại trừ nó tại chiến dịch nguồn để tránh chi tiêu trùng lặp. - **Add as negative keyword**: Loại trừ trực tiếp từ khóa đó mà không đưa vào keyword list. Dùng cách này để lọc các từ khóa 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**: để chiến dịch Discovery hoặc Search Match thu thập các truy vấn thực của người dùng, sau đó dùng quy tắc để phát hiện các từ khóa vượt ngưỡng hiệu suất và đưa chúng vào chiến dịch Probing dưới dạng exact-match keyword — đồng thời loại trừ chúng tại nguồn. Chiến dịch Probing là một chiến dịch Apple Search Ads chuyên để kiểm thử các keyword được đề xuất với mức bid có 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ụ: nhiều impression nhưng không có tap nào), hãy tự động loại trừ nó để tránh 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ừ template \{#from-a-template\} Để tạo quy tắc từ template: 1. Ở thanh điều hướng bên trái, vào **Automations** và nhấp **Templates**. 2. Chọn một template và nhấp **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. - **Apply to**: Chọn nhóm chiến dịch, ứng dụng, chiến dịch hoặc ad group mà quy tắc cần tìm kiếm từ khóa. - **Conditions**: Chỉnh sửa các điều kiện đã cấu hình sẵn nếu cần. - **Actions**: Điều chỉnh ad group đích, mức CPT bid và phạm vi negative keyword nếu cần. - **Schedule**: Đặt tần suất chạy quy tắc. 4. Nhấp **Save** để kích hoạt quy tắc. ### Thủ công \{#manually\} Để tạo quy tắc automation từ khóa tìm kiếm tùy chỉnh từ đầu: 1. Ở thanh điều hướng bên trái, vào **Automations**, nhấp **Create rule** và chọn **Search terms** làm loại quy tắc. 2. Nhập **Rule name** mô tả rõ mục đích của quy tắc. 3. Trong phần **Apply to**, chọn nhóm chiến dịch, ứng dụng, chiến dịch hoặc ad group mà quy tắc cần tìm kiếm từ khóa. 4. Nhấp **Add condition** và chọn một [chỉ số](adapty-ads-manager-metrics) từ danh sách. Các chỉ số được tính theo khoảng thời gian đã chọn bằng đơn vị tiền tệ 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 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, 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 từ khóa tìm kiếm đáp ứng điều kiện: **Add as keyword** Đưa các từ khóa tìm kiếm phù hợp vào ad group đích dưới dạng exact-match keyword. - **Target ad groups**: Chọn các ad group nhận keyword được đề xuất. Để xây dựng pipeline khám phá, hãy chọn ad group trong chiến dịch Probing hoặc chiến dịch có cấu trúc khác. - **CPT bid**: Đặt mức cost-per-tap bid ban đầu cho từng keyword được đề xuất. Các tùy chọn: mức bid mặc định của ad group, CPT hiện tại của từ khóa tìm kiếm, hoặc một giá trị cụ thể. - **Skip if keyword already exists**: Khi bật, bỏ qua các từ khóa đã có trong ad group đích. - **Add as negative**: Thêm các từ khóa đó làm negative keyword để tránh trả tiền hai lần cho cùng một lượt traffic. - **Scope**: Chọn các ad group nơi negative keyword được thêm vào. :::tip Bật **Add as negative** ngay trong cùng quy tắc — đề xuất từ khóa vào chiến dịch có cấu trúc và loại trừ nó tại nguồn chỉ trong một bước. Điều này giữ cho các chiến dịch Discovery của bạn sạch sẽ và tự động xây dựng phễu keyword. ::: **Add as negative keyword** Loại trừ từ khóa tìm kiếm phù hợp mà không đề xuất nó. - **Scope**: Chọn các ad group hoặc chiến dịch nơi negative keyword đượ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 từ khóa tìm kiếm không liên quan hoặc kém chất lượng khỏi các chiến dịch Discovery và Max Conversion. Ví dụ: nếu một từ khóa có hơn 50 impression và 0 tap, 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 theo UTC). Quy tắc chạy theo thời gian đã lên lịch theo UTC. Quá trình thực thi thường hoàn thành 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ấ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 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, ad group đích, kết quả hành động và kết quả loại trừ. Nếu không có từ khóa nào hiển thị, điều kiện chưa được đáp ứng — hãy xem lại ngưỡng hoặc mở rộng khoảng thời gian nhìn lại. ## Các thực hành tốt nhất \{#best-practices\} - **Dùng chiến dịch Discovery hoặc Search Match làm nguồn**: Các loại chiến dịch này thu thập truy vấn thực của người dùng, cung cấp cho quy tắc một lượng lớn từ khóa 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 về 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 từ khóa có thể vượt qua ngưỡng với ít lần chuyển đổi hơn. Với ứng dụng lưu lượng cao, hãy rút ngắn khoảng thời gian và nâng ngưỡng. - **Luôn loại trừ tại nguồn khi đề xuất**: Nếu bạn thêm một từ khóa vào chiến dịch Probing nhưng không loại trừ trong Discovery, cả hai chiến dịch sẽ cạnh tranh cho cùng một truy vấn. Hãy bật **Add as negative** ngay trong cùng quy tắc. - **Chọn ad group đích có chủ đích**: Chuyển các từ khóa được đề xuất vào một ad group Probing cụ thể thay vì một chiến dịch rộng. Điều này giữ cho cấu trúc keyword của bạn rõ ràng và giúp phân tích hiệu suất dễ dàng hơn. - **Xem lại logs sau mỗi lần chạy**: Kiểm tra tab Logs để xác nhận từ khóa nào đã được đề xuất và đến đâu. Ban đầu, hãy chạy thủ công quy tắc 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 logs. - **Để keyword được đề xuất có thời gian trước khi quy tắc keyword xử lý chúng**: Nếu bạn dùng quy tắc keyword trong cùng chiến dịch, hãy loại trừ các keyword mới được đề xuất hoặc chờ một đến hai ngày trước khi các quy tắc chạy trên chúng. Một quy tắc keyword có thể kích hoạt trên một từ khóa mới chưa có lịch sử hiệu suất và cắt giảm bid của nó trước khi nó chuyển đổi. - **Dùng Add as negative keyword cho các từ khóa nhiều impression nhưng không có tap**: Các chiến dịch Discovery và Max Conversion thường hiển thị các từ khóa tìm kiếm không liên quan. Quy tắc với "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-market-intelligence --- --- title: "Market Intelligence trong Apple 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 các từ khóa mà đối thủ đang đặt giá thầu trong 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. Sử dụng tính năng này để: - **Bỏ qua Discovery campaign**: Xem ngay những từ khóa đối thủ đang đặt giá thầu thay vì tốn ngân sách để mò mẫm. Bắt đầu ngay từ ngày đầu với danh sách từ khóa đã được kiểm chứng. - **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ấp 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 trên tên ứng dụng của bạn ở 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**: Kiểm tra các từ khóa đối thủ đang chạy ở một quốc gia trước khi bạn chi bất kỳ đồng nào ở đó. - **Phát hiện đối thủ bạn chưa biế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 \{#2-select-competitors\} Thêm các đối thủ 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 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 kiếm theo từ khóa hoặc tên ứng dụng**: Nhập một 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, sau đó chọn ứng dụng từ kết quả. Lặp lại 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 khác nhau. - **Danh sách đã lưu**: Nhấp **Create list** để lưu tối đa 20 đối thủ để tái sử dụng trong các phân tích sau. Bạn cũng có thể tải danh sách đã tạo trước đó. Khi đã chọn xong đối thủ, nhấp **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ổng quan của phân tích: - **Stats bar**: Tổng số đối thủ đã phân tích, các quốc gia có hoạt động Apple Ads, số từ khóa duy nhất tìm thấy trên tất cả thị trường, và từ khóa được cạnh tranh nhiều nhất. - **Keywords found by country**: Biểu đồ cột hiển thị số lượng từ khóa theo quốc gia trên 25 thị trường hàng đầu. - **Top 10 competitors by keyword coverage**: Bảng xếp hạng với tổng số từ khóa của mỗi đối thủ, Share of Voice trung bình (Avg SOV), và các quốc gia họ đang hoạt động. #### Most Contested \{#most-contested\} Hiển thị các từ khóa có nhiều đối thủ đang cùng hoạt động 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 nhất. Sử dụng ô tìm kiếm để lọc danh sách theo từ khóa. #### By App \{#by-app\} Hiển thị dữ liệu từ khóa cho từng đối thủ riêng lẻ. Dùng tab này để đi sâu vào chiến lược từ khóa của một ứng dụng cụ thể theo từng quốc gia. 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. #### By Country \{#by-country\} Hiển thị dữ liệu từ khóa được nhóm theo thị trường. Dùng tab này khi bạn muốn tập trung vào bối cảnh cạnh tranh của một quốc gia cụ thể trước khi thâm nhập hoặc mở rộng. 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 \{#4-add-keywords-to-campaigns\} Sau khi xác định được các từ khóa đáng thử nghiệm, hãy thêm chúng vào chiến dịch mà không cần rời khỏi công cụ: 1. Trong bảng từ khóa, chọn các ô checkbox bên cạnh từ khóa bạn muốn sử dụng. Để chọn tất cả từ khóa đang hiển thị, dùng checkbox ở tiêu đề bảng. 2. Nhấp **Add to campaign**. 3. Chọn thêm chúng dưới 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 và CPT bid, rồi xác nhận. ## Những gì cần chú ý \{#what-to-look-for\} Đây là những dấu hiệu đáng chú ý trong kết quả: - **Từ khóa đuôi dài với Share of Voice thấp**: Các từ khóa mà đối thủ có tỷ lệ hiển thị thấp thì ít bị cạnh tranh 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 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 các từ khóa đối thủ đang chạy mà bạn chưa thử. Đây là những từ khóa đã được chứng minh mang lại lưu lượng Apple Ads trong danh mục của bạn. - **Mức độ 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 trên thương hiệu của bạn. Hãy thêm các từ khóa đó vào chiến dịch của bạn với 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 các quốc gia mà đối thủ đang hoạt động. Các thị trường có ít hoặc không có hoạt động cạnh tranh sẽ dễ thâm nhập hơn và cần ít ngân sách hơn để tạo được sức kéo. --- # File: ads-manager-cpp-ab-tests --- --- title: "CPP A/B Tests trong Apple 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." --- CPP A/B Tests cho phép bạn so sánh các trang sản phẩm tùy chỉnh (CPPs) với nhau trong Apple Ads. Bạn chọn từ 2 đến 4 trang sản phẩm, và [Apple Ads Manager](adapty-ads-manager) sẽ luân chuyển lưu lượng truy cập giữa chúng, thu thập dữ liệu hiệu suất, và cho bạn biết trang nào chuyển đổi tốt nhất. Bạn có thể đưa **trang sản phẩm mặc đị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 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. Testing Method \{#3-testing-method\} Cấu hình cách chạy test. Apple Ads Manager tự động tính toán **Calculated Test Duration**, **Start Time**, và **End Time** — các giá trị này cập nhật mỗi khi bạn thay đổi một trong ba cài đặt bên dưới. #### 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 tần suất quá cao so với mức lưu lượng truy cập, hệ thống sẽ tự động hạ xuống. | Khoảng thời gian | Mức lưu lượng truy cập điển hình | Thời gian slot | Độ dài test điển hình | |------------------|---------------------------------------------|----------------|-----------------------| | **Hourly** | Cao (hơn 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. #### Desired Precision \{#desired-precision\} Mức chênh lệch nhỏ nhất trong tỷ lệ chuyển đổi mà 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%**. Test với độ chính xác 1% phát hiện được sự khác biệt nhỏ nhưng cần nhiều dữ liệu hơn và chạy lâu hơn. Test với độ chính xác 5% kết thúc nhanh hơn nhưng chỉ phát hiện được sự khác biệt lớn hơn. | Độ chính xác | Khi nào nên dùng | |--------------|---------------------------------------------------------------------------------------| | 1–2% | Bạn dự đoán sự khác biệt nhỏ giữa các CPP và có nhóm quảng cáo lưu lượng cao. | | 3–4% | Mặc định cân bằng cho hầu hết các test. | | 5% | Bạn dự đoán có người thắng rõ ràng và muốn có kết quả nhanh. | #### Confidence Level \{#confidence-level\} Mức độ chắc chắn bạn muốn có rằng kết quả là thật chứ không phải ngẫu nhiên. Các tùy chọn: **80%**, **85%**, **90%**, **95%**, **99%**. Mặc định: **90%**. Mức độ tin cậy cao hơn 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% | Thận trọng nhất. Cần nhiều dữ liệu nhất và test lâu 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** trong thanh 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ũ bạn không cần xem trong 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ấn **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 test: | Cột | Mô tả | |--------------------------|-----------------------------------------------------------------------------------------------------| | **Variant Name** | CPP đang được kiểm tra. Variant 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: số lượt nhấn chia cho số lượt 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ượt nhấn. | | **Avg CPA (Tap-Through)**| Chi phí trung bình mỗi lượt mua hàng dựa trên lượt tải xuống qua tap-through. | | **Spend** | Tổng chi phí quy cho biến thể. | | **Revenue** | Tổng doanh thu quy cho biến thể. | | **ROAS** | Lợi nhuận trên chi phí quảng cáo: doanh thu chia cho chi phí. | Cho đến khi mỗi biến thể có lượt hiển thị tương đương nhau, Apple Ads Manager sẽ không đánh dấu người thắng. Trong khi dữ liệu vẫn đang 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 sâu 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 chia 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**: Người dùng trả phí, Paid CR, Cost per Paid, ARPPU, Revenue, và ROAS theo từng biến thể. Cột **Winner** hiển thị biến thể nào dẫn đầu trên mỗi chỉ số. Một biến thể chỉ được đánh dấu là người thắng tổng thể khi nó 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ố, xem [Chỉ số trong Apple 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 test \{#test-statuses\} Mỗi test đi qua một tập hợp trạng thái cố định: | Trạng thái | Ý nghĩa | |---------------|----------------------------------------------------------------------------------------------------| | **Draft** | Test đã được tạo nhưng chưa bắt đầu. Bạn vẫn có thể chỉnh sửa. | | **Starting** | Đang cài đặt — hệ thống đang sao chép nhóm quảng cáo và tạo quảng cáo. | | **Running** | Test đang chạy. Các biến thể được luân chuyển và chỉ số được thu thập. | | **Completed** | Thời gian đã lên lịch đã hết hoặc đạt độ tin cậy yêu cầu. Nhóm quảng cáo gốc được khôi phục. | | **Stopped** | Bạn đã dừng test thủ công. Nhóm quảng cáo gốc được khôi phục. | | **Failed** | Cài đặt 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 test thất bại. | ## Cách hoạt động \{#how-it-works\} Apple Ads Manager sử dụng phương pháp **Ad Group Switch**: 1. Khi test bắt đầu, 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ố chúng có thể là trang mặc định của bạn). 2. Chỉ một bản sao chạy tại một thời điểm. Hệ thống luân chuyển bản sao nào đang hoạt động theo lịch cố định (theo giờ, hàng ngày, hoặc hàng tuần). 3. Nhóm quảng cáo gốc bị tạm dừng trong khi test chạy. Nó được khôi phục về trạng thái trước đó khi test kết thúc. 4. Apple Ads Manager thu thập lượt hiển thị, lượt nhấn, và lượt tải xuống theo từng biến thể, và theo dõi mức độ gần đạt mẫu có ý nghĩa thống kê của mỗi biến thể. 5. Test tự động kết thúc khi mỗi biến thể có đủ dữ liệu, hoặc khi thời gian đã lên lịch được đáp ứng. ## Những điều cần biết khi test đang chạy \{#what-to-expect-while-a-test-runs\} Một số điều đáng lưu ý về cách test đang chạy hoạt động trên dashboard: - **Các biến thể không chuyển đổi theo đồng hồ cố định**: Khoảng thời gian chuyển đổi là cơ sở, nhưng Apple Ads Manager điều chỉnh thời gian để mỗi biến thể thu thập được lượt hiển thị công bằng. Một biến thể có thể hoạt động lâu hơn một slot nếu nó đang thiếu lượt hiển thị. - **End Time có thể lùi lại**: Nếu một biến thể thiếu dữ liệu khi đến gần thời gian kết thúc đã lên lịch, test sẽ được tự động gia hạn để tiếp tục thu thập lượt nhấn. End Time mới sẽ xuất hiện 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 đã sao chép 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ó sẵn trong tab **Completed**. --- # File: ads-manager-settings --- --- title: "Cài đặt trong Apple Ads Manager" description: "Cấu hình cài đặt trong Apple Ads Manager." --- Điều hướng đến **Settings** ở góc dưới bên trái của dashboard Apple Ads Manager để cấu hình cài đặt tài khoản. ## Nhóm chiến dịch \{#campaign-groups\} Trên 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 Apple 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 phân tích của chúng sẽ được tổng hợp trong một dashboard Apple Ads Manager duy nhất. Để thêm tài khoản Apple Ads mới, nhấn **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\} Trên tab **User settings**, bạn có thể bật tùy chọn **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 đi, 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 việc khởi 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ể muốn truy cập các mục đã tạm dừng bất cứ lúc nào. --- # File: adapty-user-acquisition --- --- title: "User Acquisition (Adapty UA)" description: "Loại bỏ sự cần thiết của 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 User Acquisition là giải pháp attribution giúp 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 tổng 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ập hợp toàn bộ dữ liệu acquisition của bạn vào một nơi. - Tính toán ROAS (lợi tức trên chi tiêu quảng cáo) trên tất cả các kênh của bạn - Xem toàn bộ kinh tế ứng dụng của bạn tại 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 User Acquisition có thể hữu ích với bạn? [Đặt lịch tư vấn](https://calendly.com/tnurutdinov-adapty/30min) với chúng tôi. ::: ## Tại sao nên dùng Adapty UA? \{#why-adapty-ua\} Đo lường hiệu suất user acquisition là một thách thức. 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 các giải pháp tùy chỉnh tốn rất nhiều thời gian. Adapty UA cung cấp attribution tích hợp sẵn và analytics thống nhất trong một dashboard marketing duy nhất. Toàn bộ chỉ số acquisition của bạn — từ chi tiêu quảng cáo đến lượt cài đặt đến doanh thu gói đăng ký — đượ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 trên nhiều bảng tính hay chuyển qua lại giữa nhiều công cụ. 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 User Acquisition gán lượt cài đặt ứng dụng và doanh thu gói đăng ký cho 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, tracking link và ứng dụng của bạn. Ở cấp độ 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 tiêu quảng cáo - Tracking link được tạo trong Adapty UA mang thông tin ngữ cảnh chiến dịch từ web đến lượt cài đặt ứng dụng - SDK Adapty 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 tracking link được tạo trong Adapty UA và thêm vào chiến dịch quảng cáo.** Link chứa các tham số chiến dịch như nền tảng, chiến dịch, ad set và creative. 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 tracking link 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.** Ở lần khởi chạy đầu tiên, SDK Adapty gửi một sự kiện cài đặt. Adapty trích xuất các tham số chiến dịch liên quan đến lượt 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ừ tracking link, Adapty liên kết lượt cài đặt với chiến dịch đã tạo ra nó. 5. **Chi tiêu quảng cáo và doanh thu được kết nối.** Adapty lấy dữ liệu chi tiêu quảng cáo từ các nền tảng quảng cáo được hỗ trợ (hiện tại — 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 gán 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 analytics 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 phải đối chiếu thủ công dữ liệu từ các nguồn khác nhau. :::tip Tracking link cũng có thể bao gồm các 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 hồ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 User Acquisition" description: "Kết nối với Adapty User Acquisition để kết hợp chi phí quảng cáo và doanh thu gói đăng ký, xem toàn bộ hoạt động kinh tế của ứng dụng ở một nơi." --- Adapty User Acquisition 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ề hoạt động kinh tế của ứng dụng ở một nơi. Để xem dữ liệu doanh thu trong Adapty User Acquisition, trước tiên bạn cần bật tích hợp trong 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 User Acquisition có sẵn 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 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. - Nếu bạn **đã triển khai in-app purchase mà không dùng Adapty** và không có kế hoạch migration sang Adapty, hãy [cài đặt Adapty SDK 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: Cấu hình này cho phép attribution web-to-app: - Khi người dùng cài đặt ứng dụng của bạn, Adapty SDK lấy thông tin cài đặt từ các tham số liên kết, để Adapty UA có thể nhận thông tin chiến dịch. - Adapty SDK nắm được tất cả các sự kiện liên quan đến doanh thu trong ứng dụng và có thể attribution chúng vào các chiến dịch web. ## Bước 1. Bật tích hợp User Acquisition \{#step-1-enable-the-user-acquisition-integration\} Để bắt đầu gửi sự kiện doanh thu đến Adapty UA: 1. Vào **[Integrations > Adapty](https://app.adapty.io/integrations/user-acquisition)** trong Adapty Dashboard. 2. Bật toggle. Sau khi các sự kiện bắt đầu kích hoạt, bạn sẽ thấy các thông tin sau cho mỗi sự kiện: - Tên sự kiện - Trạng thái - Môi trường - Ngày giờ <img src="/assets/shared/img/toggle-ua.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 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 để kết nối lượt cài đặt ứng dụng với dữ liệu chiến dịch. Bạn phải sử dụng tracking link làm URL đích trong mỗi chiến dịch quảng cáo mà bạn muốn đo lường trong Adapty UA. Nếu bạn chạy quảng cáo trên nhiều nền tảng, hãy thiết lập tracking link cho từng nền tảng riêng biệt. Adapty hoạt động với các nền tảng quảng cáo theo hai cách: - **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. Tracking link được tạo tự động và các tham số chiến dịch được điền động dựa trên nơi link được sử dụng. Bạn có thể dùng cùng một link cho các chiến dịch, nhóm quảng cáo hoặc creative khác nhau, và Adapty sẽ tự động nhận đúng dữ liệu chiến dịch và chi phí quảng cáo. - **Chỉ dùng tracking link (tất cả các nền tảng quảng cáo khác).** Adapty không kết nối 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 xác định 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 UA 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 xét 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 phần **iOS** và/hoặc **Android** và dán URL ứng dụng 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 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. Giờ đây, khi bạn chạy quảng cáo trên Meta Ads, dữ liệu của nó sẽ có sẵn để phân tích trong Adapty UA dashboard. </TabItem> <TabItem value="tiktok" label="TikTok for Business"> Để tạo tracking link cho TikTok for Business: 1. Vào **[Integrations > TikTok Ads](https://app.adapty.io/ua/integrations/tiktok/accounts)** trong Adapty UA Dashboard và nhấp **Continue with TikTok**. 2. Đăng nhập bằng tài khoản TikTok của bạn và nhấp **Continue**. 3. Xem xét 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 phần **iOS** và/hoặc **Android** và dán URL ứng dụng 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 kết nối lượt cài đặt và giao dịch mua 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. Giờ đây, khi bạn chạy quảng cáo trên TikTok for Business, dữ liệu của nó sẽ có sẵn để phân tích trong Adapty UA dashboard. </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 UA dashboard, vào **Tracking links** từ menu sidebar. Tại đó, nhấp **Create link**. 2. Chọn ứng dụng của bạn từ danh sách và nhấp **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. 4. Theo mặc định, bạn đang tạo One Link. Nó 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 theo dõi 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 link cửa hàng theo từng nền tảng thủ công. 5. Nhấp **Create**. 6. Mở trang tracking link của bạn và sao chép **Click link** từ một trong các phần: - **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 từng nền tảng nếu bạn muốn có link riêng cho mỗi cửa hàng. 7. Vào nền tảng quảng cáo của bạn và dán link vào quảng cáo làm URL đích quảng cáo. </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 User Acquisition analytics và các hướng dẫn thực tế để chạy chiến dịch trên các nền tảng quảng cáo lớn: - [**Analytics trong Adapty UA**](ua-analytics): Xem cách sử dụng analytics dashboard hiệu quả. - [**Chỉ số trong Adapty UA**](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 xét các nền tảng quảng cáo và tích hợp được Adapty UA 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í từ mỗi lần người dùng nhấn vào quảng cáo của bạn. | Không | | **Impressions** | Số lần quảng cáo của bạn được hiển thị trong khoảng thời gian đã chọn. | Không | | **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. | Không | | **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` | Không | | **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` | Không | | **CPM** | **CPM (Cost per Mille)** là số tiền bạn trả cho mỗi nghìn lượt hiển thị quảng cáo. <br/>**Công thức**: `Spend / (Impressions / 1000)` | Không | | **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%` | Không | | **IPM** | **IPM (Installs per Mille)** là số lượt cài đặt trên mỗi nghìn lượt hiển thị quảng cáo. <br/>**Công thức**: `(Installs / Impressions) × 1000` | Không | | **CTR** | **CTR (Click-Through Rate)** là tỷ lệ phần trăm số lần hiển thị dẫn đến nhấp chuột. <br/>**Công thức**: `(Clicks / Impressions) × 100%` | Không | | **Inline link clicks** | Số lần người dùng nhấp vào 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. | Không | | **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` | Không | | **Inline link click CTR** | Tỷ lệ phần trăm số lần 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%` | Không | | **Installs** | Tổng số người dùng đã cài đặt ứng dụng của bạn (kể cả cài đặt lại) trong kỳ báo cáo. | Không | | **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 hàng) trong khung thời gian đã chọn. | Có | | **ROAS** | **ROAS (Return on Ad Spend)** là doanh thu từ quảng cáo chia cho chi phí quảng cáo, được biểu thị theo phần trăm. <br/> **Công thức**: `(Revenue / Spend) × 100% nếu Spend > 0, ngược lại 0%` | Có | | **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` | Có | | **LTV** | **LTV (Lifetime Value)** là doanh thu trung bình được quy cho một người dùng trong suốt vòng đời của họ. <br/>**Công thức**: `Revenue / Installs` | Không | | **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` | Không | | **Cost per subscription** | Số tiền trung bình bạn trả cho mỗi sản phẩm gói đăng ký được mua. <br/>**Công thức**: `Spend / Count subscription started` | Không | | **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 | Có | | **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 | Có | | **Count billing issue detected** | Số vấn đề thanh toán được phát hiện trong kỳ báo cáo. | Có | | **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. | Có | | **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 | Có | | **Subscription events rate** | Các chỉ số hiển thị tỷ lệ sự kiện liên quan đến gói đăng ký so với 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 | Có | | **Trial events rate** | Các chỉ số hiển thị tỷ lệ sự kiện liên quan đến dùng thử so với 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 | Có | | **Rate billing issue detected** | Tỷ lệ sự cố thanh toán so với lượt cài đặt ứng dụng trong kỳ báo cáo. | Có | | **Rate entered grace period** | Tỷ lệ gói đăng ký bước vào thời gian ân hạn so với lượt cài đặt ứng dụng trong kỳ báo cáo. | Có | | **Non-subscription events rate** | Các chỉ số hiển thị tỷ lệ sự kiện không liên quan đến gói đăng ký so với 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 | Có | ## Chỉ số dự đoán \{#predicted-metrics\} Chỉ số dự đoán chiếu hiệu suất tương lai của một cohort dựa trên dữ liệu lịch sử của ứng dụng. Chúng có sẵn ở nhiều kỳ cohort, bao gồm D30, D60, D90, D180 và D360, cùng với các kỳ tùy chỉnh theo ngày mà bạn có thể thêm vào. Để biết cách tính các giá trị này, xem [Chỉ số dự đoán trong Adapty UA](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 đến thời điểm mục tiêu. Được mô hình hóa từ dữ liệu lưu giữ cohort lịch sử của ứng dụng. | Có | | **pROAS** | Lợi tức chi phí 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ả 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. | Có | --- # File: ua-predicted-metrics --- --- title: "Chỉ số dự đoán trong Adapty UA" description: "Dự báo doanh thu, ROAS, lợi nhuận quảng cáo và LTV cho các cohort trong Adapty UA." --- :::important Bài viết này đề cập đến các dự đoán trong Adapty User Acquisition. Để xem dự đoán LTV và doanh thu trên trang phân tích Cohort, hãy tham khảo [Dự đoán trong cohort](predicted-ltv-and-revenue). ::: Adapty UA dự báo doanh thu tương lai và unit economics 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 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ỳ gói đăng ký dài. ## Các 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 mốc thời gian mục tiêu. Được mô hình hóa từ dữ liệu giữ chân cohort lịch sử của ứng dụng. | | **pROAS** | Lợi tức trên chi tiêu quảng cáo dự đoán trong cùng mốc thời gian. **Công thức**: `(pRevenue / Spend) × 100%` | | **pAdProfit** | Doanh thu dự đoán sau khi trừ chi tiêu quảng cáo trong mốc 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 mốc 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 mốc thời gian. **Công thức**: `pRevenue / paying users at d{N}`, trong đó `d{N}` khớp với mốc thời gian đã chọn. | `pRevenue` là giá trị cơ sở. Bốn chỉ số còn lại được tính từ giá trị này kết hợp với dữ liệu cohort thực tế — chi tiêu, lượt cài đặt và người dùng trả phí — chứ không phải bằng cách chạy mô hình riêng biệt. Mỗi chỉ số dự đoán có thể xem theo nhiều khoảng thời gian cohort: 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 theo ngày. Khoảng thời gian này xác định giá trị được chiếu bao xa vào tương lai kể 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\} 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 cohort đã được ghi nhận. Doanh thu ban đầu bao gồm các lượt bắt đầu gói đăng ký, chuyển đổi từ dùng thử và các 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ấu trúc 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ó thời gian dùng thử ngắn**: Ngày cơ sở thường nằm ngay sau khi dùng thử chuyển đổi. - **Ứng dụng có thời gian dùng thử dài hơn**: Ngày cơ sở có thể là một tuần hoặc hơn sau khi cài đặt, vì hầu hết doanh thu ban đầu chỉ xuất hiện sau khi kết thúc giai đoạn dùng thử. ### 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ỉ được hiển thị khi cohort có đủ dữ liệu để hỗ trợ. 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ể chiếu về phía trước. - **Không đủ dữ liệu lịch sử cho ứng dụng**: Nếu ứng dụng không có đủ các cohort trong quá khứ thuộc loại gói đăng ký liên quan, mô hình 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 hàng 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: "Liên kết theo dõi trong Adapty User Acquisition" 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." --- Liên kết theo dõi giú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 từ 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 nhiều nhất trên [trang Analytics](ua-analytics) của bạn. Bạn có thể tạo hai loại liên kết theo dõi: - **One link** — một liên kết phổ quát tự động nhận diệ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 từng cửa hàng** — liên kết nhắm đến từng 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 liên kết theo dõi \{#create-tracking-links\} Để tạo liên kết theo dõi: 1. Trong Adapty UA dashboard, vào **Tracking links** từ menu sidebar. Tại đó, nhấp **Create link**. 2. Chọn ứng dụng của bạn từ danh sách và nhấp **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. | Tham số | Mô tả | |-------------------|-----------------------------------------------------------------------------------------------| | **Name** | Tên nội bộ của liên kết theo dõi. | | **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 một One Link. Liên kết này tự động nhận diệ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 lại 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ấp **Create**. 6. Mở trang liên kết theo dõi của bạn 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** — các phiên bản theo từng nền tảng tùy chọn nếu bạn muốn có liên kết riêng cho mỗi cửa hàng. :::tip Bạn cũng có thể đặt thêm các tham số liên kết để [làm việc với dữ liệu trì hoãn](ua-deferred-data). Ví dụ: bạn có thể triển khai deep linking trì hoãn. ::: 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 quảng cáo. Giờ đây, 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: "Deeplink trì hoãn trong Adapty User Acquisition" description: "Thiết lập deeplink trong Adapty User Acquisition" --- Deeplink trì hoãn 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 nó 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. Cơ chế hoạt động như sau: 1. Khi người dùng nhấp vào quảng cáo, Adapty lưu dữ liệu của lượt nhấp đó. 2. Khi Adapty ghi nhận sự kiện cài đặt, nó lấy dữ liệu trì hoãn 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 tiên, 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ố dữ liệu trì hoãn sau: - `ios_deferred_data` - `android_deferred_data` - `deferred_data_sub[1-10]` Để thêm tham số dữ liệu trì hoãn, hãy gắn chúng vào link trong cài đặt chiến dịch của bạn: 1. Mở chiến dịch của bạn từ trang **Integrations -> Meta/TikTok Ads**. Hoặc, mở tracking link của bạn từ trang **Tracking links**. Sao chép click link bạn sẽ 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 đó gắn thêm các tham số dữ liệu trì hoãn vào cuối dưới dạng query parameter bổ sung — mỗi tham số có tiền tố `&`. Ví dụ, để điều hướng 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ẽ trông như thế này: ``` 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 ứng dụng của bạn. Lưu ý rằng các tham số dữ liệu trì hoãn 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ụ, dưới đây là cách xử lý các lượt cài đặt có `ios_deferred_data` bằng `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" description: "Truy cập dữ liệu attribution của 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ó trả về dữ liệu attribution cho ứ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 object `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 thu nạp 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 | | `adset_name` | Tên nhóm quảng cáo | | `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 object `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 UA" description: "Kết nối Meta Ads với Adapty UA để theo dõi và tối ưu hiệu suất chiến dịch trên Facebook, Instagram, Messenger và Audience Network." --- Tích hợp Meta của Adapty UA cho phép bạn theo dõi và tối ưu 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 \{#step-1-connect-your-facebook-account\} Để kết nối Meta Ads với Adapty UA, vào **Integrations > Meta** từ thanh 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 system user token 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 system user của Meta Business. <Tabs> <TabItem value="oauth" label="Continue with Facebook"> :::important Hãy đả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ấn **Continue with Facebook**. 2. Đăng nhập bằng tài khoản Facebook và nhấn **Continue**. 3. Xem lại các quyền được yêu cầu và nhấn **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 UA. :::important Trước khi bắt đầu, bạn cần có [system user](https://www.facebook.com/business/help/503306463479099) trong portfolio Meta Business 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 cho system user đó. Bạn cũng cần 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. Vào **Business Settings**. 2. Trong mục **Users**, chọn **System users**. 3. Chọn system user của bạn, sau đó 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 UA 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ẽ bị gián đoạn. Token không có ngày hết hạn tránh được vấn đề này, nhưng là một credential 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 UA, 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 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 Meta vào Adapty User Acquisition 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 campaign**. Chọn app và nhấn **Save**. 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 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 trong Meta. 4. Để gửi conversion event trở lại Meta, bạn cũng có thể liên kết các pixel từ Meta với các chiến dịch trong Adapty UA. Để làm vậy, chọn một trong các pixel hiện có của bạn trong dropdown **Pixel**. ## Bước 3. Ánh xạ sự kiện \{#step-3-map-events\} Để gửi conversion event trở lại Meta nhằm tối ưu 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 app. 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ừ dropdown hoặc đặt một sự kiện tùy chỉnh. Mặc định, Adapty ánh xạ các sự kiện Adapty sang các sự kiện chuẩn của Meta. Nhấn **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. Điều này hữu ích khi bạn cần truyền dữ liệu chiến dịch hoặc người dùng cụ thể đến các công cụ analytics 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ới dữ liệu attribution. Tham số bổ sung sẽ được đưa vào tất cả dữ liệu attribution gửi đến Meta và có thể dùng để phân tích và tối ưu 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 Tham số bổ sung thay đổi **Click link** bạn dán vào Meta Ads Manager. Nếu bạn đã sao chép link đó và thêm tham số tùy chỉnh sau, hãy đảm bảo sao chép và dán lại link click đã 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ả phương thức khớp xác định và xác suất. Để cấu hình, vào tab **Settings** trong cấu hình chiến dịch. Ở đây bạn sẽ thấy hai cài đặt chính cần điều chỉnh: - **Deterministic matching window**: 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 chiến dịch với độ chính xác cao. Đặt 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ấn quảng cáo Meta và cài app trong khoảng thời gian này, Adapty có thể quy kết chắc chắn lượt cài đặt cho lần nhấn quảng cáo cụ thể đó bằng định danh thiết bị. - **Probabilistic matching window**: Sử dụng mô hình thống kê và device fingerprinting để khớp người dùng khi không thể dùng phương thức xác định. Đặt thành 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 cho hầu hết trường hợp. Với các chiến dịch có lượng click cao, bạn có thể giảm xuống 1–2 giờ. Với những người dùng không thể khớp theo phương thức xác định (do cài đặt bảo mật hoặc các yếu tố khác), Adapty 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 sự kiện dùng thử và muốn Meta quy attribution doanh thu cho chúng, hãy 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ử, đặt tỷ lệ phần trăm giá gói đăng ký để báo cáo là doanh thu. Ví dụ, ở mức 30%, Adapty gửi 30% giá gói đăng ký làm giá trị chuyển đổi cho sự kiện dùng thử. Để thêm 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 tỷ lệ 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 UA" description: "Kết nối TikTok for Business với Adapty UA để theo dõi và tối ưu hiệu suất chiến dịch trong TikTok Ads Manager." --- Tích hợp TikTok for Business của Adapty UA cho phép bạn theo dõi và tối ưu 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 trên 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 User Acquisition 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 campaign**. Chọn ứng dụng và nhấp **Save**. 2. Trong tab **General**, mở rộng phần **iOS** và/hoặc **Android** và 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 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à giao dịch mua 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 pixel TikTok của mình với các chiến dịch trong Adapty UA. Để thực hiện, chọn một trong các pixel hiện có của bạn trong dropdown **Pixel**. ## Bước 3. Ánh xạ sự kiện \{#step-3-map-events\} Để gửi sự kiện chuyển đổi trở lại TikTok nhằm tối ưu 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 pixel TikTok 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 TikTok Ads Manager. Với mỗi sự kiện được bật, chọn sự kiện TikTok tương ứng từ dropdown hoặc đặ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 TikTok. 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. Điều này hữu ích khi bạn cần truyền dữ liệu chiến dịch hoặc 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 dữ liệu theo dõi attribution. Tham số bổ sung 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 và tối ưu 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 giữa các cách tiếp cận sáng tạo khác nhau. :::important 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 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 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 và xác suất. Để cấu hình, 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: - **Deterministic matching window**: 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 chiến dịch một cách 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 chính xác lượt cài đặt đó thuộc về lượt nhấp quảng cáo cụ thể đó bằng định danh thiết bị. - **Probabilistic matching window**: Sử dụng mô hình thống kê và device fingerprinting để khớp người dùng khi không thể thực hiện khớp xác định. Đặt giá trị này thành 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. Với các chiến dịch có lượng nhấp 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ể khớp theo cách xác định (do cài đặt quyền riêng tư hoặc các yếu tố khác), Adapty sử dụng khớp xác suất trong khoảng thời gian ngắn hơn này. Nhấp **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 từ 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ử, đặt tỷ lệ phần trăm của giá gói đăng ký để báo cáo làm doanh thu. Ví dụ, ở mức 30%, Adapty 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 ghi đè: 1. Bật **Trial started** trong phần **Events names**. 2. Trong **Revenue override**, nhấp **Add override**. 3. Chọn sự kiện mục tiêu và nhập tỷ lệ phần trăm doanh thu (0–100). 4. Nhấp **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 UA" description: "Kết nối các funnel web-to-app của FunnelFox với Adapty UA để 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 dùng trả phí." --- [FunnelFox](https://funnelfox.com) là nền tảng xây dựng funnel web2app, 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 cửa hàng. Sau khi kết nối, FunnelFox gửi các sự kiện giao dịch đến Adapty UA, cung cấp cho bạn toàn bộ đường dẫn attribution từ điểm chạm trên web đến người dùng trả phí. Để 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 mua hàng trong funnel FunnelFox của bạn, FunnelFox sẽ gửi sự kiện giao dịch đến Adapty UA. Adapty sử dụng **Project ID** để xác định giao dịch thuộc về ứng dụng nào. Sự kiện sau đó được lưu trữ và hiển thị trong phân tích UA 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 từ dùng thử, đã hoàn tiền, và nhiều hơn nữa - **Dữ liệu attribution**: định danh chiến dịch, nhóm quảng cáo và quảng cáo; các 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 trong 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) dựa trên 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 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 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" 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 UA với bộ nhớ tương thích S3 tùy chỉnh 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ữ tương thích S3 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 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 bảng điều khiển bộ nhớ tương thích S3 và Adapty UA dashboard. :::note Adapty UA 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 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 UA: 1. Truy cập [**Integrations** -> **Custom S3**](https://app.adapty.io/ua/integrations/custom-s3) 2. Bật toggle **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à hồ sơ người dùng Adapty UA | 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ụ lưu trữ tương thích S3 của bạn. 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 bí mật được sử 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 của bạn. 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 để 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ư file 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 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. | | **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 được cung cấp bởi nhà cung cấp dịch vụ lưu trữ (ví dụ: MinIO, DigitalOcean Spaces, Wasabi, v.v.). | :::note Bạn cũng có thể chỉ định các thư mục lồng nhau 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 Custom S3, Adapty UA 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 nguồn 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 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 mã 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 duy nhất | | `install_id` | Mã định danh cài đặt duy nhất | | `created_at` | Dấu thời gian tạo bản ghi (ISO 8601) | | `installed_at` | Dấu thời gian 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ã định danh bundle ứng dụng (ví dụ: `com.example.app`) | | `device_brand` | Thương hiệu thiết bị (ví dụ: `Apple`, `Samsung`) | | `device_model` | Mẫu 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 được SDK Adapty báo cáo | | `sdk_version` | Phiên bản Adapty SDK | | `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 ad set | | `adset_name` | Tên ad set | | `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: ua-amazon-s3 --- --- title: "Amazon S3" 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 của Adapty UA với Amazon S3 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 vị trí 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 Amazon S3 bucket 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 AWS Console và dashboard Adapty UA. :::note Adapty UA 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 theo 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-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 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ể thêm tags (tùy chọn) rồi nhấn **Next** để chuyển sang bước cuối 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 UA 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 người dùng có quyền ghi vào bucket cụ thể đó. 1. Truy cập IAM Console và chọn [phần Users](https://console.aws.amazon.com/iamv2/home#/users) 2. Nhấn 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 chuyển sang phần 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, 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 */ }} /> 5. Tiếp theo, đặt tên cho User Group và chọn policy mà bạn đã tạo trước đó 6. Sau khi chọn policy, nhấn nút **Create group** để hoàn tất <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 group thành công, hãy **chọn group đó** 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 nút **Create User** để hoàn tất <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 xác thực dưới dạng .csv** hoặc sao chép và dán thông tin xác thực 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 UA \{#step-2-configure-integration-in-adapty-ua\} 1. Vào [**Integrations** -> **Amazon S3**](https://app.adapty.io/ua/integrations/s3) 2. Bật toggle **Export install events to Amazon S3**. 3. Điền các trường sau để kết nối Amazon S3 với hồ sơ người dùng Adapty UA: | 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 [file csv](ua-amazon-s3#step-1-create-amazon-s3-credentials) đã tải về. | | **Secret Access Key** | Khóa bí mật dùng kết hợp với Access Key ID để xác thực quyền truy cập vào dịch vụ AWS. Tìm khóa này trong [file csv](ua-amazon-s3#step-1-create-amazon-s3-credentials) đã tải về. | | **S3 Bucket Name** | Tên duy nhất toàn cầu xác định một S3 bucket cụ thể trong AWS cloud. 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ư file và hình ảnh trên cloud. | | **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 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 IAM user. | ## 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 UA 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 và 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 — 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ể, 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 duy nhất | | `install_id` | Mã định danh cài đặt duy nhất | | `created_at` | Timestamp tạo bản ghi (ISO 8601) | | `installed_at` | Timestamp 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 ad set | | `adset_name` | Tên ad set | | `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 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: ua-google-cloud-storage --- --- title: "Google Cloud Storage" description: "Tích hợp Google Cloud Storage với Adapty UA để lưu trữ dữ liệu thu hút người dùng một cách an toàn." --- Tích hợp Adapty UA 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 UA Dashboard. :::note Lịch trình Adapty UA gửi dữ liệu của bạn đến Google Cloud Storage 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 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 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 UA có thể tải các báo cáo dữ liệu thô lên 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 service account và cấp quyền ghi vào bucket, bạn cho phép Adapty UA chuyển dữ liệu thô từ nền tảng của mình 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 Service Account HMAC key, do đó cần đảm bảo rằng Service Account HMAC key của bạn có các vai trò "Storage Object Viewer", "Storage Legacy Bucket Writer" và "Storage Object Creator" để cho phép truy cập đúng vào Google Cloud Storage. ::: #### 2.1. Tạo Service Account \{#21-create-service-account\} 1. 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 liên quan 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 service account mới cho Adapty UA bằng cách nhấp 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 vào các trường ở bước đầu tiên, vì quyền truy cập sẽ được cấp ở giai đoạn 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), điều hướng đến phần 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 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 để ủy 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 cho 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 bucket hiện có hoặc tạo bucket mới để lưu trữ các báo cáo User Acquisition Data từ Adapty UA 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 service account đã lấy ở bước thứ năm ở trên, sau đó chọn vai trò Storage Object Creator 9. Cuối cùng, nhấp vào 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 tên bucket để tham khảo sau 11. Sau khi hoàn thành các bước này, bạn đã thiết lập thành công 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 UA <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 UA \{#step-2-configure-integration-in-adapty-ua\} 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 UA: | Trường | Mô tả | |:------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Google Cloud service account key file** | File khóa [JSON](ua-google-cloud-storage#step-1-create-google-cloud-storage-credentials) riêng tư đã tải xuống. | | **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 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 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 UA 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à thu nhập, cửa hàng xuất xứ, cùng các đ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 có dữ liệu mới được chúng tôi hoặc các bên thứ ba 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 trên 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ơ Adapty duy nhất | | `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 ad set | | `adset_name` | Tên ad set | | `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: "AI-generated email campaigns that turn trial users into paid subscribers." --- <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 turns your Adapty user data into AI-generated email sequences that convert trial users into paid subscribers. It uses the profile data already in your Adapty project to build, send, and attribute campaigns — no separate email platform required. ## Why Adapty Mail? Sending targeted email campaigns requires copy, design, sending infrastructure, and revenue attribution. Each of these is a separate problem to solve. Adapty Mail handles all of it. Your brand profile is built from your store URL and any other sources you add, and a complete email sequence is generated from it in under 2 minutes — sent from your own domain with personalized checkout links and purchase attribution. ## How it works 1. **Collect emails**: Your app passes user emails and `customer_user_id` values to Adapty via SDK. Adapty Mail uses this data to identify recipients and attribute revenue to the specific email that drove each purchase. You can also send this data from your server with the [Adapty Mail API](mail-send-data-via-api). 2. **Build a web paywall**: The checkout page every email links to. 3. **Generate a sequence**: AI uses your brand profile to produce 1–15 emails — copy, design, hero images, and personalized checkout links matched to your app's category and brand voice. 4. **Launch a flow**: Pick a trigger (never purchased, renewal cancelled, billing issue, subscription expired, or refunded) and a segment, then attach your campaign. Emails start sending automatically, and revenue from email-driven purchases is attributed back to the specific email that drove the conversion. ## Requirements To use Adapty Mail, you need: - An Adapty account - Email collection set up in your app — see [Collect user emails](mail-collect-emails) - `customer_user_id` configured in your Adapty SDK - A domain you control with access to its DNS settings - A web payment provider (Stripe, Paddle, or PayPal) ## Get started Follow the [Get started with Adapty Mail](mail-get-started) guide to complete setup and launch your first campaign. --- # 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. :::note Bạn cũng có thể gửi dữ liệu đến Adapty Mail từ server của mình, không cần SDK. Nếu bạn đã lưu trữ email người dùng và dữ liệu mua hàng ở backend, hoặc đang import người đăng ký từ mộ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 phần: 1. [Cấu hình SDK](#1-configure-your-adapty-sdk) 2. [Thiết lập domain gửi email](#2-set-up-your-sending-domain) 3. [Tạo web paywall](#3-create-a-web-paywall) 4. [Tạo campaign bằng 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 email](#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 SDK \{#1-configure-your-adapty-sdk\} :::important Adapty Mail là một **sản phẩm độc lập**. Bạn có thể dùng nó ngay cả khi paywall, gói đăng ký hoặc phân tích 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 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 (để gắn doanh thu với email đã thúc đẩy mỗi lần chuyển đổi), một mã định danh người dùng ổn định, và email người dùng. 1. **Cho phép 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 thêm gì ở 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. Ở giai đoạn 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 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ý mua hàng cho Adapty. Sau đó [bật thông báo server 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 danh 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()` lúc khởi động SDK. `customer_user_id` là cách Adapty Mail khớp campaign, lượt nhấp, và giao dịch mua hàng với đú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 (ví dụ: lúc đăng ký hoặc thanh toán), hãy truyền nó cho Adapty bằng cách gọi `updateProfile` với thuộc tính email. Mỗi người nhận trong campaign đều cần 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, xem [Chiến lược thu thập email](mail-collect-emails#email-collection-strategies). ## 2. Thiết lập domain gửi email \{#2-set-up-your-sending-domain\} Adapty Mail gửi từ domain của riêng bạn. Bạn chỉ cần thêm bản ghi DNS một lần — tất cả campaign đều dùng chung domain đã xác minh. 1. Trong Adapty Mail, vào **Settings → Email Domains**. 2. Nhập domain gốc của bạn (ví dụ: `yourapp.com`) và nhấp **Preview**. Chỉ apex domain được chấp nhận — subdomain như `app.yourapp.com` sẽ bị từ chối ngay khi nhập. 3. Adapty tạo hai subdomain gửi email (`mail.yourapp.com` và `email.yourapp.com`). Nhấp **Confirm** để xem các bản ghi DNS cần thiết. 4. Trong nhà đăng ký 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 domain gốc của bạn (được khuyến nghị). 6. Quay lại **Settings → Email Domains** và nhấp **Check Verification**. Tổng quan về thời gian xác minh: - **Polling tự động**: Lần kiểm tra đầu tiên chạy khoảng 5 phút sau khi bạn gửi. Khoảng cách 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 **Check Verification** bất cứ lúc nào để kích hoạt kiểm tra ngay lập tức. - **Lan truyền DNS**: Thường mất vài phút, tối đa 48 giờ trong những trường hợp hiếm gặp. - **Cửa sổ 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òn nguyên — nhập lại domain của bạn trong **Settings → Email Domains** để bắt đầu 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 email](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**. ### Lựa chọn A: Tạo bằng AI \{#option-a-generate-with-ai\} Trang hiển thị danh sách **Prerequisites** với các nút inline — thực hiện theo thứ tự, rồi quay lại và tạo. Danh sách này bao gồm đăng nhập vào trình tạo paywall, 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) để có hướng dẫn đầy đủ. Khi tất cả điều kiện tiên quyết đã xanh, nhấp **Generate** để mở hộp thoại tạo: - **Environment**: Chọn **Production** hoặc **Sandbox**. Sandbox dùng các 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 phát triển 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** để chạy quá trình build. Khi hoàn tất, mở trình chỉnh sửa để xem lại và publish. :::important Paywall phải được publish trước khi có thể phục vụ lưu lượng thanh toán. Các paywall chưa publish sẽ trả về lỗi khi người dùng nhấp vào liên kết thanh toán trong email. ::: ### 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 campaign bằng AI \{#4-generate-a-campaign-with-ai\} AI tạo toàn bộ chuỗi email cho bạn — nội dung, thiết kế, hình ảnh hero, và liên kết thanh toán được cá nhân hóa, tất cả được điều chỉnh theo thương hiệu của bạn. 1. Trong Adapty Mail, vào **Campaigns** và nhấp **Create**. 2. Đặt tên campaign. 3. Trong dropdown **Web paywall**, chọn web paywall bạn đã thêm ở bước trước. 4. Nhấp **Generate emails**. 5. Điền vào hộp thoại tạo — giọng điệu, ngôn ngữ, prompt 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 4). Xem [Tạo campaign](mail-create-campaign) để biết chức năng 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 hết thời gian chờ sau 5 phút nếu không thể hoàn thành — hãy thử lại nếu điều đó xảy ra. 7. Xem trước từng email. Header xem trước có **Theme toggle** (Auto, Light, Dark) để kiểm soát cách hiển thị 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 riêng lẻ, 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 campaign. Campaign được lưu dưới dạng **draft** và chưa gửi — campaign chỉ hoạt động khi được gắn vào một flow (bước tiếp theo). Không có hành động "publish" riêng trong trình chỉnh sửa campaign. ## 5. Khởi chạy một flow \{#5-launch-a-flow\} Một flow kết hợp một **trigger** (một sự kiện như gói đăng ký hết hạn) với một **segment**, và gửi cho segment đó **campaign** bạn chọn. Adapty Mail đi kèm với năm trigger cố định, mỗi trigger có view 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 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 thời gian ân hạn. - **Expired** — gói đăng ký đã hết hạn và quyền truy cập đã bị thu hồi. - **Refunded** — người dùng đã yêu cầu hoàn tiền sau khi mua. Xem [Flows](mail-flows) để hiểu mục tiêu và hướng dẫn giọng điệu cho từng trigger. 2. Nhấp **Create** để mở hộp thoại. 3. Trong hộp thoại: - Chọn một **Segment** (ví dụ: **All Users** để nhắm mục tiêu tất cả mọi người khi họ kích hoạt trigger này, hoặc tạo một segment mới dựa trên thuộc tính hồ sơ người dùng). - Để loại nội dung ở **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ấp **Save**. Flow hoạt động ngay lập tức — không có bước khởi chạy riêng. Từ thời điểm này, những người dùng khớp với segment sẽ bắt đầu nhận campaign ngay khi họ kích hoạt sự kiện trigger. :::note Bạn có thể thêm nhiều hơn một hàng segment → campaign vào cùng một trigger; chúng chạy theo thứ tự ưu tiên. Hàng **All Users**, nếu được dùng, phải là hàng cuối cùng (ưu tiên thấp nhất) để nó bắt tất cả những người không khớp với segment cụ thể hơn. ::: ## 6. Bật tính năng gửi email \{#6-enable-sending\} Cho đến thời điểm này, campaign 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 nó lên là bước cuối cùng: các sự kiện bắt đầu chạy, các segment bắt đầu khớp, và email bắt đầu được gửi. Bước này chỉ mở khóa sau Bước 5. Trước khi bạn khởi chạy một flow, nút **Enable** trong **Settings → Integrations** bị vô hiệu hóa với tooltip *"Set up at least one flow before enabling Adapty integration."* 1. Trong Adapty Mail, vào **Settings → Integrations**. 2. Nhấp **Enable Adapty integration** (hoặc **Enable** nếu tích hợp đã tồn tại từ lần thiết lập trước). Khi được bật, Adapty gửi mọi sự kiện gói đăng ký — gói 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 tư cách thành viên segment, định tuyến campaign, 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* giống với workspace đối tác Adapty đã đăng nhập bạn vào Adapty Mail. Workspace đối tác là thứ đã tạo tài khoản của bạn và (nếu bạn đăng ký qua Adapty) thương hiệu của bạn. Toggle tích hợp ở đây kiểm soát việc đồng bộ sự kiện — nó phải được bật theo từng dự án. ::: ## Xử lý sự cố \{#troubleshooting\} | Vấn đề | Giải pháp | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | | Xác minh DNS bị kẹt | Kiểm tra các bản ghi khớp chính xác — không có dấu chấm ở cuối, đúng CNAME target. Đợi 5–10 phút, sau đó nhấp **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 domain 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 tiếp diễn, hãy liên hệ với 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: "Email campaigns in Adapty Mail" description: "Design multi-email sequences, pick the right tone, and target the right users." --- A campaign in Adapty Mail is a complete multi-email sequence — copy, design, hero images, and delays — generated for your app in one pass. A campaign on its own doesn't send: it saves as a **draft** and starts delivering only once you attach it to a [flow](mail-create-flow), which binds it to a trigger and audience. Use the guides below to create campaigns, pick the right tone, and target the right users. <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: "Flows in Adapty Mail" description: "How flows route campaigns to the right users at the right moment — triggers, segments, and priority rules." --- <CustomDocCardList ids={['mail-create-flow']} /> A **flow** turns a saved campaign into scheduled deliveries. It binds a trigger event (a user's subscription state) to a segment (which users qualify) and the campaign they receive. Adapty Mail evaluates every flow whenever a matching event fires — no polling, no cron, no manual launch. ## Triggers Adapty Mail ships with five fixed triggers, each with its own flow view under **Flows**: - **Never purchased**: Users who signed up but haven't made a purchase yet. Goal: activation and first conversion. Trial users aren't here — starting a trial counts as an active subscription. - **Renewal cancelled**: Users who turned off auto-renewal but still have an active subscription. Covers both paid subscribers and trial users who cancelled before conversion. Strongest window to save them — they still have access. Split paid and trial audiences via segment filters if messaging needs to differ. - **Billing issue**: Payment failed — declined or expired card, or grace period after a missed renewal. Goal: urgent, helpful recovery, not a sales push. Recover them quickly — they already wanted to pay. - **Expired**: Subscription has lapsed and access is gone. Covers both paid expirations and trials that ran out without converting. Goal: win them back. Segment filters can tailor copy for trial-expired and paid-expired audiences. - **Refunded**: Users who requested a refund after purchase. Goal: understand what went wrong and offer a better-fit option. Tone should stay humble and curious, not a hard resell. Triggers are not configurable — you can't create custom triggers or extend the list. ## The All Users segment Adapty Mail ships with a built-in **All Users** segment that has no filters — every user in the project qualifies. It's most useful in flows as a catch-all row, serving anyone not matched by a more specific segment above it. All Users can't be edited or deleted. See [Segments](mail-segments) for the full rundown. ## Priority Each trigger view holds a list of **segment → campaign** (or segment → A/B test) rows, ordered by priority. When a user hits the trigger, Adapty Mail: 1. Walks the rows top to bottom. 2. Sends the campaign in the first row whose segment matches. 3. Stops. Later rows are not evaluated for that user. Order matters. A broad segment placed above a narrower one swallows every user who would otherwise match the narrower row. To reorder, drag the handle on the left of any row — the backend reassigns priority numbers 1, 2, 3… based on the saved order. :::important The **All Users** row, if present, must be last (lowest priority). The backend rejects saves where All Users isn't in the final slot — it would otherwise swallow every user before more specific segments get a chance to match. ::: ## Content types A row can deliver either a single campaign or an A/B test: - **Campaign**: Sends one campaign to everyone who matches the segment. - **A/B Test**: Wraps two or more campaigns with configurable weights, routes incoming users across them randomly, and tracks per-variant metrics. See [A/B testing](mail-ab-testing). ## Lifecycle Flow rows have no draft state. A row is live the moment you save it — from that point on, users who hit the trigger and match the segment are routed to its campaign. - **Create a row**: Starts delivering immediately on save. - **Edit a row**: The change applies to users who hit the trigger from then on. Users already mid-sequence continue with the previous configuration. - **Delete a row**: New users stop entering the sequence. Users already mid-sequence may continue receiving their scheduled emails — there's no automatic cancellation. A/B test rows follow their own lifecycle (**draft → live → finished**) controlled separately from the row itself. See [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 nhóm đối tượng có thể tái sử dụng dựa trên dữ liệu hồ sơ và mua hàng để nhắm mục tiêu cho flow và A/B test." --- **Phân khúc** là một nhóm đối tượng có thể tái sử dụng. Bạn định nghĩa một lần — trong **Segments** — và tham chiếu từ các flow cùng A/B test. Phân khúc là định nghĩa bộ lọc, không phải ảnh chụp tại thời điểm cố đị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ơ 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 mới 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 [quy tắc](#available-filter-fields) bạn muốn thêm. Mỗi bộ lọc sẽ trở thành một thẻ có thể thu gọn, được đặt tên là **Filter 1**, **Filter 2**, v.v. 4. Với mỗi bộ lọc, chọn một 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 khớp với tất cả bộ lọc mới thuộc 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ị, hãy tách logic thành các phân khúc riêng biệt. ::: ## Các trường bộ lọc có sẵn \{#available-filter-fields\} | Nhóm | Trường | Kiểu | | -------------- | ------------------------- | ------- | | Profile | Email | String | | Profile | Age | Integer | | Profile | Country | String | | Profile | External profile ID | String | | Profile | Created at | Date | | Purchase state | Total revenue (USD) | Decimal | | Purchase state | Subscription state | Enum | | Purchase state | Subscription purchased at | Date | | Purchase state | Subscription expires at | Date | | Purchase state | One-time purchase state | Enum | | Purchase state | One-time purchased at | Date | **Giá trị Subscription state**: Never purchased, Subscribed, Auto-renew off, Billing issue, Grace period, Expired, Refunded. **Giá trị One-time purchase state**: Never purchased, Purchased, Refunded. Các toán tử có sẵn theo kiểu 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 một phân khúc tích hợp sẵn là **All Users** — không có bộ lọc nào, tức là 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 được dùng trong flow, nó đóng vai trò là hàng bắt tất cả ở cuối cùng (xem [Flows](mail-flows) để biết quy tắc ưu tiên). ## Vòng đời \{#lifecycle\} Trạng thái của 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ào flow hay A/B test nào. - **Live**: Đã gắn vào 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**: Đã 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 thu hẹp danh sách theo từng trạng thái. ## Chỉnh sửa và xóa phân khúc \{#edit-and-delete-a-segment\} - **Name và description**: Luôn có thể chỉnh sửa. - **Filters trên phân khúc Draft**: Có thể chỉnh sửa hoàn toàn. - **Filters trên phân khúc Live**: Bị khóa. Khi phân khúc đã được tham chiếu bởi một hàng flow đ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à thay thế hàng flow. - **Delete**: 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. ## Giới hạn \{#limitations\} - **Không có logic OR, không lồng nhau**: Các bộ lọc chỉ kết hợp bằng AND. - **Mỗi phân khúc một trường**: 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 country). - **Không xem trước kích thước**: Trình soạn thảo không hiển thị số lượng người dùng hiện tại khớp với các bộ lọc. - **Filters bị khóa khi live**: Các phân khúc đang hoạt động chỉ có thể chỉnh sửa tên và mô tả. --- # 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: "Analytics 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, tin nhắn hoặc trigger — và xem số liệu giao hàng, tương tác và doanh thu cùng một nơi." --- 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\} Mỗi hàng — trên trang Analytics, trong chế độ xem chi tiết và trong các chế độ xem nội tuyến được mô tả bên dưới — hiển thị cùng một tập hợp số đếm 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 analytics cơ bản và cho các tích hợp SplitMetrics Acquire và Asapty. [Apple Ads Manager](adapty-ads-manager) sử dụng kết nối riêng biệt. Kết nối tài khoản Apple Ads của bạn trong [cài đặt Apple 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ố theo 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 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 AdServices framework \{#connect-adapty-to-the-adservices-framework\} Apple Ads qua [AdServices](https://developer.apple.com/documentation/adservices) yêu cầu một số cấu hình trong Adapty Dashboard, đồng thời 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 bằng AdServices framework qua Adapty, làm theo các bước sau: #### Bước 1: Lấy public key \{#step-1-obtain-public-key\} Trong Adapty Dashboard, truy cập [Settings -> Apple Ads.](https://app.adapty.io/settings/apple-search-ads) Tìm public key đã được tạo sẵn (Adapty cung cấp cặp khóa 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 dùng dịch vụ khác hoặc giải pháp riêng cho attribution Apple Ads, bạn có thể tải lên private key của mình. ::: #### 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, 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 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 riêng 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. Vào Settings -> API trong giao diện Apple Ads. Dán public key đã sao chép 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 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à 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 Lưu ý rằng việc tắt tính năng này sẽ hoàn toàn dừng việc nhận analytics 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 phụ thuộc vào attribution ASA để hoạt động đúng. Dữ liệu attribution nhận được trước 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`. --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="note"> Nếu bạn dùng ID người dùng của bên thứ ba làm Customer User ID, đừng truyền nó trong `activate()` — SDK của bên thứ ba có thể chưa tạo ID đó. Thay vào đó, hãy gọi `activate()` trước (không có CUID), sau đó gọi `setIntegrationIdentifier()`, rồi mới gọi `identify()` với CUID. </Callout> <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`. --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="note"> Nếu bạn dùng ID người dùng của bên thứ ba làm Customer User ID, đừng truyền nó trong `activate()` — SDK của bên thứ ba có thể chưa tạo ID đó. Thay vào đó, hãy gọi `activate()` trước (không có CUID), sau đó gọi `setIntegrationIdentifier()`, rồi mới gọi `identify()` với CUID. </Callout> <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: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="note"> Nếu bạn dùng ID người dùng của bên thứ ba làm Customer User ID, đừng truyền nó trong `activate()` — SDK của bên thứ ba có thể chưa tạo ID đó. Thay vào đó, hãy gọi `activate()` trước (không có CUID), sau đó gọi `setIntegrationIdentifier()`, rồi mới gọi `identify()` với CUID. </Callout> <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: "Tích hợp Firebase và Google Analytics với Adapty để có thêm thông tin chi tiết." --- Nếu bạn đang sử dụng các sản phẩm của Google như Google Analytics và Firebase, bạn có thể làm phong phú thêm dữ liệu phân tích của mình với các sự kiện từ Adapty thông qua tích hợp được mô tả trong bài viết này. Các sự kiện được gửi qua Google Analytics đến Firebase và có thể được sử dụng trong bất kỳ dịch vụ nào trong số này. Tính năng này cho phép bạn liên kết hành vi người dùng với lịch sử thanh toán của họ trong Firebase, giúp bạn đưa ra các quyết định sản phẩm sáng suốt hơn. ## Cách thiết lập tích hợp Firebase \{#how-to-set-up-firebase-integration\} ### 1\. Thiết lập Firebase \{#1-set-up-firebase\} Trước tiên, bạn cần bật tích hợp giữa Firebase và Google Analytics. Bạn có thể thực hiện điều này trong Firebase Console tại tab **Integrations**. <img src="/assets/shared/img/14b6d84-CleanShot_2023-08-18_at_20.37.462x.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ích hợp với Adapty \{#2-integrate-with-adapty\} Adapty cần Firebase App ID và Google Analytics API Secret của bạn để gửi sự kiện và thuộc tính người dùng. Bạn có thể tìm các thông số này lần lượt trong Firebase Console và tab Google Analytics Data Streams. <img src="/assets/shared/img/14d8224-CleanShot_2023-08-21_at_12.14.182x.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, truy cập trang chi tiết App's Stream trong phần Data Streams của cài đặt Admin trên [Google Analytics.](https://analytics.google.com/analytics/web/#/) <img src="/assets/shared/img/b26ae6a-CleanShot_2023-08-21_at_12.28.482x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Trong **Additional settings**, chuyển đến trang **Measurement Protocol API secrets** và tạo một **API Secret** mới nếu chưa có. Sao chép giá trị đó. <img src="/assets/shared/img/7404bde-CleanShot_2023-08-21_at_12.33.242x.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/0266112-CleanShot_2023-08-21_at_12.34.442x.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ước tiếp theo của bạn là điều chỉnh tích hợp trong Adapty Dashboard. Bạn cần cung cấp cho chúng tôi Firebase App ID và Google Analytics API Secret cho nền tảng iOS, Android và/hoặc Stripe của mình. :::note Nếu bạn đang sử dụng tích hợp Stripe, hãy xem xét các giới hạn của nó trong [hướng dẫn](stripe#current-limitations) chuyên dụng. Những giới hạn này cũng sẽ áp dụng cho tích hợp Firebase. ::: <img src="/assets/shared/img/4eaae3f-CleanShot_2023-08-21_at_12.35.312x.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\} :::important Để tích hợp hoạt động, hãy đảm bảo bạn thêm Firebase vào ứng dụng trước: - [iOS](https://firebase.google.com/docs/ios/setup) - [Android](https://firebase.google.com/docs/android/setup) - [React Native](https://firebase.google.com/docs/web/setup) - [Flutter](https://firebase.google.com/docs/flutter/setup) - [Unity](https://firebase.google.com/docs/unity/setup) ::: Sau đó, bạn cần thiết lập Adapty SDK để liên kết người dùng với Firebase. Với mỗi người dùng, bạn cần gửi `firebase_app_instance_id` đến Adapty. Dưới đây là ví dụ về đoạn code có thể dùng để tích hợp Firebase SDK và Adapty SDK. <Tabs groupId="current-os" queryString> <TabItem value="swift" label="iOS (Swift)" 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> <TabItem value="kotlin" label="Android (Kotlin)" default> ```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="Java" default> ```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)" default> ```javascript showLineNumbers final appInstanceId = await FirebaseAnalytics.instance.appInstanceId; 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#)" default> ```csharp showLineNumbers using AdaptySDK; // We suppose FirebaseAnalytics Unity Plugin is already installed Firebase.Analytics .FirebaseAnalytics .GetAnalyticsInstanceIdAsync() .ContinueWithOnMainThread((task) => { if (!task.IsCompletedSuccessfully) { // handle error return; } var firebaseId = task.Result var builder = new Adapty.ProfileParameters.Builder(); Adapty.SetIntegrationIdentifier( "firebase_app_instance_id", firebaseId, (error) => { // handle the error }); }); ``` </TabItem> <TabItem value="rn" label="React Native (TS)" default> ```typescript showLineNumbers try { const appInstanceId = await analytics().getAppInstanceId(); await adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId); } catch (error) { // handle `AdaptyError` } ``` </TabItem> </Tabs> ## Gửi sự kiện và thuộc tính người dùng \{#sending-events-and-user-properties\} Bây giờ là lúc quyết định những sự kiện nào bạn sẽ nhận trong Firebase và Google Analytics. <img src="/assets/shared/img/7923397-set_up_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 */ }} /> Bạn có thể thấy rằng một số sự kiện có tên được chỉ định sẵn, ví dụ như "Purchase", trong khi các sự kiện khác là sự kiện Adapty thông thường. Sự khác biệt này xuất phát từ [các loại sự kiện của Google Analytics](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events). Hiện tại, các sự kiện được hỗ trợ là [Refund](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#refund) và [Purchase](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#purchase). Các sự kiện khác là sự kiện tùy chỉnh. Vì vậy, hãy đảm bảo rằng tên sự kiện của bạn được [hỗ trợ](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=firebase#limitations) bởi Google Analytics. Ngoài ra, bạn có thể thiết lập gửi thuộc tính người dùng trong Adapty dashboard. <img src="/assets/shared/img/e053006-CleanShot_2023-08-21_at_12.50.162x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Điều này có nghĩa là các sự kiện của bạn sẽ được Adapty bổ sung thêm `subscription_state` và `subscription_product_id`. Nhưng bạn cũng cần [bật](https://support.google.com/analytics/answer/14240153?hl=en) tính năng này trong Google Analytics. Vì vậy, để sử dụng **User properties** trong phân tích của bạn, hãy bắt đầu bằng cách gán chúng vào một custom dimension thông qua **Custom Definitions** trong Firebase Console bằng cách chọn **User scope**, đặt tên và mô tả cho chúng. <img src="/assets/shared/img/1962ef1-CleanShot_2023-08-21_at_12.48.222x.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/2425cc0-CleanShot_2023-08-21_at_12.52.532x.webp" style={{ border: '1px solid #727272', /* border width and color */ width: '700px', /* image width */ display: 'block', /* for alignment */ margin: '0 auto' /* center alignment */ }} /> Hãy kiểm tra rằng tên thuộc tính người dùng của bạn là `subscription_state` và `subscription_product_id`. Nếu không, chúng tôi sẽ không thể gửi dữ liệu trạng thái gói đăng ký cho bạn. Vậy là xong! Hãy chờ đón những thông tin chi tiết mới từ Google. ## Khắc phục sự cố \{#troubleshooting\} ### Sự chênh lệch dữ liệu \{#data-discrepancy\} Nếu có sự chênh lệch dữ liệu giữa Adapty và Firebase, điều đó có thể xảy ra vì 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ó tích hợp Adapty SDK. Để đảm bảo tính nhất quán của dữ liệu, bạn có thể buộc người dùng cập nhật ứng dụng lên phiên bản có Adapty SDK. Ngoài ra, các sự kiện sandbox được gửi đến Firebase theo mặc định và không thể tắt tính năng này. Vì vậy, trong các trường hợp ứng dụng có ít sự kiện Production và nhiều sự kiện Sandbox, có thể có sự chênh lệch đáng kể về số liệu giữa Analytics của Adapty và Firebase. ### Sự kiện hiển thị là đã gửi trong Adapty nhưng không có trong Firebase \{#events-are-shown-as-delivered-in-adapty-but-not-available-in-firebase\} Có độ trễ thời gian giữa lúc sự kiện được gửi từ Adapty và lúc chúng xuất hiện trên Google Analytics Dashboard. Bạn nên theo dõi Realtime Dashboard trên tài khoản Google Analytics của mình để xem các sự kiện mới nhất theo thời gian thực. --- # 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. --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="note"> Nếu bạn dùng ID người dùng của bên thứ ba làm Customer User ID, đừng truyền nó trong `activate()` — SDK của bên thứ ba có thể chưa tạo ID đó. Thay vào đó, hãy gọi `activate()` trước (không có CUID), sau đó gọi `setIntegrationIdentifier()`, rồi mới gọi `identify()` với CUID. </Callout> <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: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="note"> Nếu bạn dùng ID người dùng của bên thứ ba làm Customer User ID, đừng truyền nó trong `activate()` — SDK của bên thứ ba có thể chưa tạo ID đó. Thay vào đó, hãy gọi `activate()` trước (không có CUID), sau đó gọi `setIntegrationIdentifier()`, rồi mới gọi `identify()` với CUID. </Callout> <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 Amazon S3 của Adapty 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 vị trí trung tâm. 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 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 Dashboard. :::note Lịch trình Adapty 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 theo 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ể 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ý 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ả | |---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **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." --- Bậ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 duy nhất: bucket Google Cloud Storage của bạn. Mỗi ngày lúc 4 giờ sáng UTC, Adapty sẽ tải lên các file .csv chứa dữ liệu của ngày hôm trước vào bucket của bạn. Bạn có thể chọn nhận dữ liệu **sự kiện**, dữ liệu **lượt xem paywall**, 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, [tạo khóa truy cập bucket](#create-google-cloud-storage-credentials) trong Google Cloud Console, sau đó [thêm vào cài đặt Adapty của bạn](#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ộ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, "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\} Các 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ề những sự kiện cụ thể nào. | Trường | Kiểu | Mô tả | |:------------------------------|:--------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **ab_test_name** | String | Tên của [A/B test Adapty](ab-tests) nơi giao dịch phát sinh. | | **ab_test_revision** | Integer | Phiên bản của A/B test nơi giao dịch phát sinh. | | **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. | | **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 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) nơi giao dịch phát sinh. | | **environment** | String | Giá trị có thể là `Sandbox` hoặc `Production`. | | **event_datetime** | ISO 8601 date | Ngày và giờ của sự kiện. Giống với giá trị ở cấp gốc của sự kiện. | | **original_purchase_date** | ISO 8601 date | Đối với 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 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, `original_transaction_id` trùng với store_transaction_id.</p> | | **paywall_name** | String | Tên của paywall nơi giao dịch phát sinh. | | **paywall_revision** | String | Phiên bản của paywall nơi giao dịch phát sinh. Giá trị mặc định là 1. | | **price_local** | Float | Giá sản phẩm trước khi Apple/Google khấu trừ, tính bằng đơn vị tiền tệ địa phương. | | **price_usd** | Float | Giá sản phẩm trước khi Apple/Google khấu trừ, tính bằng USD. | | **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ừ các khoản hoàn tiền. | | **promotional_offer_id** | String | ID Adapty của [ưu đãi](offers) đã được áp 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 áp dụng mức hoa hồng giảm (thường là 15%) sau một năm gia hạn liên tục. Mức hoa hồng thay đổi tùy theo điều kiện tham gia 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. 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 server-side API, giá trị từ tham số **store** sẽ được sử dụng. | | **store_country** | String | Quốc gia được app store gửi cho chúng tôi. | | **store_offer_category** | String | Danh mục ưu đãi được áp dụng. Giá trị có thể là `introductory`, `promotional`, `winback`. | | **store_offer_discount_type** | String | Loại ưu đãi được áp dụng. Giá trị có thể là `free_trial`, `pay_as_you_go` và `pay_up_front`. | | **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 cho một 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 trong 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 thực từ cửa hàng, `vendor_product_id` sẽ là một trong các giá trị sau:</p><ul><li>`adapty_server_side_product` — được cấp qua [server-side API](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` — legacy.</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, "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: "Manage Adapty with AI agents and coding tools" description: "Every way to use Adapty with AI — integrate the SDK with a coding agent, pull analytics with an LLM, and feed Adapty docs to your AI tool." --- Adapty works with AI coding tools and agents. Use them to integrate the SDK, ask about your analytics, or look up Adapty docs without leaving your editor. This page lists what's available and who each tool is for. ## Integrate the Adapty SDK with AI Two ways to add the Adapty SDK to your app with an AI coding tool. Both work with Cursor, Claude, and other AI assistants. ### Skill-based integration The Adapty SDK integration skill runs the whole integration from your AI coding tool in one command. Use it when you want a guided, automated setup. Choose your platform: [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) ### Step-by-step integration Walk your AI tool through the integration stage by stage, feeding it the right docs in order. Use it when you want to review each step as you go. Choose your platform: [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) ## Manage Adapty from the command line The [Adapty Developer CLI](developer-cli-quickstart) lets you manage your Adapty entities — apps, access levels, products, paywalls, and placements — from the terminal, without opening the dashboard. Because it's a command-line tool, your AI coding agent can run it directly. ## Ask about your data Point an AI coding agent at the Export Analytics API to query your metrics in plain language — revenue, retention, LTV, and more. No MCP server required. [Ask AI about your analytics data](export-analytics-with-ai) ## Give your AI tool the Adapty docs ### Plain-text docs Every Adapty doc is available as Markdown — add `.md` to the page URL, or click **Copy for LLM** under the title. For broader context, give your tool the [`llms.txt`](https://adapty.io/docs/llms.txt) index or a platform-specific subset such as [`ios-llms.txt`](https://adapty.io/docs/ios-llms.txt). ### Context7 [Context7](https://context7.com/adaptyteam/adapty-docs) is an MCP server that serves Adapty docs to your AI tool, but it indexes only code snippets — not the full prose. Use it for quick code examples; for complete guidance, give your tool the plain-text docs above. Context7 works with Cursor, Claude Code, Windsurf, and other MCP-compatible tools. --- # 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ử. --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> :::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. --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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). --- no_index: true --- **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 analytics cơ bản và cho các tích hợp SplitMetrics Acquire và Asapty. [Apple Ads Manager](adapty-ads-manager) sử dụng kết nối riêng biệt. Kết nối tài khoản Apple Ads của bạn trong [cài đặt Apple 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ố theo 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 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 AdServices framework \{#connect-adapty-to-the-adservices-framework\} Apple Ads qua [AdServices](https://developer.apple.com/documentation/adservices) yêu cầu một số cấu hình trong Adapty Dashboard, đồng thời 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 bằng AdServices framework qua Adapty, làm theo các bước sau: #### Bước 1: Lấy public key \{#step-1-obtain-public-key\} Trong Adapty Dashboard, truy cập [Settings -> Apple Ads.](https://app.adapty.io/settings/apple-search-ads) Tìm public key đã được tạo sẵn (Adapty cung cấp cặp khóa 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 dùng dịch vụ khác hoặc giải pháp riêng cho attribution Apple Ads, bạn có thể tải lên private key của mình. ::: #### 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, 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 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 riêng 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. Vào Settings -> API trong giao diện Apple Ads. Dán public key đã sao chép 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 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à 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 Lưu ý rằng việc tắt tính năng này sẽ hoàn toàn dừng việc nhận analytics 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 phụ thuộc vào attribution ASA để hoạt động đúng. Dữ liệu attribution nhận được trước 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 thành viên trên Adapty Dashboard: | Vai trò | Quyền truy cập Billing | Thêm thành viên mới | Thay đổi bất cứ điều gì | Quyền truy cập tất cả phần | |-------------|------------------------|---------------------|-------------------------|----------------------------| | 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 quyền truy cập cũng như kiểm soát cao nhất. Owner có toàn quyền truy cập vào phần billing 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 có 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 có 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 tài khoản. Họ không thể truy cập bất kỳ cài đặt billing 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 có vai trò Viewer có quyền truy cập chỉ đọc vào 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 có 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 hoặc truy cập các phần khác của Adapty. Vai trò này đặc biệt phù hợp với đội ngũ hỗ trợ hoặc những cá nhân cần hỗ trợ khách hàng với các vấn đề liên quan đến gói đăng ký hoặc khắc phục sự cố. - **ASA manager:** Thành viên có vai trò ASA manager chỉ có quyền truy cập vào dashboard [Apple 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 trong 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ợ Adapty để xóa tài khoản hiện tại của họ. ::: Để thêm 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ấ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 vào 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\} --- no_index: true --- 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\} --- no_index: true --- 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 kiểm duyệt trên store" description: "Lời khuyên để ứng dụng của bạn được phê duyệt trên App Store và Google Play Store" --- Bài viết này mô tả quy trình các store thực hiện khi kiểm duyệt ứng dụng, đồng thời cung cấp các mẹo để được phê duyệt nhanh hơn. Nội dung được tổng hợp từ các hướng dẫn nộp ứng dụng chính thức: * [Hướng dẫn nộp ứng dụng lên App Store](https://developer.apple.com/app-store/review/guidelines/) * [Hướng dẫn nộp ứng dụng lên Google Play Store](https://play.google/developer-content-policy/) :::important Cả hai store đều có quy trình kiểm duyệt tương tự nhau. Khi một chính sách chỉ áp dụng cho một trong hai store, bài viết sẽ đề cập rõ tên store đó. ::: 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 dẫn đến việc 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 quy trình kiểm duyệt \{#review-process-overview\} #### Bước 1: Kiểm tra tự động Cả App Store và Google Play Store đều có quy trình kiểm duyệt hai bước tương tự nhau. Ngay sau khi nộp, ứng dụng của bạn sẽ trải qua một lần quét tự động, có thể mất đến vài giờ. Cả hai store đều quét ứng dụng để phát hiện malware, trong đó Google đặc biệt chú trọng đến bước này. Google tìm kiếm các dấu hiệu hành vi độc hại như kết nối với máy chủ đáng ngờ và truy cập dữ liệu người dùng không chính đáng. Nếu ứng dụng 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. [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 store cũng xác minh sự hiện diện của metadata cần thiết, không có các dependency gây hại hoặc quá lỗi thời, và tính toàn vẹn của bản build. #### Bước 2: Kiểm duyệt thủ công Sau khi ứng dụng vượt qua kiểm tra tự động, nó sẽ được một người kiểm duyệt xem xét. 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 kiểm duyệt hiện tại. Các ứng dụng xử lý dữ liệu nhạy cảm thường mất nhiều thời gian hơn để kiểm duyệ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 hoàn chỉnh \{#completeness\} Cả Apple và Google đều đặt ra yêu cầu *hoàn chỉnh* ("chức năng tối thiểu") đối với nội dung trên store. * Các màn hình placeholder, màn hình "sắp ra mắt", và các tính năng bị lỗi đều dẫn đến việc bị từ chối với ứng dụng iOS. * 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 trong [Early Access](https://knowledge.workspace.google.com/admin/users/access/turn-early-access-apps-on-or-off-for-users). * Cả hai store đều **từ chối** các ứ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 thuộc danh mục tương tự. * 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 về paywall \{#paywall-requirements\} Người kiểm duyệt ứng dụng kỳ vọng các paywall đơn giản, dễ hiểu. Nếu bạn 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 tìm thấy bằng chứng về các hành vi lừa đảo, tài khoản của bạn có thể bị vô hiệu hóa và ứng dụng bị [đình chỉ](https://support.google.com/googleplay/android-developer/community-guide/287283557/app-suspended-for-repeated-rejections?hl=en). Google Play sử dụng [hệ thống cảnh cáo](https://support.google.com/googleplay/android-developer/answer/9899234?hl=en) có thể dẫn đến việc xóa tất cả ứng dụng của bạn. Hãy tuân thủ các nguyên tắc sau trong thiết kế paywall: - **Minh bạch và rõ ràng.** Hiển thị giá chính xác, tần suất thanh toán, lợi ích, và điều kiện hủy của sản phẩm trước khi nhắc 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 đi kèm với bản dùng thử miễn phí, hãy nêu rõ thời hạn và điều kiện của nó. Không sử dụng ngôn ngữ cố tình 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 danh sách 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ự chênh lệch giá nào, dù nhỏ, đều là lý do để 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. Nếu paywall của bạn được code thủ công, bạn phải [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 bình đẳng.** Không chọn trước tùy chọn đắt tiền nhất, cũng không ẩn các tùy 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 buộc 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 vào các sản phẩm đã mua của người dùng. * **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ễ nhìn thấy được. Các trạng thái trung gian của quá trình xác nhận thanh toán không được gây lỗi hoặc 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 nội dung họ đã trả tiền. * **Khôi phục quyền truy cập** Người dùng phải có thể khôi phục quyền truy cập vào sản phẩm từ một thiết bị mới. Đặt nút khôi phục ở vị trí dễ thấy. Nếu bạn tạo paywall bằng [Paywall Builder](adapty-paywall-builder), nút khôi phục 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](observer-vs-full-mode). Ứng dụng phải 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 store, hoặc từ nơi khác trong app store. ### Phương thức thanh toán phù hợp \{#appropriate-payment-methods\} Cả hai store đều cấm bán hàng vật lý bằng in-app purchase và yêu cầu thanh toán qua store cho hầu hết hàng hóa kỹ thuật số. Yêu cầu thanh toán qua store không áp dụng ở một số vùng lãnh thổ, bao gồm Mỹ và EU. Tùy thuộc vào quốc gia, bạn có thể [bỏ qua hoàn toàn việc thanh toán qua store](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à thanh toán thay thế. Một số danh mục ứng dụng (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 store để biết 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 cụ thể các quốc gia cho phép phương thức thanh toán thay thế. Khi các vùng lãnh thổ mới thông qua các luật tương tự, khả năng này 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 store đều thực thi hướng dẫn về tích hợp nhà cung cấp thanh toán và tiếp tục thu hoa hồng cho các giao dịch sử dụng các 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 kiểm duyệt sẽ nêu rõ (các) hướng dẫn nào đã bị vi phạm. Hãy đọc toàn bộ hướng dẫn đó và khắc phục: * [Hướng dẫn nộp ứng dụng lên App Store](https://developer.apple.com/app-store/review/guidelines/) * [Hướng dẫn nộp ứ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 kháng cáo. Hãy cung cấp bằng chứng tuân thủ và liên hệ với store. * Không cập nhật ứng dụng trong khi đang được kiểm duyệt. * Mỗi lần bạn nộp ứng dụng để kiểm duyệt, bạn có thể gặp một người kiểm duyệt 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 lỗi một. Hãy nộp lại ứng dụng để kiểm duyệt khi tất cả các sửa chữa đã 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ả khi chúng đang bị tạm dừng hoặc không hoạt động. * Các lần kiểm duyệt tiếp theo thường mất ít thời gian hơn lần đầu. * Có thể có kiểm duyệt nhanh cho các lỗi nghiêm trọng và các deadline — hãy sử dụng thận trọng. ## Sau khi kiểm duyệt: giám sát liên tục \{#after-the-review-continuous-monitoring\} Cả hai app store tiếp tục giám sát ứng dụng của bạn ngay cả sau khi nó vượt qua quá trình kiểm 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), nó sẽ bị gắn cờ và gỡ khỏi danh sách. Lượng lớn phản hồi tiêu cực từ người dùng cũng là lý do để bị kiểm tra thêm. Trong giai đoạn 2024–2025, Google đã [xóa 47% ứng dụng khỏi Play Store](https://techcrunch.com/2025/04/29/google-play-sees-47-decline-in-apps-since-start-of-last-year/) để nâng cao chất lượng trung bình. Bỏ bê ứng dụng cũng có 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 nhận được cập nhật hoặc lượt tải xuống. ## 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 với 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 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, bạn cần thêm vào. Nhấp **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ó. 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 sẽ cần 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 sẽ cần: - Trang doanh nghiệp – thêm vào danh mục doanh nghiệp 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 danh mục doanh nghiệp - Email doanh nghiệp – thiết lập 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 danh mục doanh nghiệp tại [**Settings > Accounts > Ad accounts**](https://business.facebook.com/latest/settings/ad_accounts) Khi đã sẵn sàng, tạo một pixel: 1. Truy cập [**Events Manager**](https://www.facebook.com/events_manager2). Nhấp **Connect data**. 2. Chọn **Web** là loại nguồn dữ liệu. 3. Đặt tên cho dataset và nhấp **Create**. 4. Với [Adapty User Acquisition](adapty-user-acquisition), bạn không cần hoàn thành toàn bộ quá trình cài đặt pixel. Vì vậy, khi được hỏi về tích hợp, bạn có thể 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 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). Trên tab **Campaign**, nhấp **Create**. 2. Chọn **Sales** làm mục tiêu chiến dịch và nhấp **Continue**. 3. Đặt tên chiến dịch 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 chắn về cách thức nào 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 để phân bổ thêm ngân sách. Sau đó, chọn ngân sách **Daily** (hàng ngày) hay **Lifetime** (trọn đời) và nhập giới hạn bằng tiền tệ của bạn. Ngân sách **Daily** cho phép bạn linh hoạt hơn trong khi vẫn đang học hỏi, 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. Hoặc, bạn có thể chọn **Schedule budget increase** để đặt quy tắc tự động tăng ngân sách theo giá trị hoặc phần trăm. - **Ad set budget**: Chọn tùy chọn này nếu bạn muốn xác định thủ công đối tượng nào sẽ nhận được 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% nếu điều đó mang lại hiệu quả tốt hơn. 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)**: Lựa chọn dễ nhất để bắt đầu. Nếu chọn tùy chọn này, bạn để Meta tối ưu hóa chi phí nhấp chuột để đạt kết quả tốt nhất trong ngân sách. - **Cost per result goal**: Nhắm đến một mức chi phí nhất định cho 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 chi phí cao nhất bạn sẵn sàng đặt giá thầu. 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 của bạn. Nhấp **Next** để tiếp tục. ## Bước 4. Tạo nhóm quảng cáo \{#step-4-create-ad-set\} Để tạo nhóm quảng cáo: 1. Đặt tên nhóm quảng cáo trong trường **Ad set name**. 2. Trong 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ó trang đích hoặc **Maximize number of link clicks** nếu bạn dùng smart link dẫn người dùng thẳng đế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 **Conversion event**. Trong trường hợp của chúng ta, đó có thể là **Purchase** hoặc **Start trial**. Đừng lo nếu bạn thấy cảnh báo rằng dataset chưa có sự kiện nào — điều đó chỉ có nghĩa là dataset của bạn còn 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 ngân sách **Daily** hay **Lifetime** và nhập giới hạn bằng tiền tệ của bạn. Ngân sách **Daily** cho phép bạn linh hoạt hơn trong khi vẫn đang học hỏi, 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. Đặt ngày bắt đầu và, nếu có, ngày kết thúc cho nhóm quảng cáo. Ví dụ, nếu bạn muốn quảng cáo một ưu đãi trong ứng dụng, điều quan trọng là phải đồng bộ thời gian của nhóm quảng cáo với thời gian của ưu đãi. 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 nhóm quảng cáo để phù hợp với đặc thù của từng khu vực trong quảng cáo. - **Minimum age**: Chọn độ tuổi tối thiểu của người dùng sẽ thấy quảng cáo của bạn. Với một số quảng cáo, điều này có thể được yêu cầu theo luật. Bạn không thể chọn độ tuổi tối thiểu dưới 18 trên toàn cầu hoặc 20 tại Thái Lan. - **Language**: Chỉ đặt **Language** nếu đó không phải ngôn ngữ phổ biến nhất ở các quốc gia được chọn. Ví dụ, bạn không cần chọn **English** ở Hoa Kỳ, nhưng nếu bạn nhắm đến người nói tiếng Tây Ban Nha 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 mà quảng cáo của bạn có liên quan. Tuy nhiên, nếu bạn thêm gợi ý đối tượng, bạn có thể hướng dẫn Meta đến những người bạn nghĩ 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 phù hợp hơn với các đặc thù của 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 một cách cụ thể nhất. Tại đây, bạn có thể tạo nhóm dựa trên **Demographics** (nhân khẩu học), **Interests** (sở thích) hoặc **Behaviors** (hành vi). Tùy thuộc vào chức 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ủa các ban nhạc cụ thể, cha mẹ của trẻ sơ sinh, hoặc những người thường mua sắm trực tuyến nhiều. :::note Các 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**, nhấp **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 sẽ xuất hiện. Theo mặc định, cài đặt **Advantage+** được chọn, để Meta phân bổ ngân sách của nhóm 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 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 hóa chi tiêu. Trong phần **Placements**, nhấp **Show more settings**. Trong phần 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 của bạn chỉ hiển thị cho người dùng phù hợp. Ví dụ, người dùng máy tính để bàn sẽ không thấy quảng cáo của bạn, 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 **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 quảng cáo 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 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 bằng 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 sẽ chuyển hướng người dùng đến ứng dụng thay vì trang Facebook. Trong trường **Format**, chọn một tùy chọn tùy thuộc vào số lượng mẫu sáng tạo 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à **Main destination**. Trong trường **Website URL**, dán `https://api-ua.adapty.io/api/v1/attribution/click`. Trong [Adapty User Acquisition](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 các tệp phương tiện, cắt xén chúng 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 — nó sẽ tự động lấy văn bản từ mẫu sáng tạo 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** để chạy 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 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 và nhấn **Create**. 7. Với [Adapty User Acquisition](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, bạn có thể đóng cửa sổ thiết lập và pixel sẽ xuất hiện trong danh sách. 8. Để pixel này khả dụng trong các chiến dịch, bạn cần gửi một sự kiện thử nghiệm đến nó từ [Adapty User Acquisition](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 nền tảng – ví dụ: iOS. 3. Chọn một pixel từ danh sách thả xuống. 4. Nhấn **Send test event**. 5. Trong danh sách thả xuống, chọn sự kiện bạn sẽ sử dụng để tối ưu hóa trong 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. Sự kiện thử nghiệm sẽ xuất hiện trong TikTok sau vài phút. Khi bạn thấy nó trong 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 **Optimization event**. Lưu ý rằng chỉ những sự kiện đang hoạt động mới có thể chọn. Nếu sự kiện bạn cần không khả dụng, hãy gửi sự kiện thử nghiệm theo hướng dẫn ở [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 của TikTok. Để thiết lập thêm, nhấn **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ấn **Continue**. ## Bước 5. Thêm nội dung quảng cáo \{#step-5-add-ad-content\} Bây giờ là lúc thiết lập creative và URL đích: 1. Trong trường **TikTok account**, chọn tài khoản sẽ được dùng để đăng bài. 2. Trong [Adapty User Acquisition](adapty-user-acquisition), [tạo một web campaign](ua-tiktok) và dán **Click link** vào trường **Destination URL**. 3. Trong phần **Creatives**, nhấn **+ Videos and images**. 4. Nếu bạn muốn sử dụng các bài đăng TikTok làm creative, hãy chọn chúng trên tab **TikTok post**. Nếu không, chuyển sang tab **Creative library** và nhấn **Upload**. Các tệp bạn tải lên đó sẽ có thể truy cập từ tab này sau này, để bạn có thể tái sử dụng chúng trong các chiến dịch khác. 5. Cắt các creative để 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 hay carousel. 6. Mở rộng creative đã tải lên và nhấn **+** 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 văn bản 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 trên tài khoản TikTok của mình. 9. Trong trường **Call to action**, chọn hoặc bỏ những 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. 10. Nhấn **Continue**. ## Bước 6. Cấu hình đối tượng 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 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** phù hợp nhất khi bạn chưa hiểu rõ đối tượng của mình. Tuy nhiên, nếu bạn 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 mục tiêu, kết quả xét duyệt quảng cáo sẽ được trả về riêng biệt cho từng vị trí. Việc phân phối quảng cáo thực tế cũng có thể thay đổi 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. Chọn ngôn ngữ 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** 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 phép linh hoạt hơn khi bạn đang trong giai đoạn tìm hiểu, giúp bạn bắt đầu với số tiền nhỏ hơn và dần điều chỉnh theo thời gian. 5. Trong phần **Schedule**, chọn **Continue for at least 7 days** hoặc **Custom**. Chúng tôi khuyến nghị bạn đặt lịch **Custom** nếu quảng cáo của bạn có tính thời điểm, để bạ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**, đặ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ấn **Publish**. Khi hoàn tất, một chiến dịch mới sẽ được tạo 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 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 The no-code onboarding builder is fully functional, but Adapty is no longer adding features or shipping updates for it. For new projects, consider the [Adapty Flow Builder](adapty-flow-builder) — a visual no-code editor for single-screen paywalls and multi-screen onboarding flows that render natively on device: - **Any flow type**: Build single-screen paywalls, multi-step onboardings that include a paywall, and anything in between. - **Native rendering**: Flows render through the Adapty SDK, without web views. - **Update without redeploying**: Change copy, design, or logic any time, and updates reach users without an app release. ::: Adapty's onboardings let non-technical teams build onboarding flows without code. The no-code builder creates a series of screens that introduce users to your app. You can personalize screens with interactive questions and variables, then run A/B tests to find the best-performing flow. Onboardings are available for apps using Adapty SDK v3.8.0+ (iOS, Android, React Native, Flutter), v3.14.0+ (Unity), or v3.15.0+ (Kotlin Multiplatform, Capacitor). ## How it works 1. [Design an onboarding in the no-code editor.](design-onboarding) 2. [Create a placement for the onboarding.](create-onboarding#step-2-create-a-placement-for-your-onboarding) 3. Integrate the onboarding with your project using the 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. Test the onboarding and release it for your users. --- # 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-06-24T14:36:38.987Z_ _Successfully processed: 265/265 files_ # UNITY - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-06-24T14:36:38.996Z 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ý." --- Adapty SDK 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, bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng. - **AdaptyUI**: Module này cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder) — công cụ no-code thân thiện giúp tạo paywall đa nền tảng dễ dàng. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng 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 trở lên, nhưng yêu cầu iOS 15.0 trở lên để 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. 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 bản build Android của bạn. ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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. </Callout> ## 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) 1. Tải file [`adapty-unity-plugin-*.unitypackage`](https://github.com/adaptyteam/AdaptySDK-Unity/tree/main/Releases) từ GitHub và import vào project 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 */ }} /> 2. Tải và import [plugin External Dependency Manager](https://github.com/googlesamples/unity-jar-resolver). 3. SDK sử dụng plugin "External Dependency Manager" để quản lý các dependency iOS Cocoapods và Android gradle. Sau khi cài đặt, 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` 4. Khi build project Unity cho iOS, bạn sẽ nhận được file `Unity-iPhone.xcworkspace`. Bạn phải mở file này 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 ứng dụng của bạn. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng. ::: Để lấy **Public SDK Key**: 1. 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 `"YOUR_PUBLIC_SDK_KEY"` trong code. :::important - Đảm bảo bạn dùng **Public SDK key** để khởi tạo Adapty, còn **Secret key** chỉ 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 chắc chắn 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 trình tự đầy đủ. ::: ## 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 dùng phương thức `DontDestroyOnLoad` cho object này để đảm bảo nó tồn tại trong 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 sự kiện 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 bạn nên đ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 */ }} /> ## Thêm Kotlin Plugin vào project của bạn \{#add-kotlin-plugin-to-your-project\} :::warning Bước này là bắt buộc. Nếu bỏ qua, ứng dụng di động của bạn có thể bị crash khi hiển thị paywall. ::: 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** } ``` Bây giờ hãy thiết lập paywall trong ứng dụng của bạn: - Nếu bạn dùng [Adapty Paywall Builder](adapty-paywall-builder), trước tiên hãy [kích hoạt module AdaptyUI](#activate-adaptyui-module-of-adapty-sdk) bên dưới, sau đó làm theo [hướng dẫn nhanh Paywall Builder](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); ``` ## 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 log các lỗi và thông tin quan trọng khác để giúp bạn hiểu những gì đang xảy ra. Có các mức độ sau: | Mức độ | Mô tả | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `error` | Chỉ ghi log các lỗi | | `warn` | Ghi log các lỗi và các thông điệp từ SDK không gây ra lỗi nghiêm trọng nhưng đáng chú ý | | `info` | Ghi log các lỗi, cảnh báo và các thông điệp thông tin | | `verbose` | Ghi log mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như các lời gọi hàm, truy vấn API, v.v. | Bạn có thể đặt 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 tại runtime: ```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 rõ ràng, nhưng bạn có thể triển khai 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 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 `SetIPAddressCollectionDisabled` 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ư 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 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. ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetIPAddressCollectionDisabled(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 `SetAppleIDFACollectionDisabled` và/hoặc `SetGoogleAdvertisingIdCollectionDisabled` thành `true` để tắt thu thập các định danh quảng cáo. Giá trị mặc định là `false`. Dùng tham số này để tuân thủ chính sách 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 yêu cầu attribution quảng cáo hoặc analytics dựa trên advertising ID. ```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 cache media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Mặc định, AdaptyUI lưu cache media (chẳng hạn hình ảnh và video) để cải thiện hiệu suất và giảm sử dụng mạng. Bạn có thể tùy chỉnh cài đặt cache bằng cách cung cấp cấu hình tùy chỉnh. Dùng `SetAdaptyUIMediaCache` để ghi đè 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ố: | 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 item 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 `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 từ 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ừ backup iCloud và xóa tất cả dữ liệu SDK được lưu 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 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 server Adapty vẫn không thay đổi. ::: ```csharp showLineNumbers title="C#" var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetAppleClearDataOnBackup(true); ``` ## Khắc phục sự cố \{#troubleshooting\} #### Quy tắc backup Android (cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `<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, hãy á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 backup trong `Assets/Plugins/Android/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ái tạo hoặc tái sử dụng nó không chính xác 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ý là đã hủy. Để đảm bảo mua hàng hoạt động đúng, chỉ sử dụng chế độ `standard` hoặc `singleTop` cho Activity khởi động flow mua hàng, và tránh các chế độ khác. Trong `AndroidManifest.xml` của bạn, 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: 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <Callout type="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 đỡ! </Callout> 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\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; <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\} <Callout type="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. </Callout> 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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: 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\} --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. <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-06-24T14:36:39.018Z_ _Successfully processed: 41/41 files_