Định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này phải là mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân tách bởi dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ phụ thứ hai là vùng.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
Xem [Bản địa hóa và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.
|
| **params** | tùy chọn | Tham số bổ sung để lấy paywall. |
**Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt — nếu hôm nay paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả chúng mà không cần thay đổi code.
Tham số phản hồi:
| Tham số | Mô tả |
| :-------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Paywall | Một đối tượng [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) chứa danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. |
## Lấy cấu hình view của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\}
:::important
Hãy đảm bảo bật toggle **Show on device** trong paywall builder. Nếu tùy chọn này chưa được bật, cấu hình view sẽ không thể lấy được.
:::
Sau khi lấy paywall, hãy kiểm tra xem nó có chứa `ViewConfiguration` không — điều này cho biết paywall được tạo bằng Paywall Builder. Điều này sẽ hướng dẫn bạn cách hiển thị paywall. Nếu `ViewConfiguration` có mặt, hãy xử lý nó như một Paywall Builder paywall; nếu không, [xử lý nó như một Remote Config paywall](present-remote-config-paywalls-capacitor).
Trong Capacitor SDK, hãy gọi trực tiếp phương thức `createPaywallView` mà không cần lấy cấu hình view thủ công trước.
:::warning
Kết quả của phương thức `createPaywallView` chỉ có thể được sử dụng một lần. Nếu bạn cần dùng lại, hãy gọi phương thức `createPaywallView` một lần nữa.
:::
```typescript showLineNumbers
if (paywall.hasViewConfiguration) {
try {
const view = await createPaywallView(paywall);
} catch (error) {
// handle the error
}
} else {
// use your custom logic
}
```
Tham số:
| Tham số | Bắt buộc | Mô tả |
| :------------------- | :------- | :----------------------------------------------------------- |
| **paywall** | bắt buộc | Một đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. |
| **customTags** | tùy chọn | Định nghĩa một dictionary các custom tag và giá trị đã được xử lý của chúng. Custom tag đóng vai trò là placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để tạo nội dung cá nhân hóa trong paywall. Tham khảo chủ đề Custom tags in paywall builder để biết thêm chi tiết. |
| **prefetchProducts** | tùy chọn | Bật để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Khi là `true`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. Mặc định: `false`. |
:::note
Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](capacitor-localizations-and-locale-codes).
:::
Sau khi có view, [hiển thị paywall](capacitor-present-paywalls).
## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\}
Thông thường, paywall được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, và người dùng có kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong đợi. Trong những tình huống như vậy, bạn có thể muốn hiển thị paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào.
Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như đã mô tả chi tiết trong phần [Lấy thông tin Paywall](#fetch-paywall-designed-with-paywall-builder) ở trên.
:::warning
Tại sao chúng tôi khuyến nghị dùng `getPaywall`
Phương thức `getPaywallForDefaultAudience` có một số nhược điểm đáng kể:
- **Các vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn sẽ phải thiết kế paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với paywall không render được.
- **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của bạn).
Nếu bạn sẵn sàng chấp nhận những nhược điểm này để hưởng lợi từ việc tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` như đã mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder).
:::
```typescript showLineNumbers
try {
const paywall = await adapty.getPaywallForDefaultAudience({
placementId: 'YOUR_PLACEMENT_ID',
locale: 'en',
});
// the requested paywall
} catch (error) {
// handle the error
}
```
:::note
Phương thức `getPaywallForDefaultAudience` khả dụng từ Capacitor SDK phiên bản 2.11.2 trở lên.
:::
| Tham số | Bắt buộc | Mô tả |
|---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard của bạn. |
| **locale** | Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này phải là mã ngôn ngữ gồm một hoặc nhiều thẻ phụ được phân tách bởi dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ phụ thứ hai là vùng.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
Xem [Bản địa hóa và mã locale](capacitor-localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.
|
| **params** | tùy chọn | Tham số bổ sung để lấy paywall. |
## Tùy chỉnh assets \{#customize-assets\}
Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets.
Hình ảnh và video hero có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm mục tiêu các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng.
Đối với các hình ảnh và video khác, bạn cần [đặt custom ID](custom-media) trong Adapty dashboard.
Ví dụ, bạn có thể:
- Hiển thị hình ảnh hoặc video khác nhau cho một số người dùng.
- Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải.
- Hiển thị hình ảnh xem trước trước khi phát video.
:::important
Để sử dụng tính năng này, hãy cập nhật Adapty Capacitor SDK lên phiên bản 3.8.0 trở lên.
:::
Dưới đây là ví dụ về cách bạn có thể cung cấp custom assets thông qua một dictionary đơn giản:
```typescript showLineNumbers
const customAssets: Record = {
'custom_image': { type: 'image', relativeAssetPath: 'custom_image.png' },
'hero_video': {
type: 'video',
fileLocation: {
ios: { fileName: 'custom_video.mp4' },
android: { relativeAssetPath: 'videos/custom_video.mp4' }
}
}
};
view = await createPaywallView(paywall, { customAssets });
```
:::note
Nếu không tìm thấy asset, paywall sẽ tự động chuyển về giao diện mặc định của nó.
:::
---
# File: capacitor-present-paywalls
---
---
title: "Hiển thị paywall Paywall Builder trong Capacitor SDK"
description: "Hiển thị paywall trong ứng dụng Capacitor bằng Adapty."
---
Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Paywall như vậy chứa cả nội dung cần hiển thị lẫn cách thức hiển thị.
:::warning
Hướng dẫn này chỉ dành cho **paywall Paywall Builder**. Quy trình hiển thị paywall khác nhau đối với remote config paywall. Để hiển thị **remote config paywall**, xem [Render paywall được thiết kế bằng remote config](present-remote-config-paywalls).
:::
Để hiển thị một paywall, sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createPaywallView`](capacitor-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo một `view` mới.
:::warning
Tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi.
:::
```typescript showLineNumbers
const view = await createPaywallView(paywall);
view.setEventHandlers({
onUrlPress(url) {
window.open(url, '_blank');
return false;
},
});
try {
await view.present();
} catch (error) {
// handle the error
}
```
## Sử dụng timer do developer định nghĩa \{#use-developer-defined-timer\}
Để sử dụng timer do developer định nghĩa trong ứng dụng di động của bạn, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer do developer định nghĩa mà bạn đã thiết lập trong Adapty dashboard. Điều này đảm bảo ứng dụng của bạn cập nhật timer động với giá trị chính xác — như `13d 09h 03m 34s` (được tính bằng thời gian kết thúc của timer, chẳng hạn như Ngày đầu năm mới, trừ đi thời gian hiện tại).
```typescript showLineNumbers
const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) };
const view = await createPaywallView(paywall, { customTimers });
```
Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của timer do developer định nghĩa mà bạn đã thiết lập trong Adapty dashboard. Timer này đảm bảo ứng dụng của bạn cập nhật động với giá trị chính xác — như `13d 09h 03m 34s` (được tính bằng thời gian kết thúc của timer, chẳng hạn như Ngày đầu năm mới, trừ đi thời gian hiện tại).
## Hiển thị hộp thoại \{#show-dialog\}
Sử dụng phương thức này thay cho các hộp thoại alert thông thường khi một paywall view đang được hiển thị trên Android. Trên Android, các alert thông thường xuất hiện phía sau paywall view, khiến người dùng không nhìn thấy chúng. Phương thức này đảm bảo hộp thoại được hiển thị đúng cách phía trên paywall trên mọi nền tảng.
```typescript showLineNumbers title="Capacitor"
try {
const action = await view.showDialog({
title: 'Close paywall?',
content: 'You will lose access to exclusive offers.',
primaryActionTitle: 'Stay',
secondaryActionTitle: 'Close',
});
if (action === 'secondary') {
// User confirmed - close the paywall
await view.dismiss();
}
// If primary - do nothing, user stays
} catch (error) {
// handle error
}
```
## Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\}
Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`.
```typescript showLineNumbers
await view.present({ iosPresentationStyle: 'page_sheet' });
```
---
# File: capacitor-handle-paywall-actions
---
---
title: "Phản hồi hành động button trong Capacitor SDK"
description: "Xử lý hành động button trên paywall trong Capacitor bằng Adapty để tối ưu hóa việc kiếm tiền từ ứng dụng."
---
Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập button đúng cách là rất quan trọng:
1. Thêm một [button trong paywall builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một ID hành động tùy chỉnh.
2. Viết code trong ứng dụng để xử lý từng hành động bạn đã gán.
Hướng dẫn này trình bày cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn.
## Đóng paywall \{#close-paywalls\}
Để thêm một button đóng paywall:
1. Trong paywall builder, thêm một button và gán cho nó hành động **Close**.
2. Trong code ứng dụng, triển khai handler cho hành động `close` để đóng paywall.
:::info
Trong Capacitor SDK, hành động `close` mặc định sẽ kích hoạt việc đóng paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một paywall có thể kích hoạt mở một paywall khác.
:::
```typescript showLineNumbers
const view = await createPaywallView(paywall);
const unsubscribe = view.setEventHandlers({
onCloseButtonPress() {
console.log('User closed paywall');
return true; // Allow the paywall to close
}
});
```
## Mở URL từ paywall \{#open-urls-from-paywalls\}
:::tip
Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch mua), hãy thêm phần tử **Link** trong paywall builder và xử lý nó theo cách tương tự như button với hành động **Open URL**.
:::
Để thêm một button mở liên kết từ paywall của bạn (ví dụ: **Terms of use** hoặc **Privacy policy**):
1. Trong paywall builder, thêm một button, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở.
2. Trong code ứng dụng, triển khai handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt.
:::info
Trong Capacitor SDK, hành động `window.open` mặc định sẽ kích hoạt mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần.
:::
```typescript showLineNumbers
const view = await createPaywallView(paywall);
const unsubscribe = view.setEventHandlers({
onUrlPress(url) {
window.open(url, '_blank');
return false; // Don't close the paywall
},
});
```
## Đăng nhập vào ứng dụng \{#log-into-the-app\}
Để thêm một button cho phép người dùng đăng nhập vào ứng dụng:
1. Trong paywall builder, thêm một button và gán cho nó hành động **Login**.
2. Trong code ứng dụng, triển khai handler cho hành động `login` để xác định người dùng.
```typescript showLineNumbers
const view = await createPaywallView(paywall);
const unsubscribe = view.setEventHandlers({
onCustomAction(actionId) {
if (actionId === 'login') {
// Navigate to login screen
console.log('User requested login');
}
}
});
```
## Xử lý hành động tùy chỉnh \{#handle-custom-actions\}
Để thêm một button xử lý các hành động khác:
1. Trong paywall builder, thêm một button, gán cho nó hành động **Custom**, và đặt ID cho nó.
2. Trong code ứng dụng, triển khai handler cho ID hành động bạn đã tạo.
Ví dụ, nếu bạn có một bộ ưu đãi gói đăng ký khác hoặc sản phẩm mua một lần, bạn có thể thêm một button để hiển thị paywall khác:
```typescript showLineNumbers
const unsubscribe = view.setEventHandlers({
onCustomAction(actionId) {
if (actionId === 'openNewPaywall') {
// Display another paywall
console.log('User requested new paywall');
}
},
});
```
---
# File: capacitor-handling-events
---
---
title: "Capacitor - Xử lý sự kiện paywall"
description: "Xử lý sự kiện gói đăng ký trong Capacitor với SDK của Adapty."
---
:::important
Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút bấm (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](capacitor-handle-paywall-actions) để biết thêm chi tiết.
:::
Các paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Những sự kiện đó bao gồm các lần nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua trên paywall. Tìm hiểu cách phản hồi những sự kiện này bên dưới.
Để kiểm soát hoặc theo dõi các quy trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai phương thức `view.setEventHandlers`:
```typescript showLineNumbers
const view = await createPaywallView(paywall);
const unsubscribe = view.setEventHandlers({
onCloseButtonPress() {
console.log('User closed paywall');
return true; // Allow the paywall to close
},
onAndroidSystemBack() {
console.log('User pressed back button');
return true; // Allow the paywall to close
},
onAppeared() {
console.log('Paywall appeared');
return false; // Don't close the paywall
},
onDisappeared() {
console.log('Paywall disappeared');
},
onPurchaseCompleted(purchaseResult, product) {
console.log('Purchase completed:', purchaseResult);
return purchaseResult.type !== 'user_cancelled'; // Close if not cancelled
},
onPurchaseStarted(product) {
console.log('Purchase started:', product);
return false; // Don't close the paywall
},
onPurchaseFailed(error, product) {
console.error('Purchase failed:', error);
return false; // Don't close the paywall
},
onRestoreCompleted(profile) {
console.log('Restore completed:', profile);
return true; // Close the paywall after successful restore
},
onRestoreFailed(error) {
console.error('Restore failed:', error);
return false; // Don't close the paywall
},
onProductSelected(productId) {
console.log('Product selected:', productId);
return false; // Don't close the paywall
},
onRenderingFailed(error) {
console.error('Rendering failed:', error);
return false; // Don't close the paywall
},
onLoadingProductsFailed(error) {
console.error('Loading products failed:', error);
return false; // Don't close the paywall
},
onUrlPress(url) {
window.open(url, '_blank');
return false; // Don't close the paywall
},
});
```
Ví dụ sự kiện (Nhấp để mở rộng)
```typescript
// onCloseButtonPress
{
"event": "close_button_press"
}
// onAndroidSystemBack
{
"event": "android_system_back"
}
// onAppeared
{
"event": "paywall_shown"
}
// onDisappeared
{
"event": "paywall_closed"
}
// onUrlPress
{
"event": "url_press",
"url": "https://example.com/terms"
}
// onCustomAction
{
"event": "custom_action",
"actionId": "login"
}
// onProductSelected
{
"event": "product_selected",
"productId": "premium_monthly"
}
// onPurchaseStarted
{
"event": "purchase_started",
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
// onPurchaseCompleted - Success
{
"event": "purchase_completed",
"purchaseResult": {
"type": "success",
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
}
}
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
// onPurchaseCompleted - Cancelled
{
"event": "purchase_completed",
"purchaseResult": {
"type": "user_cancelled"
},
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
// onPurchaseFailed
{
"event": "purchase_failed",
"error": {
"code": "purchase_failed",
"message": "Purchase failed due to insufficient funds",
"details": {
"underlyingError": "Insufficient funds in account"
}
}
}
// onRestoreCompleted
{
"event": "restore_completed",
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
},
"subscriptions": [
{
"vendorProductId": "premium_monthly",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
]
}
}
// onRestoreFailed
{
"event": "restore_failed",
"error": {
"code": "restore_failed",
"message": "Purchase restoration failed",
"details": {
"underlyingError": "No previous purchases found"
}
}
}
// onRenderingFailed
{
"event": "rendering_failed",
"error": {
"code": "rendering_failed",
"message": "Failed to render paywall interface",
"details": {
"underlyingError": "Invalid paywall configuration"
}
}
}
// onLoadingProductsFailed
{
"event": "loading_products_failed",
"error": {
"code": "products_loading_failed",
"message": "Failed to load products from the server",
"details": {
"underlyingError": "Network timeout"
}
}
}
```
Bạn có thể đăng ký những event handler mà bạn cần và bỏ qua những cái không cần. Trong trường hợp đó, các event listener không dùng đến sẽ không được tạo. Không có event handler nào là bắt buộc.
Các event handler trả về kiểu boolean. Nếu trả về `true`, quá trình hiển thị được coi là hoàn tất, màn hình paywall sẽ đóng lại và các event listener của view này sẽ bị gỡ bỏ.
Một số event handler có hành vi mặc định mà bạn có thể ghi đè nếu cần:
- `onCloseButtonPress`: đóng paywall khi nhấn nút đóng.
- `onAndroidSystemBack`: đóng paywall khi nhấn nút **Back**.
- `onRestoreCompleted`: đóng paywall sau khi khôi phục thành công.
- `onPurchaseCompleted`: đóng paywall trừ khi người dùng hủy.
- `onRenderingFailed`: đóng paywall nếu hiển thị thất bại.
- `onUrlPress`: mở URL trên trình duyệt hệ thống và giữ paywall mở.
### Các event handler \{#event-handlers\}
| Event handler | Mô tả |
|:----------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **onCustomAction** | Được gọi khi người dùng thực hiện một hành động tùy chỉnh, ví dụ: nhấp vào [nút tùy chỉnh](paywall-buttons). |
| **onUrlPress** | Được gọi khi người dùng nhấp vào một URL trong paywall. |
| **onAndroidSystemBack** | Được gọi khi người dùng nhấn nút **Back** của hệ thống Android. |
| **onCloseButtonPress** | Được gọi khi nút đóng hiển thị và người dùng nhấp vào đó. Nên đóng màn hình paywall trong handler này. |
| **onPurchaseCompleted** | Được gọi khi giao dịch mua hoàn tất, dù thành công, bị người dùng hủy hay đang chờ phê duyệt. Trong trường hợp mua thành công, sự kiện này cung cấp `AdaptyProfile` đã được cập nhật. Người dùng hủy và thanh toán đang chờ xử lý (ví dụ: cần phê duyệt của phụ huynh) sẽ kích hoạt sự kiện này, không phải `onPurchaseFailed`. |
| **onPurchaseStarted** | Được gọi khi người dùng nhấn nút hành động "Mua" để bắt đầu quá trình mua. |
| **onPurchaseCancelled** | Được gọi khi người dùng khởi tạo quá trình mua và tự tay ngắt nó (hủy hộp thoại thanh toán). |
| **onPurchaseFailed** | Được gọi khi giao dịch mua thất bại do lỗi (ví dụ: hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng, xác minh giao dịch thất bại). Không được gọi khi người dùng hủy hoặc thanh toán đang chờ xử lý — những trường hợp này sẽ kích hoạt `onPurchaseCompleted`. |
| **onRestoreStarted** | Được gọi khi người dùng bắt đầu quá trình khôi phục giao dịch mua. |
| **onRestoreCompleted** | Được gọi khi khôi phục giao dịch mua thành công và cung cấp `AdaptyProfile` đã được cập nhật. Nên đóng màn hình nếu người dùng có `accessLevel` cần thiết. Tham khảo chủ đề [Trạng thái gói đăng ký](capacitor-listen-subscription-changes) để biết cách kiểm tra. |
| **onRestoreFailed** | Được gọi khi quá trình khôi phục thất bại và cung cấp `AdaptyError`. |
| **onProductSelected** | Được gọi khi bất kỳ sản phẩm nào trong paywall view được chọn, cho phép bạn theo dõi những gì người dùng chọn trước khi mua. |
| **onAppeared** | Được gọi khi paywall view xuất hiện trên màn hình. Trên iOS, cũng được gọi khi người dùng nhấn [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall và web paywall mở ra trong trình duyệt trong ứng dụng. |
| **onDisappeared** | Được gọi khi paywall view biến mất khỏi màn hình. Trên iOS, cũng được gọi khi [web paywall](web-paywall#step-2a-add-a-web-purchase-button) được mở từ một paywall trong trình duyệt trong ứng dụng biến mất khỏi màn hình. |
| **onRenderingFailed** | Được gọi khi xảy ra lỗi trong quá trình hiển thị view và cung cấp `AdaptyError`. Những lỗi như vậy không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết. |
| **onLoadingProductsFailed** | Được gọi khi tải sản phẩm thất bại và cung cấp `AdaptyError`. Nếu bạn chưa đặt `prefetchProducts: true` khi tạo view, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ máy chủ. |
---
# File: capacitor-use-fallback-paywalls
---
---
title: "Capacitor - Sử dụng paywall dự phòng"
description: "Xử lý các trường hợp người dùng ngoại tuyến hoặc máy chủ Adapty không khả dụng"
---
Để duy trì trải nghiệm người dùng mượt mà, điều quan trọng là phải thiết lập [paywall dự phòng](/fallback-paywalls) cho các flow, [paywall](paywalls) và [onboarding](onboardings) của bạn. Biện pháp phòng ngừa này giúp mở rộng khả năng của ứng dụng trong trường hợp mất kết nối internet một phần hoặc hoàn toàn.
* **Nếu ứng dụng không thể kết nối đến máy chủ Adapty:**
Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng, và truy cập cấu hình onboarding đã lưu cục bộ.
* **Nếu ứng dụng không thể kết nối internet:**
Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng. Onboarding chứa nội dung từ xa và cần có kết nối internet để hoạt động.
:::important
Trước khi thực hiện các bước trong hướng dẫn này, hãy [tải xuống](/local-fallback-paywalls) các file cấu hình dự phòng từ Adapty.
:::
## Cấu hình \{#configuration\}
### Android \{#android\}
1. Thêm file cấu hình dự phòng vào ứng dụng của bạn. Chọn một trong các thư mục sau:
* **android/app/src/main/assets/**
* **android/app/src/main/res/raw/**
Lưu ý: Thư mục `res/raw` có quy tắc đặt tên file đặc biệt (phải bắt đầu bằng chữ cái, không dùng chữ hoa, không dùng ký tự đặc biệt ngoại trừ dấu gạch dưới, và không có dấu cách trong tên).
2. Cập nhật thuộc tính `android` của hằng số `FileLocation`:
* Nếu file nằm trong thư mục `assets`, truyền đường dẫn của file tương đối so với thư mục đó.
* Nếu file nằm trong thư mục `res/raw`, truyền tên file không bao gồm phần mở rộng.
### iOS \{#ios\}
1. Thêm file JSON dự phòng vào bundle dự án của bạn: mở menu **File** trong XCode và chọn tùy chọn **Add Files to "YourProjectName"**.
2. Truyền tên file cấu hình của bạn vào thuộc tính `ios` của hằng số `FileLocation`.
## Ví dụ \{#example\}
```typescript showLineNumbers
const fileLocation = {
ios: {
fileName: 'ios_fallback.json'
},
android: {
//if the file is located in 'android/app/src/main/assets/'
relativeAssetPath: 'android_fallback.json'
}
};
await adapty.setFallback({ fileLocation });
```
Tham số:
| Tham số | Mô tả |
| :------------------- | :------------------------------------------------------- |
| **fileLocation** | Đối tượng đại diện cho vị trí của file cấu hình dự phòng. |
---
# File: capacitor-localizations-and-locale-codes
---
---
title: "Sử dụng localizations và mã locale trong Capacitor SDK"
description: "Tìm hiểu cách localize paywall trong ứng dụng Capacitor của bạn với Adapty SDK."
---
## Tại sao điều này quan trọng \{#why-this-is-important\}
Có một vài trường hợp mã locale được dùng đến — ví dụ, khi bạn cần lấy đúng paywall cho ngôn ngữ hiện tại của ứng dụng.
Vì mã locale khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi dựa vào một tiêu chuẩn nội bộ cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, vì các mã này phức tạp, điều quan trọng là bạn phải hiểu chính xác mình đang gửi gì lên server để nhận đúng localization, và điều gì xảy ra tiếp theo — để bạn luôn nhận được kết quả như mong đợi.
## Tiêu chuẩn mã locale tại Adapty \{#locale-code-standard-at-adapty\}
Với mã locale, Adapty sử dụng tiêu chuẩn [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) có chỉnh sửa nhẹ: mỗi mã gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số ví dụ: `en` (Tiếng Anh), `pt-br` (Tiếng Bồ Đào Nha (Brazil)), `zh` (Tiếng Trung Giản thể), `zh-hant` (Tiếng Trung Phồn thể).
## Khớp mã locale \{#locale-code-matching\}
Khi Adapty nhận được yêu cầu từ SDK phía client kèm mã locale và bắt đầu tìm localization tương ứng của paywall, quá trình diễn ra như sau:
1. Chuỗi locale đầu vào được chuyển về chữ thường và tất cả dấu gạch dưới (`_`) được thay bằng dấu gạch ngang (`-`)
2. Chúng tôi tìm localization có mã locale khớp hoàn toàn
3. Nếu không tìm thấy, chúng tôi lấy phần chuỗi trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tìm localization khớp
4. Nếu vẫn không tìm thấy, chúng tôi trả về localization mặc định `en`
Nhờ vậy, thiết bị iOS gửi `'pt_BR'`, thiết bị Android gửi `pt-BR`, và thiết bị khác gửi `pt-br` đều nhận được cùng một kết quả.
## Cách triển khai localizations được khuyến nghị \{#implementing-localizations-recommended-way\}
Nếu bạn đang tìm hiểu về localizations, có khả năng bạn đã làm việc với các file chuỗi đã localize trong dự án. Nếu vậy, chúng tôi khuyến nghị đặt một cặp key-value chứa mã locale Adapty dự kiến vào mỗi file tương ứng. Sau đó trích xuất giá trị theo key này khi gọi SDK, như sau:
```javascript showLineNumbers
// 1. Modify your localization files (e.g., using react-i18next)
/*
en.json
*/
{
"adapty_paywalls_locale": "en"
}
/*
es.json
*/
{
"adapty_paywalls_locale": "es"
}
/*
pt-BR.json
*/
{
"adapty_paywalls_locale": "pt-br"
}
// 2. Extract and use the locale code
const MyComponent = () => {
const { t } = useTranslation();
const fetchPaywall = async () => {
const locale = t('adapty_paywalls_locale');
// pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method
const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale);
};
};
```
Cách này giúp bạn kiểm soát hoàn toàn localization nào sẽ được lấy cho từng người dùng trong ứng dụng.
## Cách triển khai localizations: hướng khác \{#implementing-localizations-the-other-way\}
Bạn có thể đạt kết quả tương tự (nhưng không hoàn toàn giống) mà không cần định nghĩa tường minh mã locale cho từng localization. Điều đó có nghĩa là trích xuất mã locale từ các đối tượng khác mà nền tảng cung cấp, như sau:
```javascript showLineNumbers
const getLocaleCode = () => {
if (Capacitor.getPlatform() === 'ios') {
return navigator.language || 'en';
} else {
return navigator.language || 'en';
}
};
const fetchPaywall = async () => {
const locale = getLocaleCode();
// pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method
const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale);
};
```
Lưu ý rằng chúng tôi không khuyến nghị cách tiếp cận này vì một số lý do:
1. Trên iOS, ngôn ngữ ưu tiên và locale hiện tại không giống nhau. Nếu muốn localization được chọn đúng, bạn sẽ phải dựa vào logic của Apple (hoạt động ngay lập tức nếu bạn dùng cách được khuyến nghị với file chuỗi đã localize), hoặc tự tái tạo lại logic đó.
2. Khó đoán chính xác server của Adapty sẽ nhận được gì. Ví dụ, trên iOS có thể lấy được locale như `ar_OM@numbers='latn'` từ thiết bị rồi gửi lên server. Với lời gọi này, bạn sẽ không nhận được localization `ar-om` như mong muốn, mà thay vào đó là `ar` — điều này có thể ngoài ý muốn.
Nếu bạn vẫn quyết định dùng cách này — hãy đảm bảo bạn đã xử lý đầy đủ tất cả các trường hợp liên quan.
---
# File: capacitor-web-paywall
---
---
title: "Implement web paywalls"
description: "Tìm hiểu cách triển khai web paywalls trong ứng dụng Capacitor của bạn với Adapty SDK."
---
:::important
Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trong dashboard](web-paywall) và đã cài đặt Adapty SDK phiên bản 3.6.1 trở lên.
:::
## Mở web paywalls \{#open-web-paywalls\}
Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywalls bằng phương thức SDK. Phương thức `.openWebPaywall`:
1. Tạo một URL duy nhất cho phép Adapty liên kết một paywall cụ thể được hiển thị cho một người dùng nhất định với trang web họ được chuyển hướng đến.
2. Theo dõi khi người dùng quay lại ứng dụng rồi gọi `.getProfile` theo các khoảng thời gian ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không.
Nhờ vậy, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ kích hoạt trong ứng dụng gần như ngay lập tức.
```typescript showLineNumbers
try {
await adapty.openWebPaywall({ paywallOrProduct: product });
} catch (error) {
console.error('Failed to open web paywall:', error);
}
```
:::note
Có hai phiên bản của phương thức `openWebPaywall`:
1. `openWebPaywall({ paywallOrProduct: product })` tạo URL theo paywall và cũng thêm dữ liệu sản phẩm vào URL.
2. `openWebPaywall({ paywallOrProduct: paywall })` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng khi các sản phẩm trong Adapty paywall khác với các sản phẩm trong web paywall.
:::
#### Xử lý lỗi \{#handle-errors\}
| Lỗi | Mô tả | Hành động khuyến nghị |
|-----------------------------------------|---------------------------------------------------------------|--------------------------------------------------------------------------------------|
| AdaptyError.paywallWithoutPurchaseUrl | Paywall chưa được cấu hình URL mua hàng trên web | Kiểm tra xem paywall đã được cấu hình đúng trong Adapty Dashboard chưa |
| AdaptyError.productWithoutPurchaseUrl | Sản phẩm chưa có URL mua hàng trên web | Xác minh cấu hình sản phẩm trong Adapty Dashboard |
| AdaptyError.failedOpeningWebPaywallUrl | Không thể mở URL trên trình duyệt | Kiểm tra cài đặt thiết bị hoặc cung cấp phương thức mua hàng thay thế |
| AdaptyError.failedDecodingWebPaywallUrl | Không thể mã hóa các tham số trong URL đúng cách | Xác minh các tham số URL hợp lệ và được định dạng đúng |
## Mở web paywalls trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\}
:::important
Mở web paywalls trong trình duyệt trong ứng dụng được hỗ trợ bắt đầu từ Adapty SDK v3.15.
:::
Theo mặc định, web paywalls mở trong trình duyệt bên ngoài.
Để mang lại trải nghiệm người dùng liền mạch, bạn có thể mở web paywalls trong trình duyệt ngay trong ứng dụng. Điều này hiển thị trang mua hàng trên web ngay trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác.
Để bật tính năng này, đặt `openIn` thành `WebPresentation.BrowserInApp` trong `openWebPaywall`:
```typescript showLineNumbers
try {
await adapty.openWebPaywall({
paywallOrProduct: product,
openIn: WebPresentation.BrowserInApp, // default – WebPresentation.BrowserOutApp
});
} catch (error) {
console.error('Failed to open web paywall:', error);
}
```
---
# File: capacitor-implement-paywalls-manually
---
---
title: "Implement paywalls manually"
description: "Learn how to implement paywalls manually in your Capacitor app with Adapty SDK."
---
## Accept purchases
If you are working with paywalls you've implemented yourself, you can delegate handling purchases to Adapty, using the `makePurchase` method. This way, we will handle all the user scenarios, and you will only need to handle the purchase results.
:::important
`makePurchase` works with products created in the Adapty dashboard. Make sure you configure products and ways to retrieve them in the dashboard by following the [quickstart guide](quickstart).
:::
## 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: capacitor-quickstart-manual
---
---
title: "Kích hoạt giao dịch mua trong paywall tùy chỉnh của bạn trong Capacitor SDK"
description: "Tích hợp Adapty SDK vào các paywall tùy chỉnh trên Capacitor để kích hoạt in-app purchase."
---
Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Bạn hoàn toàn kiểm soát việc triển khai paywall, trong khi Adapty SDK lo việc lấy sản phẩm, xử lý giao dịch mua mới và khôi phục các giao dịch trước đó.
:::important
**Hướng dẫn này dành cho các nhà phát triển đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để kích hoạt giao dịch mua, hãy sử dụng [Adapty Paywall Builder](capacitor-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic giao dịch, và bạn có thể kiểm tra các thiết kế khác nhau mà không cần phát hành lại ứng dụng.
:::
## Trước khi bắt đầu \{#before-you-start\}
### Thiết lập sản phẩm \{#set-up-products\}
Để kích hoạt in-app purchase, bạn cần hiểu ba khái niệm chính:
- [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời)
- [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn chỉnh sửa sản phẩm, giá cả và ưu đãi mà không cần thay đổi code ứng dụng.
- [**Placements**](placements) – nơi và thời điểm hiển thị paywall trong ứng dụng của bạn (ví dụ: `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trong dashboard, sau đó yêu cầu chúng theo placement ID trong code. Điều này giúp bạn dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho các nhóm người dùng khác nhau.
Hãy đảm bảo bạn hiểu các khái niệm này ngay cả khi bạn làm việc với paywall tùy chỉnh. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng.
Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy các sản phẩm. Để hiểu những gì cần làm trên dashboard, hãy xem hướng dẫn bắt đầu nhanh [tại đây](quickstart).
### Quản lý người dùng \{#manage-users\}
Bạn có thể làm việc với hoặc không có xác thực backend ở phía mình.
Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo cách khác nhau. Đọc [hướng dẫn bắt đầu nhanh về nhận dạng người dùng](capacitor-quickstart-identify) để hiểu rõ các đặc điểm và đảm bảo bạn đang làm việc với người dùng đúng cách.
## Bước 1. Lấy sản phẩm \{#step-1-get-products\}
Để lấy sản phẩm cho paywall tùy chỉnh, bạn cần:
1. Lấy đối tượng `paywall` bằng cách truyền [placement](placements) ID vào phương thức `getPaywall`.
2. Lấy mảng sản phẩm cho paywall này bằng phương thức `getPaywallProducts`.
```typescript showLineNumbers
async function loadPaywall() {
try {
const paywall: AdaptyPaywall = await adapty.getPaywall({
placementId: 'YOUR_PLACEMENT_ID'
});
const products: AdaptyPaywallProduct[] = await adapty.getPaywallProducts({
paywall
});
// Use products to build your custom paywall UI
} catch (error) {
// Handle the error
}
}
```
## Bước 2. Chấp nhận giao dịch mua \{#step-2-accept-purchases\}
Khi người dùng nhấn vào một sản phẩm trong paywall tùy chỉnh của bạn, hãy gọi phương thức `makePurchase` với sản phẩm đã chọn. Phương thức này sẽ xử lý luồng giao dịch và trả về hồ sơ người dùng đã cập nhật.
```typescript showLineNumbers
async function purchaseProduct(product: AdaptyPaywallProduct) {
try {
const result: AdaptyPurchaseResult = await adapty.makePurchase({ product });
if (result.type === 'success') {
// Purchase successful, profile updated
} else if (result.type === 'user_cancelled') {
// User canceled the purchase
} else if (result.type === 'pending') {
// Purchase is pending (e.g., user will pay offline with cash)
}
} catch (error) {
// Handle the error
}
}
```
## Bước 3. Khôi phục giao dịch mua \{#step-3-restore-purchases\}
Các cửa hàng ứng dụng yêu cầu tất cả ứng dụng có gói đăng ký phải cung cấp cách để người dùng khôi phục giao dịch mua của họ.
Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Phương thức này sẽ đồng bộ lịch sử giao dịch của họ với Adapty và trả về hồ sơ người dùng đã cập nhật.
```typescript showLineNumbers
async function restorePurchases() {
try {
const profile: AdaptyProfile = await adapty.restorePurchases();
// Restore successful, profile updated
} catch (error) {
// Handle the error
}
}
```
## Các bước tiếp theo \{#next-steps\}
---
no_index: true
---
import Callout from '../../../components/Callout.astro';
Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ!
Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra giao dịch mua trong [sandbox App Store](test-purchases-in-sandbox) hoặc trong [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một giao dịch mua thử nghiệm từ paywall. Để xem cách hoạt động trong một triển khai sẵn sàng cho môi trường sản xuất, hãy xem [App.tsx](https://github.com/adaptyteam/AdaptySDK-Capacitor/blob/master/examples/adapty-devtools/src/screens/app/App.tsx) trong ứng dụng ví dụ của chúng tôi, nơi minh họa cách xử lý giao dịch mua với xử lý lỗi phù hợp, trạng thái tải và tích hợp SDK toàn diện.
Tiếp theo, [kiểm tra xem người dùng đã hoàn thành giao dịch mua chưa](capacitor-check-subscription-status) để xác định có nên hiển thị paywall hay cấp quyền truy cập các tính năng trả phí.
---
# File: fetch-paywalls-and-products-capacitor
---
---
title: "Lấy paywalls và sản phẩm cho remote config paywalls trong Capacitor SDK"
description: "Lấy paywalls và sản phẩm trong Adapty Capacitor SDK để tăng cường monetization cho người dùng."
---
Trước khi hiển thị remote config và custom paywalls, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến remote config và custom paywalls. Để biết hướng dẫn lấy paywalls cho Paywall Builder, vui lòng tham khảo [Lấy Paywall Builder paywalls và cấu hình của chúng](capacitor-get-pb-paywalls).
:::tip
Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác.
:::
Trước khi bắt đầu lấy paywalls và sản phẩm trong ứng dụng (nhấn để mở rộng)
1. [Tạo sản phẩm](create-product) trong Adapty Dashboard.
2. [Tạo paywall và thêm sản phẩm vào paywall](create-paywall) trong Adapty Dashboard.
3. [Tạo placements và thêm paywall vào placement](create-placement) trong Adapty Dashboard.
4. [Cài đặt Adapty SDK](sdk-installation-capacitor) trong ứng dụng của bạn.
## Lấy thông tin paywall \{#fetch-paywall-information\}
Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào paywalls, cho phép bạn hiển thị chúng tại các placement cụ thể trong ứng dụng.
Để hiển thị sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placements](placements) của bạn bằng phương thức `getPaywall`.
```typescript showLineNumbers
try {
const paywall = await adapty.getPaywall({
placementId: 'YOUR_PLACEMENT_ID',
locale: 'en',
params: {
fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache
loadTimeoutMs: 5000 // 5 second timeout
}
});
// the requested paywall
} catch (error) {
console.error('Failed to fetch paywall:', error);
}
```
| Tham số | Bắt buộc | Mô tả |
|---------|--------|-----------|
| **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. |
| **locale** | 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 phải là mã ngôn ngữ gồm một hoặc nhiều subtag được phân tách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là khu vực.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
Xem [Localizations and locale codes](capacitor-localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.
|
| **params.fetchPolicy** | tùy chọn
mặc định: `'reload_revalidating_cache_data'`
| 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 người dùng của bạn thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn dù kết nối có yếu đến đâu. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi dùng trong phiên để tránh các yêu cầu mạng.
Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.
|
| **params.loadTimeoutMs** | tùy chọn
mặc định: 5000 ms
| Giá trị này giới hạn thời gian chờ (tính bằng mili giây) cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.
Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeoutMs`, vì quá trình có thể bao gồm nhiều request khác nhau bên dưới.
|
**Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywalls được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt — nếu paywall hôm nay trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code.
Tham số trả về:
| Tham số | Mô tả |
| :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Paywall | Đối tượng [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) bao gồm: danh sách ID sản phẩm, định danh paywall, remote config và một số thuộc tính khác. |
## Lấy sản phẩm \{#fetch-products\}
Sau khi có paywall, bạn có thể truy vấn mảng sản phẩm tương ứng với nó:
```typescript showLineNumbers
try {
const products = await adapty.getPaywallProducts({ paywall });
// the requested products list
} catch (error) {
console.error('Failed to fetch products:', error);
}
```
Tham số trả về:
| Tham số | Mô tả |
| :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Products | Danh sách đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) bao gồm: định danh sản phẩm, tên sản phẩm, giá, tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. |
Khi tự thiết kế giao diện paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct). Dưới đây là các thuộc tính được dùng phổ biến nhất, nhưng hãy tham khảo tài liệu được liên kết để xem đầy đủ tất cả các thuộc tính có sẵn.
| Thuộc tính | Mô tả |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Title** | Để hiển thị tiêu đề sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải locale của thiết bị. |
| **Price** | Để hiển thị giá đã được bản địa hóa, dùng `product.price?.localizedString`. Bản địa hóa này dựa trên thông tin locale của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price?.amount`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price?.currencySymbol`. |
| **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedSubscriptionPeriod`. Bản địa hóa này dựa trên locale của thiết bị. Để lấy chu kỳ gói đăng ký theo dạng lập trình, dùng `product.subscription?.subscriptionPeriod`. Từ đó bạn có thể truy cập thuộc tính `unit` để lấy độ dài (tức là 'day', 'week', 'month', 'year', hoặc 'unknown'). Giá trị `numberOfUnits` sẽ cho bạn số đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `'month'` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. |
| **Introductory Offer** | Để hiển thị huy hiệu hoặc chỉ số khác cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:
• `paymentMode`: chuỗi với các giá trị `'free_trial'`, `'pay_as_you_go'`, `'pay_up_front'` và `'unknown'`. Dùng thử miễn phí sẽ có loại `'free_trial'`.
• `price`: Giá giảm dưới dạng số. Với dùng thử miễn phí, giá trị này là `0`.
• `localizedNumberOfPeriods`: chuỗi được bản địa hóa theo locale của thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày hiển thị `'3 days'` trong trường này.
• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết từng phần của chu kỳ ưu đãi bằng thuộc tính này. Nó hoạt động theo cách tương tự như mô tả ở phần trước.
• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo locale của người dùng cho phần giảm giá. |
## Tăng tốc lấy paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\}
Thông thường, paywalls được lấy gần như ngay lập tức nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywalls, và người dùng có kết nối internet yếu, việc lấy paywall có thể mất nhiều thời gian hơn mong muốn. Trong tình huống đó, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả.
Để giải quyết vấn đề này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này lấy paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy paywall bằng phương thức `getPaywall`, như được mô tả chi tiết trong phần [Lấy thông tin paywall](fetch-paywalls-and-products-capacitor#fetch-paywall-information) ở trên.
:::warning
Lý do chúng tôi khuyến nghị dùng `getPaywall`
Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể:
- **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywalls khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế paywalls hỗ trợ phiên bản hiện tại (legacy) hoặc chấp nhận rằng người dùng phiên bản hiện tại (legacy) có thể gặp vấn đề với paywalls không render được.
- **Mất targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng targeting cá nhân hóa (bao gồm dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của bạn).
Nếu bạn sẵn sàng chấp nhận những hạn chế này để được hưởng lợi từ việc lấy paywall nhanh hơn, hãy dùng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` được mô tả [ở trên](fetch-paywalls-and-products-capacitor#fetch-paywall-information).
:::
```typescript showLineNumbers
try {
const paywall = await adapty.getPaywallForDefaultAudience({
placementId: 'YOUR_PLACEMENT_ID',
locale: 'en',
params: {
fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache
}
});
// the requested paywall
} catch (error) {
console.error('Failed to fetch default audience paywall:', error);
}
```
:::note
Phương thức `getPaywallForDefaultAudience` khả dụng từ Capacitor SDK phiên bản 2.11.2 trở lên.
:::
| Tham số | Bắt buộc | Mô tả |
|---------|--------|-----------|
| **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. |
| **locale** | 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 phải là mã ngôn ngữ gồm một hoặc nhiều subtag được phân tách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là khu vực.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
Xem [Localizations and locale codes](capacitor-localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.
|
| **params.fetchPolicy** | tùy chọn
mặc định: `'reload_revalidating_cache_data'`
| 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 người dùng của bạn thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc dùng `'return_cache_data_else_load'` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn dù kết nối có yếu đến đâu. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi dùng trong phiên để tránh các yêu cầu mạng.
Lưu ý rằng cache vẫn tồn tại khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.
|
---
# File: present-remote-config-paywalls-capacitor
---
---
title: "Hiển thị paywall được thiết kế bằng Remote Config trong Capacitor SDK"
description: "Khám phá cách trình bày paywall Remote Config trong Adapty Capacitor SDK để cá nhân hóa trải nghiệm người dùng."
---
Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng có thể thấy. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn kiểm soát những gì được hiển thị và giao diện paywall trông như thế nào. Chúng tôi cung cấp một phương thức để lấy cấu hình remote, giúp bạn tự do trình bày paywall tùy chỉnh đã được thiết lập qua Remote Config.
## Lấy Remote Config của paywall và hiển thị nó \{#get-paywall-remote-config-and-present-it\}
Để lấy Remote Config của một paywall, truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết.
```typescript showLineNumbers
try {
const paywall = await adapty.getPaywall({
placementId: 'YOUR_PLACEMENT_ID',
params: {
fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache
loadTimeoutMs: 5000 // 5 second timeout
}
});
const headerText = paywall.remoteConfig?.data?.['header_text'];
} catch (error) {
console.error('Failed to fetch paywall:', error);
}
```
Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang trực quan hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều loại màn hình điện thoại và hướng xoay khác nhau, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị.
:::warning
Hãy nhớ [ghi lại sự kiện xem paywall](present-remote-config-paywalls-capacitor#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập dữ liệu cho funnel và A/B test.
:::
Sau khi hoàn tất việc hiển thị paywall, hãy tiếp tục thiết lập luồng mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](capacitor-making-purchases).
Chúng tôi khuyến nghị [tạo một paywall dự phòng](capacitor-use-fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó.
## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\}
Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi dữ liệu mua hàng được thu thập tự động, việc ghi lại lượt xem paywall cần có sự tham gia của bạn vì chỉ bạn mới biết khi nào người dùng nhìn thấy paywall.
Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test.
:::important
Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị paywall được tạo trong [Paywall Builder](adapty-paywall-builder).
:::
```typescript showLineNumbers
try {
await adapty.logShowPaywall({ paywall });
} catch (error) {
console.error('Failed to log paywall view:', error);
}
```
Tham số yêu cầu:
| Tham số | Bắt buộc | Mô tả |
| :---------- | :------- | :--------------------------------------------------------- |
| **paywall** | bắt buộc | Một đối tượng [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall). |
---
# File: capacitor-making-purchases
---
---
title: "Thực hiện mua hàng trong ứng dụng với Capacitor SDK"
description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty."
---
Hiển thị paywall trong ứng dụng là bước không thể thiếu để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ cần hiển thị paywall là đủ để hỗ trợ mua hàng nếu bạn dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall của mình.
Nếu bạn không dùng Paywall Builder, bạn phải sử dụng một phương thức riêng là `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung mong muốn. Phương thức này đóng vai trò là cổng để người dùng tương tác với paywall và thực hiện giao dịch.
Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm mà người dùng muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng.
Hãy đảm bảo bạn đã [hoàn tất cấu hình ban đầu](quickstart) mà không bỏ qua bước nào. Nếu thiếu, chúng tôi không thể xác thực giao dịch mua hàng.
## Thực hiện mua hàng \{#make-purchase\}
:::note
**Đang dùng [Paywall Builder](adapty-paywall-builder)?** Giao dịch mua hàng được xử lý tự động — bạn có thể bỏ qua bước này.
**Muốn có hướng dẫn từng bước?** Xem [hướng dẫn quickstart](capacitor-implement-paywalls-manually) để có hướng dẫn triển khai đầy đủ từ đầu đến cuối.
:::
```typescript showLineNumbers
try {
const result = await adapty.makePurchase({ product });
if (result.type === 'success') {
const isSubscribed = result.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive;
if (isSubscribed) {
// Grant access to the paid features
console.log('User is now subscribed!');
}
} else if (result.type === 'user_cancelled') {
console.log('Purchase cancelled by user');
} else if (result.type === 'pending') {
console.log('Purchase is pending');
}
} catch (error) {
console.error('Purchase failed:', error);
}
```
Tham số yêu cầu:
| Tham số | Bắt buộc | Mô tả |
| :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------|
| **product** | bắt buộc | Đối tượng [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) lấy từ paywall. |
Tham số phản hồi:
| Tham số | Mô tả |
|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **result** | Đối tượng [`AdaptyPurchaseResult`](https://capacitor.adapty.io/types/adaptypurchaseresult) với trường `type` cho biết kết quả mua hàng (`'success'`, `'user_cancelled'` hoặc `'pending'`) và trường `profile` chứa [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile) đã được cập nhật khi mua hàng thành công. |
## Thay đổi gói đăng ký khi mua hàng \{#change-subscription-when-making-a-purchase\}
Khi người dùng chọn một gói đăng ký mới thay vì gia hạn gói hiện tại, cách thức hoạt động phụ thuộc vào cửa hàng:
- Với App Store, gói đăng ký được cập nhật tự động trong cùng nhóm gói đăng ký. Nếu người dùng mua gói đăng ký từ một nhóm trong khi đang có gói đăng ký từ nhóm khác, cả hai sẽ cùng hoạt động.
- Với Google Play, gói đăng ký không được cập nhật tự động. Bạn cần tự xử lý việc chuyển đổi trong code ứng dụng của mình như mô tả bên dưới.
Để thay thế gói đăng ký bằng gói khác trên Android, hãy gọi phương thức `.makePurchase()` với tham số bổ sung:
```typescript showLineNumbers
try {
const result = await adapty.makePurchase({
product,
params: {
android: {
subscriptionUpdateParams: {
oldSubVendorProductId: 'old_product_id',
prorationMode: 'charge_prorated_price'
},
isOfferPersonalized: true
}
}
});
if (result.type === 'success') {
const isSubscribed = result.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive;
if (isSubscribed) {
// Grant access to the paid features
console.log('Subscription updated successfully!');
}
} else if (result.type === 'user_cancelled') {
console.log('Purchase cancelled by user');
} else if (result.type === 'pending') {
console.log('Purchase is pending');
}
} catch (error) {
console.error('Purchase failed:', error);
}
```
Tham số yêu cầu bổ sung:
| Tham số | Bắt buộc | Mô tả |
| :--------- | :------- | :----------------------------------------------------------- |
| **params** | tùy chọn | Đối tượng kiểu [`MakePurchaseParamsInput`](https://capacitor.adapty.io/types/makepurchaseparamsinput) chứa các tham số mua hàng theo từng nền tảng. |
Cấu trúc `MakePurchaseParamsInput` bao gồm:
```typescript
{
android: {
subscriptionUpdateParams: {
oldSubVendorProductId: 'old_product_id',
prorationMode: 'charge_prorated_price'
},
isOfferPersonalized: true
}
}
```
Bạn có thể đọc thêm về gói đăng ký và các chế độ thay thế trong tài liệu Google Developer:
- [Giới thiệu về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-modes)
- [Khuyến nghị của Google về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations)
- Chế độ thay thế [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Lưu ý: phương thức này chỉ khả dụng khi nâng cấp gói đăng ký. Không hỗ trợ hạ cấp.
- Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Việc thay đổi gói đăng ký thực sự chỉ diễn ra khi chu kỳ thanh toán hiện tại kết thúc.
### Quản lý gói trả trước (Android) \{#manage-prepaid-plans-android\}
Nếu người dùng ứng dụng của bạn có thể mua [gói trả trước](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (ví dụ: mua gói đăng ký không tự gia hạn cho vài tháng), bạn có thể bật [giao dịch đang chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước.
```typescript showLineNumbers
await adapty.activate({
apiKey: 'YOUR_PUBLIC_SDK_KEY',
params: {
android: {
enablePendingPrepaidPlans: true,
},
}
});
```
## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\}
---
no_index: true
---
import Callout from '../../../components/Callout.astro';
Về offer code
Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng.
Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code:
- **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ.
- **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ.
- **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ.
Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds).
#### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\}
Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect:
- Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code.
- Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm.
Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**.
#### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\}
Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect.
Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect.
Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác.
Để hiển thị trang đổi mã trong ứng dụng của bạn:
```typescript showLineNumbers
try {
await adapty.presentCodeRedemptionSheet();
} catch (error) {
console.error('Failed to present code redemption sheet:', error);
}
```
:::danger
Theo quan sát của chúng tôi, trang Offer Code Redemption trong một số ứng dụng có thể hoạt động không ổn định. Chúng tôi khuyên bạn nên chuyển hướng người dùng thẳng đến App Store.
Để làm điều này, bạn cần mở URL theo định dạng sau:
`https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}`
:::
---
# File: capacitor-restore-purchase
---
---
title: "Khôi phục giao dịch trong ứng dụng mobile với Capacitor SDK"
description: "Tìm hiểu cách khôi phục giao dịch trong Adapty để đảm bảo trải nghiệm người dùng liền mạch."
---
Khôi phục giao dịch trên cả iOS và Android là tính năng cho phép người dùng lấy lại quyền truy cập vào nội dung đã mua trước đó, chẳng hạn như các gói đăng ký hoặc in-app purchase, mà không bị tính phí lần nữa. Tính năng này đặc biệt hữu ích cho những người dùng đã gỡ cài đặt rồi cài lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập nội dung đã mua mà không phải thanh toán lại.
:::note
Với các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), giao dịch sẽ được khôi phục tự động mà không cần thêm code từ phía bạn. Nếu đó là trường hợp của bạn — bạn có thể bỏ qua bước này.
:::
Để khôi phục giao dịch khi bạn không dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`:
```typescript showLineNumbers
try {
const profile = await adapty.restorePurchases();
const isSubscribed = profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive;
if (isSubscribed) {
// Restore access to paid features
console.log('Access restored successfully!');
} else {
console.log('No active subscriptions found');
}
} catch (error) {
console.error('Failed to restore purchases:', error);
}
```
Tham số phản hồi:
| Tham số | Mô tả |
|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **profile** | Đối tượng [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile). Model này chứa thông tin về các mức độ truy cập, gói đăng ký và các sản phẩm mua một lần. Kiểm tra **trạng thái mức độ truy cập** để xác định xem người dùng có quyền truy cập vào ứng dụng hay không. |
---
# File: implement-observer-mode-capacitor
---
---
title: "Triển khai Observer mode trong Capacitor SDK"
description: "Triển khai observer mode trong Adapty để theo dõi các sự kiện đăng ký của người dùng trong Capacitor SDK."
---
Nếu bạn đã có hạ tầng mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp phân tích nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics.
Nếu điều này phù hợp với nhu cầu của bạn, bạn chỉ cần:
1. Bật nó lên khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. Làm theo hướng dẫn cài đặt cho [Capacitor](sdk-installation-capacitor#activate-adapty-module-of-adapty-sdk).
2. [Báo cáo giao dịch](report-transactions-observer-mode-capacitor) từ hạ tầng mua hàng hiện có của bạn lên Adapty.
### Cài đặt Observer mode \{#observer-mode-setup\}
Bật Observer mode nếu bạn tự xử lý việc mua hàng và trạng thái gói đăng ký, đồng thời chỉ sử dụng Adapty để gửi các sự kiện gói đăng ký và analytics.
:::important
Khi chạy ở Observer mode, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý việc đó.
:::
```typescript showLineNumbers
try {
await adapty.activate({
apiKey: 'YOUR_PUBLIC_SDK_KEY',
params: {
observerMode: true // Enable observer mode
}
});
} catch (error) {
console.error('Failed to activate Adapty:', error);
}
```
Tham số:
| Tham số | Mô tả |
| --------------------------- | ------------------------------------------------------------ |
| **observerMode** | Giá trị boolean kiểm soát [Observer mode](observer-vs-full-mode). Giá trị mặc định là `false`. |
## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\}
Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thêm một số cài đặt trong Observer mode. Đây là những gì bạn cần thực hiện ngoài các bước trên:
1. Hiển thị paywall như bình thường cho [remote config paywalls](present-remote-config-paywalls-capacitor).
2. [Liên kết paywall](report-transactions-observer-mode-capacitor) với các giao dịch mua hàng.
---
# File: report-transactions-observer-mode-capacitor
---
---
title: "Báo cáo giao dịch trong Observer Mode trong Capacitor SDK"
description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trong Capacitor SDK."
---
Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng được thực hiện qua hệ thống mua hàng hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng của mình. Việc thiết lập điều này **trước** khi phát hành ứng dụng là rất quan trọng để tránh sai sót trong analytics.
Sử dụng `reportTransaction` để báo cáo rõ ràng từng giao dịch để Adapty nhận biết được.
:::warning
**Đừng bỏ qua việc báo cáo giao dịch!**
Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận ra giao dịch đó, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp.
:::
Nếu bạn sử dụng paywall của Adapty, hãy đính kèm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác.
```typescript showLineNumbers
const variationId = paywall.variationId;
try {
await adapty.reportTransaction({
transactionId: 'your_transaction_id',
variationId: variationId
});
} catch (error) {
console.error('Failed to report transaction:', error);
}
```
Tham số:
| Tham số | Bắt buộc | Mô tả |
| ------------- | -------- | ------------------------------------------------------------ |
| **transactionId** | bắt buộc | - Đối với iOS: Mã định danh của giao dịch.
- Đối với Android: Mã định danh dạng chuỗi (`purchase.getOrderId`) của giao dịch mua hàng, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong billing library.
|
| **variationId** | tùy chọn | Mã định danh dạng chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://capacitor.adapty.io/interfaces/adaptypaywall). |
---
# File: capacitor-user
---
---
title: "Users & access"
description: "Learn how to work with users and access levels in your Capacitor app with Adapty SDK."
---
---
# File: capacitor-identifying-users
---
---
title: "Xác định người dùng trong Capacitor SDK"
description: "Tìm hiểu cách xác định người dùng trong ứng dụng Capacitor của bạn với Adapty SDK."
---
Adapty tạo một ID hồ sơ nội bộ cho mỗi người dùng. Tuy nhiên, nếu bạn có hệ thống xác thực riêng, bạn nên thiết lập Customer User ID của chính mình. Bạn có thể tìm người dùng theo Customer User ID của họ trong phần [Profiles](profiles-crm) và sử dụng nó trong [server-side API](getting-started-with-server-side-api), ID này sẽ được gửi đến tất cả các tích hợp.
### Thiết lập customer user ID khi cấu hình \{#setting-customer-user-id-on-configuration\}
Nếu bạn đã có user ID trong quá trình cấu hình, chỉ cần truyền nó dưới dạng tham số `customerUserId` vào phương thức `.activate()`:
```typescript showLineNumbers
try {
await adapty.activate({
apiKey: 'YOUR_PUBLIC_SDK_KEY',
params: {
customerUserId: 'YOUR_USER_ID'
}
});
} catch (error) {
console.error('Failed to activate Adapty:', error);
}
```
### Thiết lập customer user ID sau khi cấu hình \{#setting-customer-user-id-after-configuration\}
Nếu bạn chưa có user ID khi cấu hình SDK, bạn có thể thiết lập nó sau bất kỳ lúc nào bằng phương thức `.identify()`. Trường hợp phổ biến nhất để sử dụng phương thức này là sau khi đăng ký hoặc đăng nhập, khi người dùng chuyển từ trạng thái ẩn danh sang người dùng đã xác thực.
```typescript showLineNumbers
try {
await adapty.identify({ customerUserId: 'YOUR_USER_ID' });
console.log('User identified successfully');
} catch (error) {
console.error('Failed to identify user:', error);
}
```
Tham số yêu cầu:
| Tham số | Bắt buộc | Mô tả |
|---------|--------|-----------|
| **customerUserId** | bắt buộc | Chuỗi định danh người dùng. |
:::warning
Gửi lại dữ liệu quan trọng của người dùng
Trong một số trường hợp, chẳng hạn khi người dùng đăng nhập lại vào tài khoản của họ, máy chủ của Adapty đã có thông tin về người dùng đó. Trong những tình huống này, Adapty SDK sẽ tự động chuyển sang làm việc với người dùng mới. Nếu bạn đã truyền dữ liệu nào đó cho người dùng ẩn danh, chẳng hạn như các thuộc tính tùy chỉnh hoặc attribution từ các mạng bên thứ ba, bạn nên gửi lại dữ liệu đó cho người dùng đã xác định.
Điều quan trọng cần lưu ý là bạn nên yêu cầu lại tất cả các paywall và sản phẩm sau khi xác định người dùng, vì dữ liệu của người dùng mới có thể khác.
:::
### Đăng xuất và đăng nhập \{#logging-out-and-logging-in\}
Bạn có thể đăng xuất người dùng bất kỳ lúc nào bằng cách gọi phương thức `.logout()`:
```typescript showLineNumbers
try {
await adapty.logout();
console.log('User logged out successfully');
} catch (error) {
console.error('Failed to logout user:', error);
}
```
Sau đó, bạn có thể đăng nhập người dùng bằng phương thức `.identify()`.
## Gán `appAccountToken` (iOS) \{#assign-appaccounttoken-ios\}
[`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) là một **UUID** cho phép bạn liên kết các giao dịch App Store với danh tính người dùng nội bộ của bạn.
StoreKit gắn token này với mọi giao dịch, nhờ đó backend của bạn có thể khớp dữ liệu App Store với người dùng của bạn.
Hãy sử dụng một UUID ổn định được tạo cho mỗi người dùng và tái sử dụng nó cho cùng một tài khoản trên các thiết bị.
Điều này đảm bảo rằng các giao dịch mua và thông báo App Store được liên kết chính xác.
Bạn có thể thiết lập token theo hai cách – trong quá trình kích hoạt SDK hoặc khi xác định người dùng.
:::important
Bạn phải luôn truyền `appAccountToken` cùng với `customerUserId`.
Nếu bạn chỉ truyền token, nó sẽ không được đưa vào giao dịch.
:::
```typescript showLineNumbers
// During configuration:
await adapty.activate({
apiKey: 'YOUR_PUBLIC_SDK_KEY',
params: {
customerUserId: 'YOUR_USER_ID',
ios: { appAccountToken: "YOUR_APP_ACCOUNT_TOKEN" },
}
});
// Or when identifying users
await adapty.identify({
customerUserId: 'YOUR_USER_ID',
params: {
ios: { appAccountToken: 'YOUR_APP_ACCOUNT_TOKEN' },
}
});
```
### Thiết lập obfuscated account ID (Android) \{#set-obfuscated-account-ids-android\}
Google Play yêu cầu obfuscated account ID cho một số trường hợp sử dụng nhất định nhằm tăng cường quyền riêng tư và bảo mật cho người dùng. Các ID này giúp Google Play xác định các giao dịch mua trong khi vẫn giữ thông tin người dùng ẩn danh, điều này đặc biệt quan trọng để ngăn chặn gian lận và phân tích.
Bạn có thể cần thiết lập các ID này nếu ứng dụng của bạn xử lý dữ liệu người dùng nhạy cảm hoặc nếu bạn phải tuân thủ các quy định về quyền riêng tư cụ thể. Các ID obfuscated cho phép Google Play theo dõi các giao dịch mua mà không để lộ định danh người dùng thực tế.
```typescript showLineNumbers
// During configuration:
await adapty.activate({
apiKey: 'YOUR_PUBLIC_SDK_KEY',
params: {
android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' },
}
});
// Or when identifying users
await adapty.identify({
customerUserId: 'YOUR_USER_ID',
params: {
android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' },
}
});
```
## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\}
---
no_index: true
---
Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`.
Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID.
**Những gì Adapty có thể phát hiện trên nhiều thiết bị**
| Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì |
| --- | --- | --- |
| Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. |
| Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. |
| Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. |
| Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. |
**Khôi phục giao dịch mua trên thiết bị mới**
Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn.
Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành.
---
# File: capacitor-setting-user-attributes
---
---
title: "Đặt thuộc tính người dùng trong Capacitor SDK"
description: "Tìm hiểu cách cập nhật thuộc tính người dùng và dữ liệu hồ sơ trong ứng dụng Capacitor của bạn với Adapty SDK."
---
Bạn có thể đặt các thuộc tính tùy chọn như email, số điện thoại, v.v. cho người dùng ứng dụng của mình. Sau đó, bạn có thể sử dụng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM.
### Đặt thuộc tính người dùng \{#setting-user-attributes\}
Để đặt thuộc tính người dùng, gọi phương thức `.updateProfile()`:
```typescript showLineNumbers
const params = {
email: 'email@email.com',
phoneNumber: '+18888888888',
firstName: 'John',
lastName: 'Appleseed',
gender: 'other',
birthday: new Date().toISOString(),
};
try {
await adapty.updateProfile(params);
console.log('Profile updated successfully');
} catch (error) {
console.error('Failed to update profile:', error);
}
```
Lưu ý rằng các thuộc tính bạn đã đặt trước đó bằng phương thức `updateProfile` sẽ không bị reset.
:::tip
Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác.
:::
### Danh sách các key được phép \{#the-allowed-keys-list\}
Các key được phép của `AdaptyProfileParameters` và giá trị tương ứng được liệt kê bên dưới:
| Key | Giá trị |
|---|-----|
| **email** | String |
| **phoneNumber** | String |
| **firstName** | String |
| **lastName** | String |
| **gender** | Enum, các giá trị được phép: `'female'`, `'male'`, `'other'` |
| **birthday** | Chuỗi ngày tháng theo định dạng ISO |
### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\}
Bạn có thể đặt các thuộc tính tùy chỉnh của riêng mình. Những thuộc tính này thường liên quan đến cách người dùng sử dụng ứng dụng. Ví dụ, với ứng dụng thể dục, đó có thể là số buổi tập mỗi tuần; với ứng dụng học ngôn ngữ, đó có thể là trình độ của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi có mục tiêu, đồng thời sử dụng trong analytics để xác định chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu.
```typescript showLineNumbers
try {
await adapty.updateProfile({
codableCustomAttributes: {
key_1: 'value_1',
key_2: 2,
},
});
console.log('Custom attributes updated successfully');
} catch (error) {
console.error('Failed to update custom attributes:', error);
}
```
Để xóa các key hiện có, truyền `null` làm giá trị của chúng:
```typescript showLineNumbers
try {
// to remove keys, pass null as their values
await adapty.updateProfile({
codableCustomAttributes: {
key_1: null,
key_2: null,
},
});
console.log('Custom attributes removed successfully');
} catch (error) {
console.error('Failed to remove custom attributes:', error);
}
```
Đôi khi bạn cần biết những thuộc tính tùy chỉnh nào đã được thiết lập trước đó. Để làm điều này, hãy sử dụng trường `customAttributes` của đối tượng `AdaptyProfile`.
:::warning
Lưu ý rằng giá trị của `customAttributes` có thể không còn mới nhất, vì thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau vào bất kỳ lúc nào, nên các thuộc tính trên server có thể đã thay đổi sau lần đồng bộ cuối cùng.
:::
### Giới hạn \{#limits\}
- Tối đa 30 thuộc tính tùy chỉnh mỗi người dùng
- Tên key tối đa 30 ký tự. Tên key có thể bao gồm ký tự chữ và số cùng với các ký tự sau: `_` `-` `.`
- Giá trị có thể là chuỗi hoặc số thực (float) với tối đa 50 ký tự.
---
# File: capacitor-listen-subscription-changes
---
---
title: "Kiểm tra trạng thái gói đăng ký trong Capacitor SDK"
description: "Theo dõi và quản lý trạng thái gói đăng ký của người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng trong ứng dụng Capacitor của bạn."
---
Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải chèn thủ công các ID sản phẩm vào code của mình. Thay vào đó, bạn có thể dễ dàng xác nhận trạng thái gói đăng ký của người dùng bằng cách kiểm tra [mức độ truy cập](access-level) đang hoạt động.
Trước khi bắt đầu kiểm tra trạng thái gói đăng ký (Nhấn để mở rộng)
- Đối với iOS, thiết lập [App Store Server Notifications](enable-app-store-server-notifications)
- Đối với Android, thiết lập [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn)
## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\}
Mức độ truy cập là các thuộc tính của đối tượng [AdaptyProfile](https://capacitor.adapty.io/interfaces/adaptyprofile). Chúng tôi khuyến nghị lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](capacitor-identifying-users#setting-customer-user-id-on-configuration), sau đó cập nhật mỗi khi có thay đổi. Như vậy, bạn có thể sử dụng đối tượng profile mà không cần phải gọi lại nhiều lần.
Để nhận thông báo khi hồ sơ người dùng được cập nhật, hãy lắng nghe các thay đổi như mô tả trong phần [Lắng nghe cập nhật hồ sơ, bao gồm mức độ truy cập](capacitor-listen-subscription-changes) bên dưới.
:::tip
Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác.
:::
## Lấy mức độ truy cập từ server \{#retrieving-the-access-level-from-the-server\}
Để lấy mức độ truy cập từ server, sử dụng phương thức `.getProfile()`:
```typescript showLineNumbers
try {
const profile = await adapty.getProfile();
console.log('Profile retrieved successfully');
} catch (error) {
console.error('Failed to get profile:', error);
}
```
Các tham số phản hồi:
| Tham số | Mô tả |
| --------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **profile** | Một đối tượng [AdaptyProfile](https://capacitor.adapty.io/interfaces/adaptyprofile). Thông thường, bạn chỉ cần kiểm tra trạng thái mức độ truy cập của hồ sơ người dùng để xác định xem người dùng có quyền truy cập premium hay không. Phương thức `.getProfile` luôn cố gắng truy vấn API để cung cấp kết quả mới nhất. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ server, dữ liệu từ cache sẽ được trả về. Cần lưu ý rằng Adapty SDK thường xuyên cập nhật cache của `AdaptyProfile` để giữ thông tin luôn được cập nhật nhất có thể. |
Phương thức `.getProfile()` trả về hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Ví dụ, nếu bạn có ứng dụng báo chí và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Nhưng trong hầu hết các trường hợp, bạn chỉ cần một mức độ truy cập — khi đó bạn có thể dùng mức độ truy cập mặc định là "premium".
Dưới đây là ví dụ kiểm tra mức độ truy cập "premium" mặc định:
```typescript showLineNumbers
try {
const profile = await adapty.getProfile();
const isActive = profile.accessLevels['premium']?.isActive;
if (isActive) {
// Grant access to premium features
console.log('User has premium access');
} else {
console.log('User does not have premium access');
}
} catch (error) {
console.error('Failed to check subscription status:', error);
}
```
### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\}
Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện.
Để nhận các thông báo từ Adapty, bạn cần thực hiện thêm một số cấu hình:
```typescript showLineNumbers
// Create an "onLatestProfileLoad" event listener
adapty.addListener('onLatestProfileLoad', (data) => {
const profile = data.profile;
const isActive = profile.accessLevels['premium']?.isActive;
if (isActive) {
console.log('Subscription status updated: User has premium access');
} else {
console.log('Subscription status updated: User does not have premium access');
}
});
```
Adapty cũng kích hoạt một sự kiện khi ứng dụng khởi động. Trong trường hợp này, trạng thái gói đăng ký được lưu trong cache sẽ được truyền vào.
### Cache trạng thái gói đăng ký \{#subscription-status-cache\}
Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi server không khả dụng, dữ liệu từ cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ.
Tuy nhiên, cần lưu ý rằng không thể truy cập dữ liệu trực tiếp từ cache. SDK định kỳ truy vấn server mỗi phút để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ. Nếu có bất kỳ thay đổi nào, chẳng hạn như giao dịch mới hoặc các cập nhật khác, chúng sẽ được đồng bộ vào dữ liệu cache để giữ cache luôn nhất quán với server.
---
# File: capacitor-deal-with-att
---
---
title: "Xử lý ATT trong Capacitor SDK"
description: "Bắt đầu với Adapty trên Capacitor để đơn giản hóa việc thiết lập và quản lý gói đăng ký."
---
Nếu ứng dụng của bạn sử dụng framework AppTrackingTransparency và hiển thị yêu cầu cấp phép theo dõi ứng dụng cho người dùng, bạn cần gửi [trạng thái cấp phép](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) đến Adapty.
```typescript showLineNumbers
try {
await adapty.updateProfile({
appTrackingTransparencyStatus: AppTrackingTransparencyStatus.Authorized,
});
console.log('ATT status updated successfully');
} catch (error) {
console.error('Failed to update ATT status:', error);
}
```
:::warning
Chúng tôi khuyến nghị bạn gửi giá trị này càng sớm càng tốt khi nó thay đổi. Chỉ như vậy, dữ liệu mới được gửi kịp thời đến các tích hợp bạn đã cấu hình.
:::
---
# File: capacitor-onboardings
---
---
title: "Onboardings"
description: "Learn how to work with onboardings in your Capacitor app with Adapty SDK."
---
---
# File: capacitor-get-onboardings
---
---
title: "Lấy onboarding trong Capacitor SDK"
description: "Tìm hiểu cách lấy onboarding trong Adapty cho Capacitor."
---
Sau khi [bạn đã thiết kế phần hiển thị cho onboarding](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng Capacitor của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement và cấu hình view của nó như mô tả bên dưới.
Trước khi bắt đầu, hãy đảm bảo rằng:
1. Bạn đã [tạo một onboarding](create-onboarding).
2. Bạn đã thêm onboarding vào một [placement](placements).
## Lấy onboarding \{#fetch-onboarding\}
Khi bạn tạo một [onboarding](onboardings) bằng builder không cần code của chúng tôi, nó được lưu trữ dưới dạng một container với cấu hình mà ứng dụng của bạn cần lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu form nhập vào). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần triển khai tính năng theo dõi view riêng.
Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để ảnh có đủ thời gian tải về trước khi hiển thị cho người dùng.
Để lấy một onboarding, sử dụng phương thức `getOnboarding`:
```typescript showLineNumbers
try {
const onboarding = await adapty.getOnboarding({
placementId: 'YOUR_PLACEMENT_ID',
locale: 'en',
params: {
fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache
loadTimeoutMs: 5000 // 5 second timeout
}
});
console.log('Onboarding fetched successfully');
} catch (error) {
console.error('Failed to fetch onboarding:', error);
}
```
Sau đó, gọi phương thức `createOnboardingView` để tạo một instance view.
:::warning
Kết quả của phương thức `createOnboardingView` chỉ có thể dùng một lần. Nếu bạn cần dùng lại, hãy gọi phương thức `createOnboardingView` một lần nữa.
:::
```typescript showLineNumbers
if (onboarding.hasViewConfiguration) {
try {
const view = await createOnboardingView(onboarding);
console.log('Onboarding view created successfully');
} catch (error) {
console.error('Failed to create onboarding view:', error);
}
} else {
// Use your custom logic
console.log('Onboarding does not have view configuration');
}
```
Các tham số:
| Tham số | Bắt buộc | Mô tả |
|---------|--------|-----------|
| **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. |
| **locale** | tùy chọn
mặc định: `en`
| Định danh của bản địa hóa onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai subtag phân cách bằng dấu trừ (**-**). Subtag đầu tiên là cho ngôn ngữ, subtag thứ hai là cho khu vực.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
Xem [Localizations and locale codes](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.
|
| **params.fetchPolicy** | tùy chọn
mặc định: `'reload_revalidating_cache_data'`
| 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 gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `'return_cache_data_else_load'` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ không ổn định đến đâu. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng nó trong phiên để tránh các yêu cầu mạng.
Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.
|
| **params.loadTimeoutMs** | tùy chọn
mặc định: 5000 ms
| Giá trị này giới hạn thời gian chờ (tính bằng mili giây) cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.
Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị chỉ định trong `loadTimeoutMs`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.
|
Các tham số phản hồi:
| Tham số | Mô tả |
|:----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **onboarding** | Một đối tượng [`AdaptyOnboarding`](https://capacitor.adapty.io/interfaces/adaptyonboarding) bao gồm: định danh và cấu hình onboarding, Remote Config, và một số thuộc tính khác. |
## Tăng tốc lấy onboarding bằng onboarding dành cho đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\}
Thông thường, onboarding được lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong các trường hợp bạn có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào cả.
Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như được mô tả chi tiết trong phần [Lấy onboarding](#fetch-onboarding) ở trên.
:::warning
Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng:
- **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, yêu cầu thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không đúng.
- **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ việc nhắm mục tiêu theo quốc gia, attribution, hoặc các thuộc tính tùy chỉnh.
Nếu việc lấy nhanh hơn có lợi hơn những hạn chế này cho trường hợp sử dụng của bạn, hãy dùng `getOnboardingForDefaultAudience` như bên dưới. Nếu không, hãy sử dụng `getOnboarding` như mô tả [ở trên](#fetch-onboarding).
:::
```typescript showLineNumbers
try {
const onboarding = await adapty.getOnboardingForDefaultAudience({
placementId: 'YOUR_PLACEMENT_ID',
locale: 'en',
params: {
fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache
}
});
console.log('Default audience onboarding fetched successfully');
} catch (error) {
console.error('Failed to fetch default audience onboarding:', error);
}
```
Các tham số:
| Tham số | Bắt buộc | Mô tả |
|---------|--------|-----------|
| **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. |
| **locale** | tùy chọn
mặc định: `en`
| Định danh của bản địa hóa onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai subtag phân cách bằng dấu trừ (**-**). Subtag đầu tiên là cho ngôn ngữ, subtag thứ hai là cho khu vực.
Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.
Xem [Localizations and locale codes](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.
|
| **params.fetchPolicy** | tùy chọn
mặc định: `'reload_revalidating_cache_data'`
| 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 gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `'return_cache_data_else_load'` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ không ổn định đến đâu. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng nó trong phiên để tránh các yêu cầu mạng.
Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.
|
---
# File: capacitor-present-onboardings
---
---
title: "Hiển thị onboarding trong Capacitor SDK"
description: "Khám phá cách hiển thị onboarding trên Capacitor để tăng tỷ lệ chuyển đổi và doanh thu."
---
Nếu bạn đã tùy chỉnh một onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Onboarding đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị.
Trước khi bắt đầu, hãy đảm bảo rằng:
1. Bạn đã [tạo một onboarding](create-onboarding).
2. Bạn đã thêm onboarding vào một [placement](placements).
## Hiển thị onboarding \{#present-onboarding\}
Để hiển thị một onboarding, sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức `createOnboardingView`. Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại onboarding, hãy gọi `createOnboardingView` thêm một lần nữa để tạo một instance `view` mới.
:::warning
Tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi.
:::
```typescript showLineNumbers
try {
const view = await createOnboardingView(onboarding);
view.setEventHandlers({
onClose: (actionId, meta) => {
console.log('Onboarding closed:', actionId);
return true; // Allow the onboarding to close
},
onCustom: (actionId, meta) => {
console.log('Custom action:', actionId);
return false; // Don't close the onboarding
}
});
await view.present();
console.log('Onboarding presented successfully');
} catch (error) {
console.error('Failed to present onboarding:', error);
}
```
## Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\}
Cấu hình cách onboarding được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`.
```typescript showLineNumbers
await view.present({ iosPresentationStyle: 'page_sheet' });
```
## Tùy chỉnh cách mở liên kết trong onboarding \{#customize-how-links-open-in-onboardings\}
:::important
Tùy chỉnh cách mở liên kết trong onboarding được hỗ trợ từ Adapty SDK v3.15 trở lên.
:::
Theo mặc định, các liên kết trong onboarding mở trong trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch cho người dùng bằng cách hiển thị các trang web ngay trong ứng dụng của bạn, cho phép người dùng xem mà không cần chuyển sang ứng dụng khác.
Nếu bạn muốn mở liên kết trong trình duyệt ngoài thay thế, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `openIn` thành `browser_out_app`:
```typescript showLineNumbers
await view.present({ openIn: 'browser_out_app' }); // default — browser_in_app
```
## Các bước tiếp theo \{#next-steps\}
Sau khi hiển thị onboarding, bạn sẽ muốn [xử lý các tương tác và sự kiện của người dùng](capacitor-handling-onboarding-events). Tìm hiểu cách xử lý các sự kiện onboarding để phản hồi hành động của người dùng và theo dõi analytics.
---
# File: capacitor-handling-onboarding-events
---
---
title: "Xử lý sự kiện onboarding trong Capacitor SDK"
description: "Xử lý các sự kiện liên quan đến onboarding trong Capacitor bằng Adapty."
---
Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể xử lý. Sử dụng phương thức `setEventHandlers` để xử lý các sự kiện này khi hiển thị màn hình độc lập.
Trước khi bắt đầu, hãy đảm bảo rằng:
1. Bạn đã [tạo một onboarding](create-onboarding).
2. Bạn đã thêm onboarding vào một [placement](placements).
## Thiết lập event handler \{#set-up-event-handlers\}
Để xử lý sự kiện cho các onboarding, hãy sử dụng phương thức `view.setEventHandlers`:
```typescript showLineNumbers
try {
const view = await createOnboardingView(onboarding);
view.setEventHandlers({
onAnalytics(event, meta) {
console.log('Analytics event:', event);
},
onClose(actionId, meta) {
console.log('Onboarding closed:', actionId);
return true; // Allow the onboarding to close
},
onCustom(actionId, meta) {
console.log('Custom action:', actionId);
return false; // Don't close the onboarding
},
onPaywall(actionId, meta) {
console.log('Paywall action:', actionId);
view.dismiss().then(() => {
openPaywall(actionId);
});
},
onStateUpdated(action, meta) {
console.log('State updated:', action);
},
onFinishedLoading(meta) {
console.log('Onboarding finished loading');
},
onError(error) {
console.error('Onboarding error:', error);
},
});
await view.present();
} catch (error) {
console.error('Failed to present onboarding:', error);
}
```
## Các loại sự kiện \{#event-types\}
Các phần dưới đây mô tả các loại sự kiện khác nhau mà bạn có thể xử lý.
### Xử lý hành động tùy chỉnh \{#handle-custom-actions\}
Trong builder, bạn có thể thêm hành động **custom** vào một nút và gán cho nó một ID.
Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. Ví dụ: nếu người dùng nhấn vào một nút tùy chỉnh như **Login** hoặc **Allow notifications**, event handler sẽ được kích hoạt với tham số `actionId` trùng với **Action ID** trong builder. Bạn có thể tự tạo ID, ví dụ "allowNotifications".
```typescript showLineNumbers
view.setEventHandlers({
onCustom(actionId, meta) {
switch (actionId) {
case 'login':
console.log('Login action triggered');
break;
case 'allow_notifications':
console.log('Allow notifications action triggered');
break;
}
return false; // Don't close the onboarding
},
});
```
Ví dụ sự kiện (Nhấn để mở rộng)
```json
{
"actionId": "allow_notifications",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
}
}
```
### Hoàn tất tải onboarding \{#finishing-loading-onboarding\}
Khi onboarding hoàn tất việc tải, sự kiện này sẽ được kích hoạt:
```typescript showLineNumbers
view.setEventHandlers({
onFinishedLoading(meta) {
console.log('Onboarding loaded:', meta.onboardingId);
},
});
```
Ví dụ sự kiện (Nhấn để mở rộng)
```json
{
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "welcome_screen",
"screen_index": 0,
"total_screens": 4
}
}
```
### Đóng onboarding \{#closing-onboarding\}
Onboarding được coi là đã đóng khi người dùng nhấn vào nút có hành động **Close** được gán.
:::important
Lưu ý rằng bạn cần tự quản lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ, bạn cần dừng việc hiển thị onboarding đó.
:::
```typescript showLineNumbers
view.setEventHandlers({
onClose(actionId, meta) {
console.log('Onboarding closed:', actionId);
return true; // Allow the onboarding to close
},
});
```
Ví dụ sự kiện (Nhấn để mở rộng)
```json
{
"action_id": "close_button",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "final_screen",
"screen_index": 3,
"total_screens": 4
}
}
```
### Mở paywall \{#opening-a-paywall\}
:::tip
Xử lý sự kiện này để mở paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi onboarding đóng, có một cách đơn giản hơn — xử lý hành động đóng và mở paywall mà không cần dựa vào dữ liệu sự kiện.
:::
Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall.
Lưu ý rằng với iOS, chỉ có thể hiển thị một view (paywall hoặc onboarding) trên màn hình tại một thời điểm. Nếu bạn hiển thị paywall lên trên onboarding, bạn không thể điều khiển onboarding ở nền bằng lập trình. Việc cố gắng dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn dismiss view onboarding trước khi hiển thị paywall.
```typescript showLineNumbers
view.setEventHandlers({
onPaywall(actionId, meta) {
// Dismiss onboarding before presenting paywall
view.dismiss().then(() => {
openPaywall(actionId);
});
},
});
async function openPaywall(placementId: string) {
// Implement your paywall opening logic here
}
```
Ví dụ sự kiện (Nhấn để mở rộng)
```json
{
"action_id": "premium_offer_1",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "pricing_screen",
"screen_index": 2,
"total_screens": 4
}
}
```
### Theo dõi điều hướng \{#tracking-navigation\}
Bạn nhận được sự kiện analytics khi các sự kiện liên quan đến điều hướng xảy ra trong flow onboarding:
```typescript showLineNumbers
view.setEventHandlers({
onAnalytics(event, meta) {
console.log('Analytics event:', event.type, meta.onboardingId);
},
});
```
Đối tượng `event` có thể là một trong các loại sau:
| Loại | Mô tả |
|------------|-------------|
| `onboardingStarted` | Khi onboarding đã được tải |
| `screenPresented` | Khi bất kỳ màn hình nào được hiển thị |
| `screenCompleted` | Khi một màn hình hoàn tất. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn tất) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. |
| `secondScreenPresented` | Khi màn hình thứ hai được hiển thị |
| `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu |
| `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy [gán ID `final` cho màn hình cuối cùng](design-onboarding). |
| `unknown` | Cho bất kỳ loại sự kiện không xác định nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) |
Mỗi sự kiện bao gồm thông tin `meta` chứa:
| Trường | Mô tả |
|------------|-------------|
| `onboardingId` | Định danh duy nhất của flow onboarding |
| `screenClientId` | Định danh của màn hình hiện tại |
| `screenIndex` | Vị trí của màn hình hiện tại trong flow |
| `screensTotal` | Tổng số màn hình trong flow |
Ví dụ sự kiện (Nhấn để mở rộng)
```javascript
// onboardingStarted
{
"name": "onboarding_started",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "welcome_screen",
"screen_index": 0,
"total_screens": 4
}
}
// screenPresented
{
"name": "screen_presented",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "interests_screen",
"screen_index": 2,
"total_screens": 4
}
}
// screenCompleted
{
"name": "screen_completed",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 1,
"total_screens": 4
},
"params": {
"element_id": "profile_form",
"reply": "success"
}
}
// secondScreenPresented
{
"name": "second_screen_presented",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 1,
"total_screens": 4
}
}
// userEmailCollected
{
"name": "user_email_collected",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 1,
"total_screens": 4
}
}
// onboardingCompleted
{
"name": "onboarding_completed",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "final_screen",
"screen_index": 3,
"total_screens": 4
}
}
```
---
# File: capacitor-onboarding-input
---
---
title: "Xử lý dữ liệu từ onboardings trong Capacitor SDK"
description: "Lưu và sử dụng dữ liệu từ onboardings trong ứng dụng Capacitor với Adapty SDK."
---
Khi người dùng trả lời câu hỏi trong quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình.
Ví dụ:
```typescript
view.setEventHandlers({
onStateUpdated(action, meta) {
// Process data
},
});
```
Xem định dạng action [tại đây](https://capacitor.adapty.io/types/onboardingstateupdatedaction).
Ví dụ về dữ liệu đã lưu (định dạng có thể khác nhau tùy theo cách triển khai của bạn)
```javascript
// Example of a saved select action
{
"elementId": "preference_selector",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "preferences_screen",
"screenIndex": 1,
"screensTotal": 3
},
"params": {
"type": "select",
"value": {
"id": "option_1",
"value": "premium",
"label": "Premium Plan"
}
}
}
// Example of a saved multi-select action
{
"elementId": "interests_selector",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "interests_screen",
"screenIndex": 2,
"screensTotal": 3
},
"params": {
"type": "multiSelect",
"value": [
{
"id": "interest_1",
"value": "sports",
"label": "Sports"
},
{
"id": "interest_2",
"value": "music",
"label": "Music"
}
]
}
}
// Example of a saved input action
{
"elementId": "name_input",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
},
"params": {
"type": "input",
"value": {
"type": "text",
"value": "John Doe"
}
}
}
// Example of a saved date picker action
{
"elementId": "birthday_picker",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
},
"params": {
"type": "datePicker",
"value": {
"day": 15,
"month": 6,
"year": 1990
}
}
}
```
## Các trường hợp sử dụng \{#use-cases\}
### Bổ sung hồ sơ người dùng với dữ liệu \{#enrich-user-profiles-with-data\}
Nếu bạn muốn liên kết ngay dữ liệu nhập vào với hồ sơ người dùng và tránh hỏi họ lặp lại thông tin, bạn cần [cập nhật hồ sơ người dùng](capacitor-setting-user-attributes) với dữ liệu đó khi xử lý action.
Ví dụ: bạn yêu cầu người dùng nhập tên vào trường văn bản có ID là `name`, và bạn muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code ứng dụng, có thể trông như sau:
```typescript showLineNumbers
view.setEventHandlers({
onStateUpdated(action, meta) {
// Store user preferences or responses
if (action.elementType === 'input') {
const profileParams: any = {};
// Map elementId to appropriate profile field
switch (action.elementId) {
case 'name':
if (action.value.type === 'text') {
profileParams.firstName = action.value.value;
}
break;
case 'email':
if (action.value.type === 'email') {
profileParams.email = action.value.value;
}
break;
}
// Update profile if we have data to update
if (Object.keys(profileParams).length > 0) {
adapty.updateProfile({ params: profileParams }).catch((error) => {
// handle the error
});
}
}
},
});
```
### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\}
Sử dụng quiz trong onboardings, bạn cũng có thể tùy chỉnh các paywall hiển thị cho người dùng sau khi họ hoàn thành onboarding.
Ví dụ: bạn có thể hỏi người dùng về kinh nghiệm thể thao của họ và hiển thị các CTA và sản phẩm khác nhau cho từng nhóm người dùng.
1. [Thêm quiz](onboarding-quizzes) trong trình xây dựng onboarding và gán ID có ý nghĩa cho các tùy chọn.
2. Xử lý các câu trả lời quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](capacitor-setting-user-attributes) cho người dùng.
```typescript showLineNumbers
view.setEventHandlers({
onStateUpdated(action, meta) {
// Handle quiz responses and set custom attributes
if (action.elementType === 'select') {
const profileParams: any = {};
// Map quiz responses to custom attributes
switch (action.elementId) {
case 'experience':
// Set custom attribute 'experience' with the selected value (beginner, amateur, pro)
profileParams.codableCustomAttributes = {
experience: action.value.value
};
break;
}
// Update profile if we have data to update
if (Object.keys(profileParams).length > 0) {
adapty.updateProfile({ params: profileParams }).catch((error) => {
// handle the error
});
}
}
},
});
```
3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh.
4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho mỗi phân khúc bạn đã tạo.
5. [Hiển thị paywall](capacitor-paywalls) cho placement trong code ứng dụng của bạn. Nếu onboarding có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho action của nút đó](capacitor-handling-onboarding-events#opening-a-paywall).
---
# File: capacitor-best-practices
---
---
title: "Best practices in Capacitor SDK"
description: "Reference patterns for integrating Adapty SDK on Capacitor — call order, error handling, and other production-readiness rules."
---
---
# File: capacitor-sdk-call-order
---
---
title: "Thứ tự gọi trong Capacitor SDK"
description: "Tránh mất quyền truy cập premium, thiếu attribution, và lỗi #2002 không liên tục bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự."
---
`adapty.activate()` phải hoàn tất trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi nó resolve, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` sẽ thất bại với [`#2002 notActivated`](capacitor-handle-errors#custom-network-codes).
Nếu ứng dụng của bạn xác thực người dùng và bạn thu thập customer user ID sau khi khởi chạy, hãy gọi `adapty.identify()` vào thời điểm đó. Đừng gọi các phương thức liên quan đến hành động người dùng cho đến khi `identify` resolve. Các lệnh gọi chạy đua với nó sẽ thất bại với [`#3006 profileWasChanged`](capacitor-handle-errors#custom-network-codes), hoặc rơi vào hồ sơ người dùng ẩn danh được tạo lúc activation. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh.
Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo quy tắc tương tự. Khởi tạo chúng trước và chờ callback UID của chúng trước khi gọi `adapty.activate`. Nếu không, MMP ID sẽ rơi vào một hồ sơ người dùng ẩn danh tạm thời và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Để biết thêm về AppsFlyer, xem [AppsFlyer](appsflyer).
## Thứ tự đúng \{#the-correct-order\}
Con đường của bạn phụ thuộc vào hai điều: khi nào bạn biết customer user ID và liệu bạn có sử dụng SDK MMP hay analytics không.
- **Bước 2 và 5**: Bắt buộc cho mọi ứng dụng. Kích hoạt SDK, sau đó gọi các phương thức SDK.
- **Bước 1 và 3**: Chỉ bắt buộc nếu bạn tích hợp SDK MMP hoặc analytics (AppsFlyer, Adjust, Branch, PostHog).
- **Bước 4**: Chỉ bắt buộc nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy.
Nếu bạn có customer user ID ngay lúc khởi chạy ứng dụng, hãy truyền trực tiếp vào `activate()` (bước 2a). Con đường này không bao giờ tạo hồ sơ người dùng ẩn danh, vì vậy bước 4 là không cần thiết.
| Bước | Lệnh gọi | Khi nào | Ghi chú |
|------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
| 1 | Khởi tạo SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi chạy ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. |
| 2a | `adapty.activate({ apiKey: '...', params: { customerUserId: '...' } })` | Khởi chạy ứng dụng, sau bước 1, nếu bạn có customer user ID | Được khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. |
| 2b | `adapty.activate({ apiKey: '...' })` không có `customerUserId` | Khởi chạy ứng dụng, sau bước 1, nếu bạn chưa có customer user ID (hoặc không bao giờ thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. |
| 3 | `adapty.setIntegrationIdentifier({ key: '...', value: '...' })` cho mỗi MMP | Sau bước 2, trước bất kỳ lệnh gọi hành động người dùng nào | Bắt buộc để MMP ID rơi vào đúng hồ sơ người dùng. |
| 4 | `await adapty.identify({ customerUserId: 'YOUR_USER_ID' })` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên con đường 2b với xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong lúc `identify` tạo ra `#3006 profileWasChanged`. |
| 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; ngược lại sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. |
:::important
Bỏ qua các bước này gây ra mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về cho sai đối tượng.
:::
## Cài đặt từ web2app và web-funnel \{#web2app-and-web-funnel-installs\}
Nếu người dùng mua trên web checkout (Stripe, Paddle) và sau đó cài đặt ứng dụng native, `activate()` đầu tiên trên thiết bị sẽ tạo một hồ sơ người dùng ẩn danh mới. Hồ sơ người dùng này không được liên kết với hồ sơ người dùng trên web. Nếu bạn có thể xác định customer user ID trước khi khởi chạy ứng dụng (từ flow xác thực hoặc install referrer), hãy truyền trực tiếp vào `activate()`. Nếu không, giao dịch mua trên web sẽ không hiển thị trên thiết bị cho đến khi bạn gọi `identify({ customerUserId: 'YOUR_USER_ID' })` và sau đó là `restorePurchases`.
Để biết metadata cần gửi với mỗi web checkout, xem:
- [Stripe](stripe)
- [Paddle](paddle)
---
# File: capacitor-optimize-paywall-fetching
---
---
title: "Tối ưu hóa việc tải paywall trong Capacitor SDK"
description: "Tải paywall Adapty đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho Capacitor."
---
Một lần tải paywall đáng tin cậy trên Capacitor cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng mục tiêu, và có phương án dự phòng khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó.
:::tip
Các quy tắc này giả định `adapty.activate()` và `adapty.identify()` đã được giải quyết xong. Xem [Thứ tự gọi trong Capacitor SDK](capacitor-sdk-call-order).
:::
## Quy tắc và những lỗi cần tránh \{#rules-and-pitfalls\}
| Nên làm | Không nên làm | Lý do |
|---|---|---|
| Tải placement mà bạn sắp hiển thị. | Tải trước tất cả placement cùng lúc khi khởi động. | Tải trước hàng loạt sẽ chặn luồng chính và gây màn hình đen trong lúc xử lý. |
| Gọi `getPaywall` sau khi attribution đã có cơ hội xử lý — ví dụ, 1–2 giây sau `activate` hoặc sau khi listener `onLatestProfileLoad` kích hoạt. | Gọi `getPaywall` khi khởi động app trong `App.tsx`. | Attribution chưa được ghi nhận. Paywall sẽ được xử lý theo đối tượng mặc định và bỏ qua các phân khúc cũng như cá nhân hóa ASA một cách thầm lặng. |
| Đặt `loadTimeoutMs` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` vô thời hạn. | Nếu không có timeout, người dùng có kết nối kém sẽ thấy màn hình trắng cho đến khi mạng phục hồi — hoặc họ sẽ tắt app. |
Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-capacitor) để tham khảo các tham số `fetchPolicy` và `loadTimeoutMs`, và [Placements](placements) để chọn placement phù hợp.
## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\}
Đối với các thị trường có kết nối kém thường xuyên (vùng nông thôn, khi di chuyển, các khu vực bị ảnh hưởng bởi định tuyến):
- Đặt `fetchPolicy: 'return_cache_data_else_load'` cho mọi lần tải ngoại trừ lần đầu tiên.
- Cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement trong Adapty Dashboard.
- Đặt `loadTimeoutMs` từ 3000–5000 milliseconds và chấp nhận paywall dự phòng khi timeout kích hoạt.
- Đừng chặn việc hiển thị paywall bằng `adapty.getProfile()`. Gọi `getPaywall` độc lập để một profile chậm không cản trở giao diện.
---
# File: capacitor-show-aa-targeted-paywall
---
---
title: "Hiển thị paywall nhắm mục tiêu AA ngay lần khởi chạy đầu tiên trong Capacitor SDK"
description: "Hiển thị paywall ngay lập tức và nâng cấp cho người dùng Apple Ads khi attribution được áp dụng trong Capacitor, sử dụng AdaptyProfile.appliedAttributionSources."
---
Attribution của Apple Ads (AA) đến bất đồng bộ sau `adapty.activate()`. Ở lần khởi chạy đầu tiên, attribution thường chưa có, nên `getPaywall` sẽ xử lý theo đối tượng mặc định và người dùng Apple Ads bỏ lỡ paywall nhắm mục tiêu AA của bạn. Thay vì trì hoãn việc hiển thị paywall cho đến khi attribution đến, hãy hiển thị ngay một paywall và làm mới nó khi attribution AA được áp dụng — như vậy người dùng Apple Ads sẽ nhận được biến thể nhắm mục tiêu còn những người khác thấy paywall mà không phải chờ. `AdaptyProfile.appliedAttributionSources` cho bạn biết khi nào attribution AA đã được áp dụng.
## Trước khi bắt đầu \{#before-you-start\}
Bạn cần:
- Adapty Capacitor SDK **3.17.1** trở lên.
- Apple Ads được cấu hình cho ứng dụng trong Adapty. Xem [Apple Ads](apple-search-ads).
## Cách hoạt động \{#how-it-works\}
Sau `adapty.activate()`, SDK yêu cầu attribution Apple Ads từ Apple trong nền và chuyển kết quả đến backend của Adapty. Khi AA trở thành nguồn attribution đang hoạt động cho hồ sơ người dùng, SDK sẽ gửi một `AdaptyProfile` đã cập nhật đến listener `onLatestProfileLoad` của bạn, với `'apple_search_ads'` trong mảng `appliedAttributionSources`.
Điều này cho phép bạn tải paywall theo hai bước:
1. Gọi `getPaywall` ngay lập tức. Khi chưa có attribution nào được áp dụng, Adapty xử lý yêu cầu theo đối tượng mặc định, vì vậy người dùng thấy paywall ngay lập tức.
2. Khi `'apple_search_ads'` xuất hiện, gọi `getPaywall` lần nữa. Lúc này Adapty xử lý yêu cầu theo đối tượng Apple Ads và trả về paywall nhắm mục tiêu, thay thế paywall đầu tiên.
`appliedAttributionSources` có thể rỗng hoặc vắng mặt. Điều đó có nghĩa là:
- Attribution Apple Ads chưa được xử lý cho hồ sơ người dùng này, hoặc
- Không có attribution nào đến cả.
Dù thế nào, bước 1 vẫn an toàn — Adapty xử lý yêu cầu theo đối tượng phù hợp với trạng thái hồ sơ người dùng hiện tại, thường là đối tượng mặc định. Bước 2 chỉ chạy khi `'apple_search_ads'` xuất hiện.
:::important
Ở mọi lần khởi chạy tiếp theo, hồ sơ người dùng được cache đã có sẵn `'apple_search_ads'` trong `appliedAttributionSources`, nên `getPaywall` đầu tiên đã trả về paywall nhắm mục tiêu Apple Ads — không có lần fetch thứ hai hay thay đổi hiển thị nào. Flow hai bước chỉ quan trọng ở lần khởi chạy đầu tiên, khi attribution vẫn đang trong quá trình xử lý.
:::
## Triển khai \{#implementation\}
Hiển thị paywall ngay lập tức, sau đó lắng nghe `'apple_search_ads'` và làm mới paywall khi nó đến.
1. **Kích hoạt SDK.** Xem [Cài đặt & cấu hình Capacitor SDK](sdk-installation-capacitor).
2. **Tải và hiển thị paywall** với `getPaywall` như thường — đừng chờ attribution.
3. **Đăng ký nhận cập nhật hồ sơ người dùng** với `adapty.addListener('onLatestProfileLoad', …)` và theo dõi `'apple_search_ads'`. Khi nó xuất hiện, fetch lại paywall và hiển thị paywall đã cập nhật. Nếu bạn chưa thiết lập listener, xem [Lắng nghe cập nhật gói đăng ký](capacitor-check-subscription-status#listen-to-subscription-updates):
```typescript
const listener = await adapty.addListener('onLatestProfileLoad', async ({ profile }) => {
if (!profile.appliedAttributionSources?.includes('apple_search_ads')) return;
const targeted = await adapty.getPaywall({ placementId });
// present the targeted paywall in place of the first one
});
// Call listener.remove() after the upgrade, or after a timeout (see below).
```
4. **Dừng lắng nghe sau một khoảng thời gian chờ.** Hầu hết người dùng không bao giờ nhận được attribution Apple Ads, vì vậy hãy xóa listener sau một thời gian thay vì giữ nó mở suốt phiên. Cấu hình [paywall dự phòng](capacitor-use-fallback-paywalls) cho placement để người dùng luôn thấy gì đó nếu yêu cầu thất bại.
## Ví dụ đầy đủ \{#complete-example\}
`onAppleAdsAttribution` resolve khi attribution Apple Ads được áp dụng, hoặc reject sau `timeoutMs`. Cách dùng bên dưới tải paywall ngay lập tức, sau đó fetch lại khi attribution đến — người dùng Apple Ads nhận được paywall nhắm mục tiêu, và nếu attribution không bao giờ đến thì paywall đầu tiên vẫn được giữ nguyên:
```typescript
const APPLE_ADS_SOURCE = 'apple_search_ads';
const placementId = 'YOUR_PLACEMENT_ID';
function hasAppleAdsAttribution(profile: AdaptyProfile): boolean {
return profile.appliedAttributionSources?.includes(APPLE_ADS_SOURCE) ?? false;
}
/**
* Resolves once Apple Ads attribution is applied to the profile.
* Rejects with a timeout error if attribution never arrives within `timeoutMs`.
* Call after `adapty.activate()`.
*/
export function onAppleAdsAttribution(timeoutMs: number): Promise {
return new Promise((resolve, reject) => {
let timer: ReturnType | undefined;
let handle: { remove: () => void } | undefined;
const stop = () => {
clearTimeout(timer);
handle?.remove();
};
adapty
.addListener('onLatestProfileLoad', ({ profile }) => {
if (!hasAppleAdsAttribution(profile)) return;
stop();
resolve();
})
.then(listener => {
handle = listener;
});
timer = setTimeout(() => {
stop();
reject(new Error(`Apple Ads attribution timed out after ${timeoutMs}ms`));
}, timeoutMs);
});
}
let paywall = await adapty.getPaywall({ placementId });
onAppleAdsAttribution(30_000)
.then(() => adapty.getPaywall({ placementId }))
.then(updated => {
paywall = updated;
})
.catch(() => {
console.log('Apple Ads attribution or loading failed');
});
```
Ở lần khởi chạy đầu tiên, người dùng Apple Ads sẽ thấy paywall mặc định trong giây lát trước khi nó được thay thế. Nếu bạn hiển thị paywall bằng Paywall Builder, hãy cân nhắc xem việc hiển thị lại có chấp nhận được không, hoặc chỉ áp dụng nâng cấp trước khi paywall được hiển thị. Điều chỉnh `timeoutMs` theo thời gian bạn sẵn lòng tiếp tục lắng nghe — attribution đang đến thường xuất hiện trong vài giây kể từ khi khởi chạy.
Nếu ứng dụng của bạn đã lắng nghe `onLatestProfileLoad` cho các mục đích khác (ví dụ: [kiểm tra trạng thái gói đăng ký](capacitor-check-subscription-status#listen-to-subscription-updates)), bạn không cần thay đổi gì. `adapty.addListener` hỗ trợ nhiều listener độc lập, vì vậy đoạn code này thêm listener riêng của nó mà không ảnh hưởng đến những cái khác.
---
# File: capacitor-test
---
---
title: "Test & release in Capacitor SDK"
description: "Learn how to test and release your Capacitor app with Adapty SDK."
---
If you've already implemented the Adapty SDK in your Capacitor app, you'll want to test that everything is set up correctly and that purchases work as expected across both iOS and Android platforms. This involves testing both the SDK integration and the actual purchase flow with Apple's sandbox environment and Google Play's testing environment.
## Test your app
For comprehensive testing of your in-app purchases, see our platform-specific testing guides: [iOS testing guide](test-purchases-in-sandbox) and [Android testing guide](testing-on-android).
## Prepare for release
Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm:
- Store connection and server notifications are configured
- Purchases complete and are reported to Adapty
- Access unlocks and restores correctly
- Privacy and review requirements are met
---
# File: kids-mode-capacitor
---
---
title: "Chế độ Trẻ em trong Capacitor SDK"
description: "Dễ dàng bật Chế độ Trẻ em để tuân thủ chính sách của Apple và Google. Không thu thập IDFA, GAID hay dữ liệu quảng cáo trong Capacitor SDK."
---
Nếu ứng dụng Capacitor của bạn dành cho trẻ em, bạn phải tuân thủ chính sách của [Apple](https://developer.apple.com/kids/) và [Google](https://support.google.com/googleplay/android-developer/answer/9893335). Nếu bạn đang sử dụng Adapty SDK, chỉ cần vài bước đơn giản là có thể cấu hình SDK để đáp ứng các chính sách này và vượt qua quá trình xét duyệt trên cửa hàng ứng dụng.
## Cần làm gì? \{#whats-required\}
Bạn cần cấu hình Adapty SDK để tắt việc thu thập:
- [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS)
- [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android)
- [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf)
Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách cẩn thận. ID người dùng theo định dạng `` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, cũng như việc sử dụng email. Đối với Chế độ Trẻ em, cách tốt nhất là sử dụng các định danh được tạo ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã hash hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ.
## Bật Chế độ Trẻ em \{#enabling-kids-mode\}
### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\}
Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để làm điều này, hãy vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** trong phần **Collect users' IP address**.
### Cập nhật trong code ứng dụng di động \{#updates-in-your-mobile-app-code\}
Để tuân thủ các chính sách, hãy tắt việc thu thập IDFA, GAID và địa chỉ IP của người dùng:
```typescript showLineNumbers
try {
await adapty.activate({
apiKey: 'YOUR_PUBLIC_SDK_KEY',
params: {
// Disable IP address collection
ipAddressCollectionDisabled: true,
// Disable IDFA collection on iOS
ios: {
idfaCollectionDisabled: true
},
// Disable Google Advertising ID collection on Android
android: {
adIdCollectionDisabled: true
}
}
});
console.log('Adapty activated with Kids Mode enabled');
} catch (error) {
console.error('Failed to activate Adapty with Kids Mode:', error);
}
```
### Cấu hình theo từng nền tảng \{#platform-specific-configurations\}
#### iOS: Bật Chế độ Trẻ em bằng CocoaPods \{#ios-enable-kids-mode-using-cocoapods\}
Nếu bạn đang dùng CocoaPods cho iOS, bạn cũng có thể bật Chế độ Trẻ em ở cấp độ native:
1. Cập nhật Podfile của bạn:
- Nếu bạn **chưa có** phần `post_install`, hãy thêm toàn bộ đoạn code bên dưới.
- Nếu bạn **đã có** phần `post_install`, hãy merge các dòng được highlight vào đó.
```ruby showLineNumbers title="Podfile"
def adapty_enable_kids_mode(installer)
installer.pods_project.targets.each do |target|
next unless target.name == 'Adapty'
target.build_configurations.each do |config|
flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'
flags = flags.join(' ') if flags.is_a?(Array)
config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE"
end
target.frameworks_build_phase.files.dup.each do |bf|
target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport')
end
end
installer.pods_project.save
Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc|
File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, ''))
end
end
post_install do |installer|
# ... keep your existing post_install body (Flutter adds one automatically) ...
adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode
end
```
2. Chạy lệnh sau để áp dụng các thay đổi:
```sh showLineNumbers title="Shell"
pod install
```
#### Android: Bật Chế độ Trẻ em bằng Gradle \{#android-enable-kids-mode-using-gradle\}
Đối với Android, bạn cũng có thể bật Chế độ Trẻ em ở cấp độ native bằng cách thêm nội dung sau vào `build.gradle` của ứng dụng:
```groovy showLineNumbers title="android/app/build.gradle"
android {
defaultConfig {
// ... existing config ...
// Enable Kids Mode
buildConfigField "boolean", "ADAPTY_KIDS_MODE", "true"
}
}
```
## Các bước tiếp theo \{#next-steps\}
Sau khi bật Chế độ Trẻ em, hãy đảm bảo:
1. Kiểm thử ứng dụng kỹ lưỡng để đảm bảo tất cả các tính năng hoạt động đúng
2. Xem lại chính sách quyền riêng tư của ứng dụng để phản ánh việc đã tắt thu thập dữ liệu
3. Nộp ứng dụng để xét duyệt kèm theo tài liệu rõ ràng về việc tuân thủ Chế độ Trẻ em
Để biết thêm thông tin về yêu cầu theo từng nền tảng:
- [Chế độ Trẻ em trong iOS SDK](kids-mode) để biết thêm chi tiết cấu hình iOS
- [Chế độ Trẻ em trong Android SDK](kids-mode-android) để biết thêm chi tiết cấu hình Android
---
# File: capacitor-reference
---
---
title: "Reference"
description: "Reference documentation for Adapty Capacitor SDK."
---
This page contains reference documentation for Adapty Capacitor SDK. Choose the topic you need:
- **[SDK models](https://capacitor.adapty.io/)** - Data models and structures used by the SDK
- **[Handle errors](capacitor-handle-errors)** - Error handling and troubleshooting
---
# File: capacitor-handle-errors
---
---
title: "Xử lý lỗi trong Capacitor SDK"
description: "Xử lý lỗi trong Capacitor SDK."
---
Mọi lỗi được SDK trả về đều là một instance của `AdaptyError`. Dưới đây là ví dụ:
:::tip
**Bật verbose logs trước khi debug.** Hầu hết các `AdaptyError` đều bọc một lỗi StoreKit, Play Billing, mạng hoặc backend bên trong. Khi bật verbose logs (`adapty.setLogLevel({ logLevel: 'verbose' })` — xem [Logging](sdk-installation-capacitor#logging)), lỗi bên trong đó sẽ được in ra console, giúp bạn xác định nguyên nhân thực sự. Thuộc tính `detail` trên `AdaptyError` luôn được điền dữ liệu bất kể mức log — verbose logs chỉ giúp hiển thị nó trên console.
:::
```typescript showLineNumbers
try {
const result = await adapty.makePurchase({ product });
// Handle purchase result
if (result.type === 'success') {
console.log('Purchase successful:', result.profile);
} else if (result.type === 'user_cancelled') {
console.log('User cancelled the purchase');
} else if (result.type === 'pending') {
console.log('Purchase is pending');
}
} catch (error) {
if (error instanceof AdaptyError) {
console.error('Adapty error:', error.adaptyCode, error.localizedDescription);
// Handle specific error codes
switch (error.adaptyCode) {
case ErrorCodeName.cantMakePayments:
console.log('In-app purchases are not allowed on this device');
break;
case ErrorCodeName.notActivated:
console.log('Adapty SDK is not activated');
break;
case ErrorCodeName.productPurchaseFailed:
console.log('Purchase failed:', error.detail);
break;
default:
console.log('Other error occurred:', error.detail);
}
} else {
console.error('Non-Adapty error:', error);
}
}
```
## Thuộc tính của lỗi \{#error-properties\}
Lớp `AdaptyError` cung cấp các thuộc tính sau:
| Thuộc tính | Kiểu | Mô tả |
|----------|------|-------------|
| `adaptyCode` | `number` | Mã lỗi dạng số (ví dụ: `1003` cho cantMakePayments) |
| `localizedDescription` | `string` | Thông báo lỗi thân thiện với người dùng |
| `detail` | `string \| undefined` | Thông tin chi tiết bổ sung về lỗi (tùy chọn) |
| `message` | `string` | Thông báo lỗi đầy đủ bao gồm mã và mô tả |
## Mã lỗi \{#error-codes\}
SDK xuất các hằng số và tiện ích để làm việc với mã lỗi:
### Hằng số ErrorCodeName \{#errorcodename-constant\}
Ánh xạ các định danh chuỗi sang mã số:
```typescript
ErrorCodeName.cantMakePayments // 1003
ErrorCodeName.notActivated // 2002
ErrorCodeName.networkFailed // 2005
```
### Hằng số ErrorCode \{#errorcode-constant\}
Ánh xạ mã số sang định danh chuỗi:
```typescript
ErrorCode[1003] // 'cantMakePayments'
ErrorCode[2002] // 'notActivated'
ErrorCode[2005] // 'networkFailed'
```
### Các hàm hỗ trợ \{#helper-functions\}
```typescript
// Get numeric code from string name:
getErrorCode('cantMakePayments') // 1003
// Get string name from numeric code:
getErrorPrompt(1003) // 'cantMakePayments'
```
### So sánh mã lỗi \{#comparing-error-codes\}
**Lưu ý quan trọng:** `error.adaptyCode` là một **số**, vì vậy hãy so sánh trực tiếp với mã số:
```typescript
// Option 1: Use ErrorCodeName constant (recommended) ✅
if (error.adaptyCode === ErrorCodeName.cantMakePayments) {
console.log('Cannot make payments');
}
// Option 2: Compare with numeric literal ✅
if (error.adaptyCode === 1003) {
console.log('Cannot make payments');
}
// NOT like this ❌ - compares number to string and will never match
if (error.adaptyCode === ErrorCode[1003]) {
}
```
## Xử lý lỗi toàn cục \{#global-error-handler\}
Bạn có thể thiết lập một trình xử lý lỗi toàn cục để bắt tất cả các lỗi Adapty:
```typescript showLineNumbers
// Set up global error handler
AdaptyError.onError = (error: AdaptyError) => {
console.error('Global Adapty error:', {
code: error.adaptyCode,
message: error.localizedDescription,
detail: error.detail
});
// Handle specific error types globally
if (error.adaptyCode === ErrorCodeName.notActivated) {
// SDK not activated - maybe retry activation
console.log('SDK not activated, attempting to reactivate...');
}
};
```
## Các mẫu xử lý lỗi phổ biến \{#common-error-handling-patterns\}
### Xử lý lỗi mua hàng \{#handle-purchase-errors\}
```typescript showLineNumbers
async function handlePurchase(product: AdaptyPaywallProduct) {
try {
const result = await adapty.makePurchase({ product });
if (result.type === 'success') {
console.log('Purchase successful:', result.profile);
} else if (result.type === 'user_cancelled') {
console.log('User cancelled the purchase');
} else if (result.type === 'pending') {
console.log('Purchase is pending');
}
} catch (error) {
if (error instanceof AdaptyError) {
switch (error.adaptyCode) {
case ErrorCodeName.cantMakePayments:
console.log('In-app purchases not allowed');
break;
case ErrorCodeName.productPurchaseFailed:
console.log('Purchase failed:', error.detail);
break;
default:
console.error('Purchase error:', error.localizedDescription);
}
}
}
}
```
### Xử lý lỗi mạng \{#handle-network-errors\}
```typescript showLineNumbers
async function fetchPaywall(placementId: string) {
try {
const paywall = await adapty.getPaywall({ placementId });
return paywall;
} catch (error) {
if (error instanceof AdaptyError) {
switch (error.adaptyCode) {
case ErrorCodeName.networkFailed:
console.log('Network error, retrying...');
// Implement retry logic
break;
case ErrorCodeName.serverError:
console.log('Server error:', error.detail);
break;
case ErrorCodeName.notActivated:
console.log('SDK not activated');
break;
default:
console.error('Paywall fetch error:', error.localizedDescription);
}
}
throw error;
}
}
```
## System StoreKit codes
| Error | Code | Description |
|-----|----|-----------|
| unknown | 0 | This error indicates that an unknown or unexpected error occurred. |
| clientInvalid | 1 | This error code indicates that the client is not allowed to perform the attempted action. |
| paymentCancelled | 2 | This error code indicates that the user canceled a payment request.
No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.
|
| paymentInvalid | 3 | This error indicates that one of the payment parameters was not recognized by the store. |
| paymentNotAllowed | 4 | This error code indicates that the user is not allowed to authorize payments. Possible reasons:
- Payments are not supported in the user's country.
- The user is a minor.
|
| storeProductNotAvailable | 5 | This error code indicates that the requested product is absent from the App Store. Make sure the product is available for the used country. |
| cloudServicePermissionDenied | 6 | This error code indicates that the user has not allowed access to Cloud service information. |
| cloudServiceNetworkConnectionFailed | 7 | This error code indicates that the device could not connect to the network. |
| cloudServiceRevoked | 8 | This error code indicates that the user has revoked permission to use this cloud service. |
| privacyAcknowledgementRequired | 9 | This error code indicates that the user has not yet acknowledged the store privacy policy. |
| unauthorizedRequestData | 10 | This error code indicates that the request is built incorrectly. |
| invalidOfferIdentifier | 11 | The offer identifier is not valid. Possible reasons:
- You have not set up an offer with that identifier in the App Store.
- You have revoked the offer.
- You misprinted the offer ID.
|
| invalidSignature | 12 | This error code indicates that the signature in a payment discount is not valid. Make sure you've filled out the **In-app purchase Key ID** field and uploaded the **In-App Purchase Private Key** file. Refer to the [Configure App Store integration](app-store-connection-configuration) topic for details. |
| missingOfferParams | 13 | This error indicates issues with Adapty integration or with offers.
Refer to the [Configure App Store integration](app-store-connection-configuration) and to [Offers](offers) for details on how to set them up.
|
| invalidOfferPrice | 14 | This error code indicates that the price you specified in the store is no longer valid. Offers must always represent a discounted price. |
## Custom Android codes
| Error | Code | Description |
|-----|----|-----------|
| adaptyNotInitialized | 20 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native]( sdk-installation-reactnative). |
| productNotFound | 22 | This error indicates that the product requested for purchase is not available in the store. |
| invalidJson | 23 | The paywall JSON is not valid. Fix it in the Adapty Dashboard. Refer to the [Customize paywall with remote config](customize-paywall-with-remote-config) topic for details on how to fix it. |
| currentSubscriptionToUpdateNotFoundInHistory | 24 | The original subscription that needs to be renewed is not found. |
| pendingPurchase | 25 | This error indicates that the purchase state is pending rather than purchased. Refer to the [Handling pending transactions](https://developer.android.com/google/play/billing/integrate#pending) page in the Android Developer docs for details. |
| billingServiceTimeout | 97 | This error indicates that the request has reached the maximum timeout before Google Play can respond. This could be caused, for example, by a delay in the execution of the action requested by the Play Billing Library call. |
| featureNotSupported | 98 | The requested feature is not supported by the Play Store on the current device. |
| billingServiceDisconnected | 99 | This fatal error indicates that the client app’s connection to the Google Play Store service via the `BillingClient` has been severed. |
| billingServiceUnavailable | 102 | This transient error indicates the Google Play Billing service is currently unavailable. In most cases, this means there is a network connection issue anywhere between the client device and Google Play Billing services. |
| billingUnavailable | 103 | This error indicates that a user billing error occurred during the purchase process. Examples of when this can occur include:
1\. The Play Store app on the user's device is out of date.
2. The user is in an unsupported country.
3. The user is an enterprise user, and their enterprise admin has disabled users from making purchases.
4. Google Play is unable to charge the user’s payment method. For example, the user's credit card might have expired.
5. The user is not logged into the Play Store app.
|
| developerError | 105 | This is a fatal error that indicates you're improperly using an API. |
| billingError | 106 | This is a fatal error that indicates an internal problem with Google Play itself. |
| itemAlreadyOwned | 107 | The consumable product has already been purchased. |
| itemNotOwned | 108 | This error indicates that the requested action on the item failed sin |
## Custom StoreKit codes
| Error | Code | Description |
|-----|----|-----------|
| noProductIDsFound | 1000 | This error indicates that none of the products in the paywall is available in the store.
If you are encountering this error, please follow the steps below to resolve it:
1. Check if all the products have been added to Adapty Dashboard.
2. Ensure that the Bundle ID of your app matches the one from the Apple Connect.
3. Verify that the product identifiers from the app stores match with the ones you have added to the Dashboard. Please note that the identifiers should not contain Bundle ID, unless it is already included in the store.
4. Confirm that the app paid status is active in your Apple tax settings. Ensure that your tax information is up-to-date and your certificates are valid.
5. Check if a bank account is attached to the app, so it can be eligible for monetization.
6. Check if the products are available in all regions.Also, ensure that your products are in **“Ready to Submit”** state.
|
| productRequestFailed | 1002 | Unable to fetch available products at the moment. Possible reason:
- No cache was yet created and no internet connection at the same time.
|
| cantMakePayments | 1003 | In-App purchases are not allowed on this device. |
| noPurchasesToRestore | 1004 | This error indicates that Google Play did not find the purchase to restore. |
| cantReadReceipt | 1005 | There is no valid receipt available on the device. This can be an issue during sandbox testing.
No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.
|
| productPurchaseFailed | 1006 | Product purchase failed. This wraps an underlying StoreKit error — read the wrapped error (or enable verbose logs to see it in the console) for the actual reason. The wrapped error is typically one of the StoreKit codes 0–14 in the table above — most commonly `paymentCancelled`, `paymentInvalid`, `paymentNotAllowed`, or `invalidOfferPrice`. If you can't identify a specific reason, try a new [sandbox profile](test-purchases-in-sandbox); if it still fails, contact Apple support. |
| refreshReceiptFailed | 1010 | This error indicates that the receipt was not received. Applicable to StoreKit 1 only. |
| receiveRestoredTransactionsFailed | 1011 | Purchase restoration failed. |
## Custom network codes
| Error | Code | Description |
| :------------------- | :--- | :----------------------------------------------------------- |
| notActivated | 2002 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native](sdk-installation-reactnative). |
| badRequest | 2003 | Bad request. |
| serverError | 2004 | Server error. |
| networkFailed | 2005 | The network request failed. |
| decodingFailed | 2006 | This error indicates that response decoding failed. |
| encodingFailed | 2009 | This error indicates that request encoding failed. |
| analyticsDisabled | 3000 | We can't handle analytics events, since you've opted it out. Refer to the [Analytics integration](analytics-integration) topic for details. |
| wrongParam | 3001 | This error indicates that some of your parameters are not correct: blank when it cannot be blank or wrong type, etc. |
| activateOnceError | 3005 | It is not possible to call `.activate` method more than once. |
| profileWasChanged | 3006 | The user profile was changed during the operation. |
| fetchTimeoutError | 3101 | This error means that the paywall could not be fetched within the set limit. To avoid this situation, [set up local fallbacks](fetch-paywalls-and-products). |
| operationInterrupted | 9000 | This operation was interrupted by the system. |
---
# File: capacitor-sdk-migration-guides
---
---
title: "Capacitor SDK Migration Guides"
description: "Migration guides for Adapty Capacitor SDK versions."
---
This page contains all migration guides for Adapty Capacitor SDK. Choose the version you want to migrate to for detailed instructions:
- [**Migrate to v3.16**](migration-to-capacitor-316)
---
# File: migration-to-capacitor-316
---
---
title: "Migrate Adapty Capacitor SDK to v3.16"
description: "Migrate sang Adapty Capacitor SDK v3.16 để có hiệu suất tốt hơn và các tính năng kiếm tiền mới."
---
Bắt đầu từ Adapty SDK v3.16.0, Capacitor 8 là bắt buộc. Nếu bạn cần Capacitor 7, hãy dùng Adapty SDK v3.15.
Để nâng cấp lên Capacitor SDK v3.16, hãy đảm bảo dự án của bạn đang sử dụng Capacitor 8. Nếu bạn vẫn đang dùng Capacitor 7, bạn có hai lựa chọn:
1. **Nâng cấp lên Capacitor 8**: Làm theo [hướng dẫn migration chính thức của Capacitor](https://capacitorjs.com/docs/updating/8-0) để cập nhật dự án, sau đó cài đặt Adapty SDK v3.16.
2. **Tiếp tục dùng Adapty SDK v3.15**: Nếu việc nâng cấp lên Capacitor 8 chưa khả thi, hãy tiếp tục sử dụng Adapty SDK v3.15, phiên bản này hỗ trợ Capacitor 7.
---
# End of Documentation
_Generated on: 2026-06-24T14:36:38.682Z_
_Successfully processed: 43/43 files_