:::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
```
---
# 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.
:::
### 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.
**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.
:::
**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.
:::
**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.
:::
### 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
:::tip
To get started with the Adapty Paywall Builder paywalls quickly, see our [quickstart guide](kmp-quickstart-paywalls).
:::
### Implement paywalls manually
For more guides on implementing paywalls and handling purchases manually, see the [category](kmp-implement-paywalls-manually).
## Useful features
---
# 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.
:::
Trước khi bắt đầu hiển thị paywall trong ứng dụng di động (nhấp để mở rộng)
1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard.
2. [Tạo paywall và 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.
## 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** | tùy chọn
mặc định: `en`
| Đị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.
Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha ở Brazil.
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.
|
| **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | 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.
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.
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.
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ế.
|
| **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 nhiều yêu cầu khác nhau bên dưới.
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`.
|
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** | tùy chọn
mặc định: `en`
| Định danh của [bản dịch paywall](add-remote-config-locale). Tham số này đượ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.
Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha ở Brazil.
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.
|
| **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | 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.
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.
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.
|
## 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 = 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:
```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()
)
```
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.
```
### 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 (``).
:::
Để sử dụng custom tags trong paywall, hãy truyền chúng khi tạo paywall view:
```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
}
}
```
```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,
)
```
## 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:
```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
}
}
```
```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,
)
```
---
# 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.
:::
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
// Paywall appeared
{
// No additional data
}
// Paywall disappeared
{
// No additional data
}
```
### 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
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
{
"productId": "premium_monthly"
}
```
### Bắt đầu mua \{#started-purchase\}
Nếu người dùng khởi tạo quá trình mua, phương thức này sẽ được gọi:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) {
// Handle purchase start
// You can show loading indicators or track analytics here
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
```
### Mua 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
}
}
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
// Successful purchase
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"purchaseResult": {
"type": "Success",
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
}
}
}
}
// 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"
}
}
```
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
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": {
"code": "purchase_failed",
"message": "Purchase failed due to insufficient funds",
"details": {
"underlyingError": "Insufficient funds in account"
}
}
}
```
### Bắt đầu khôi phục \{#started-restore\}
Nếu người dùng khởi tạo quá trình khôi phục, phương thức này sẽ được gọi:
```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()
}
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
{
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
},
"subscriptions": [
{
"vendorProductId": "premium_monthly",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
]
}
}
```
Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` 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
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
{
"error": {
"code": "restore_failed",
"message": "Purchase restoration failed",
"details": {
"underlyingError": "No previous purchases found"
}
}
}
```
### 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
}
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```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"
}
}
}
```
## 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
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
{
"error": {
"code": "products_loading_failed",
"message": "Failed to load products from the server",
"details": {
"underlyingError": "Network timeout"
}
}
}
```
### Lỗi hiển thị \{#rendering-errors\}
Nếu xảy ra lỗi trong quá trình hiển thị giao diện, lỗi đó sẽ được báo cáo bở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
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
{
"error": {
"code": "rendering_failed",
"message": "Failed to render paywall interface",
"details": {
"underlyingError": "Invalid paywall configuration"
}
}
}
```
Trong điều kiện bình thường, các lỗi như vậy không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết.
---
# File: 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).
Đườ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)
*/
en
/*
composeResources/values-es/strings.xml (Spanish)
*/
es
/*
composeResources/values-pt-rBR/strings.xml (Portuguese — Brazil)
*/
pt-br
// 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`.
## 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).
:::
## 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).
:::
---
# 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';
Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ!
Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra giao dịch 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.
:::
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)
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.
## 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** | tùy chọn
mặc định: `en`
| Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này được kỳ vọng là mã 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.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
|
| **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | 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.
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.
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ữ 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.
|
| **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 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.
|
Đừ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:
• `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`.
• `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.
• `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.
• `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ả.
• `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** | tùy chọn
mặc định: `en`
| Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này được kỳ vọng là mã 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.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
|
| **fetchPolicy** | mặc định: `AdaptyPaywallFetchPolicy.Default` | 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.
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.
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.
|
---
# 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** | 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.
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.
|
:::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';
Về offer code
Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng.
Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code:
- **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ.
- **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ.
- **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ.
Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds).
#### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\}
Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect:
- Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code.
- Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm.
Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**.
#### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\}
Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect.
Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect.
Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác.
Để hiển thị 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** | 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.
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-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 `` đượ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: `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 | Đố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.
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ể.
|
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 `` 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."
---
---
# 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** | 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ộ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.Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
|
| **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.
Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp 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.
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.
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.
|
| **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 này có thể bao gồm nhiều yêu cầu khác nhau bên dưới.
|
Tham số phản hồi:
| Tham số | Mô tả |
|:----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 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** | 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ộ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.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil. |
| **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` | Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.
Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp 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.
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.
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.
|
---
# 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:
```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()
)
```
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.
```
### 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.
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())
```
Ví dụ sự kiện (Nhấn để mở rộng)
```json
{
"actionId": "allowNotifications",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
}
}
```
## Đóng onboarding \{#closing-onboarding\}
Onboarding được coi là đã đóng khi người dùng nhấn vào một nút được 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())
```
Ví dụ sự kiện (Nhấn để mở rộng)
```json
{
"action_id": "close_button",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "final_screen",
"screen_index": 3,
"total_screens": 4
}
}
```
## Mở paywall \{#opening-a-paywall\}
:::tip
Xử lý sự kiện này để mở paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi onboarding đóng 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())
```
Ví dụ sự kiện (Nhấn để mở rộng)
```json
{
"action_id": "premium_offer_1",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "pricing_screen",
"screen_index": 2,
"total_screens": 4
}
}
```
## Hoàn tất tải onboarding \{#finishing-loading-onboarding\}
Khi onboarding hoàn tất 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())
```
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 `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())
```
Ví dụ các sự kiện (Nhấn để mở rộng)
```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
}
}
```
---
# 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())
```
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)
```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
}
}
}
```
## 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."
---
---
# 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."
---
---
# 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:
```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}")
}
}
}
```
```java showLineNumbers
Adapty.getProfile(result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyProfile profile = ((AdaptyResult.Success) result).getValue();
// Handle success
} else if (result instanceof AdaptyResult.Error) {
AdaptyError error = ((AdaptyResult.Error) result).getError();
// Handle error
Log.e("Adapty", "Error: " + error.getMessage());
}
});
```
## 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
```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)
}
}
}
}
}
```
```java showLineNumbers
Adapty.getPaywall("main", result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyPaywall paywall = ((AdaptyResult.Success) 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;
}
}
});
```
### Purchase errors
```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)
}
}
}
}
}
```
```java showLineNumbers
product.makePurchase(result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyPurchase purchase = ((AdaptyResult.Success) 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;
}
}
});
```
## Error recovery strategies
### Retry on network errors
```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()
}
```
```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) 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();
}
```
### Fallback to cached data
```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)
}
}
}
}
}
}
```
```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) 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());
}
}
});
}
}
```
## 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**.
3. Mở tab [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) từ menu trên cùng của Adapty và dán giá trị vừa sao chép vào trường **Bundle ID**.
4. Quay lại trang **App information** trong App Store Connect và sao chép **Apple ID** tại đó.
5. Trên trang [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) trong Adapty dashboard, dán ID vào trường **Apple app ID**.
## Bước 2. Kiểm tra sản phẩm \{#step-3-check-products\}
1. Truy cập **App Store Connect** và điều hướng đến [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) trong menu bên trái.
2. Nhấp vào tên nhóm gói đăng ký. Bạn sẽ thấy 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).
4. So sánh ID sản phẩm trong bảng với ID trong tab [**Products**](https://app.adapty.io/products) trên Adapty Dashboard. Nếu các ID không khớp, hãy sao chép ID sản phẩm từ bảng và [tạo sản phẩm](create-product) với ID đó trong Adapty Dashboard.
## Bước 3. Kiểm tra tình 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ự.
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.
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ê.
## 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**.
2. Nhấp vào tên nhóm gói đăng ký.
3. Chọn sản phẩm bạn đang kiểm tra.
4. Cuộn xuống **Subscription Pricing** và mở rộng mục **Current Pricing for New Subscribers**.
5. Đảm bảo tất cả các mức giá cần thiết đều được liệt kê.
## Bước 5. Kiểm tra trạng thái thanh toán 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**.
2. Chọn tên công ty của bạn.
3. Cuộn xuống và kiểm tra rằng **Paid Apps Agreement**, **Bank Account** và **Tax forms** của bạn đều hiển thị trạng thái **Active**.
Bằng cách làm theo các bước trên, bạn sẽ có thể khắc phục cảnh báo `InvalidProductIdentifiers` và đưa sản phẩm 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-07-01T16:30:13.725Z_
_Successfully processed: 50/50 files_