---
title: "Hiển thị flows & paywalls - React Native"
description: "Hiển thị flows và paywalls cho người dùng trong ứng dụng React Native của bạn với Adapty."
---

<MethodPromo method="getFlow" label="Hiển thị flows và paywalls" />
Nếu bạn đã tạo một flow hoặc paywall trong Flow Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow đó đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị.

Trước khi bắt đầu, hãy đảm bảo rằng:

1. Bạn đã [tạo một flow hoặc paywall](create-paywall).
2. Bạn đã thêm nó vào một [placement](placements).
3. Bạn đã [tải flow và chuẩn bị view](react-native-get-pb-paywalls).

:::warning
Hướng dẫn này chỉ dành cho **flow và paywall sử dụng Paywall Builder**, yêu cầu SDK v4.0 trở lên. Quy trình hiển thị flow khác với paywall remote config.

- Để hiển thị **paywall remote config**, xem [Hiển thị paywall thiết kế bằng remote config](present-remote-config-paywalls).

:::

Adapty React Native SDK cung cấp hai cách để hiển thị flow và paywall:

- **React component**: Component nhúng cho phép tích hợp vào kiến trúc và hệ thống điều hướng của ứng dụng.

- **Modal presentation**
## Component React \{#react-component\}

Để nhúng một flow vào cây component hiện có của bạn, hãy sử dụng component `AdaptyFlowView` trực tiếp trong phân cấp component React Native. Component nhúng này cho phép bạn tích hợp nó vào kiến trúc và hệ thống điều hướng của ứng dụng.
:::tip
Component `AdaptyFlowView` tạo view khi render, tức là khi cấu hình và hình ảnh được tải xuống. Để tải trước, hãy gọi [`createFlowView`](react-native-get-pb-paywalls#fetch-the-view-configuration) cho cùng một flow ở phần trước của ứng dụng. Component sẽ tái sử dụng dữ liệu đã cache và render mà không cần chờ tải xuống.
:::
```typescript showLineNumbers title="React Native (TSX)"

function MyFlow({ flow }) {
  const flowParams = useMemo(() => ({
    loadTimeoutMs: 3000,
  }), []);

  const onCloseButtonPress = useCallback<FlowEventHandlers['onCloseButtonPress']>(() => {}, []);
  const onProductSelected = useCallback<FlowEventHandlers['onProductSelected']>((productId) => {}, []);
  const onPurchaseStarted = useCallback<FlowEventHandlers['onPurchaseStarted']>((product) => {}, []);
  const onPurchaseCompleted = useCallback<FlowEventHandlers['onPurchaseCompleted']>((purchaseResult, product) => {}, []);
  const onPurchaseFailed = useCallback<FlowEventHandlers['onPurchaseFailed']>((error, product) => {}, []);
  const onRestoreStarted = useCallback<FlowEventHandlers['onRestoreStarted']>(() => {}, []);
  const onRestoreCompleted = useCallback<FlowEventHandlers['onRestoreCompleted']>((profile) => {}, []);
  const onRestoreFailed = useCallback<FlowEventHandlers['onRestoreFailed']>((error) => {}, []);
  const onAppeared = useCallback<FlowEventHandlers['onAppeared']>(() => {}, []);
  const onError = useCallback<FlowEventHandlers['onError']>((error) => {}, []);
  const onLoadingProductsFailed = useCallback<FlowEventHandlers['onLoadingProductsFailed']>((error) => {}, []);
  const onUrlPress = useCallback<FlowEventHandlers['onUrlPress']>((url) => {}, []);
  const onCustomAction = useCallback<FlowEventHandlers['onCustomAction']>((actionId) => {}, []);
  const onWebPaymentNavigationFinished = useCallback<FlowEventHandlers['onWebPaymentNavigationFinished']>(() => {}, []);

  return (
    <AdaptyFlowView
      flow={flow}
      params={flowParams}
      style={styles.flow}
      onCloseButtonPress={onCloseButtonPress}
      onProductSelected={onProductSelected}
      onPurchaseStarted={onPurchaseStarted}
      onPurchaseCompleted={onPurchaseCompleted}
      onPurchaseFailed={onPurchaseFailed}
      onRestoreStarted={onRestoreStarted}
      onRestoreCompleted={onRestoreCompleted}
      onRestoreFailed={onRestoreFailed}
      onAppeared={onAppeared}
      onError={onError}
      onLoadingProductsFailed={onLoadingProductsFailed}
      onCustomAction={onCustomAction}
      onUrlPress={onUrlPress}
      onWebPaymentNavigationFinished={onWebPaymentNavigationFinished}
    />
  );
}
```

## Trình bày dạng modal \{#modal-presentation\}

Để hiển thị một flow dưới dạng màn hình độc lập, hãy dùng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createFlowView`](react-native-get-pb-paywalls#fetch-the-view-configuration). Mỗi `view` chỉ có thể dùng một lần. Nếu bạn cần hiển thị flow lại, hãy gọi `createFlowView` thêm một lần nữa để tạo một `view` mới.

:::warning
Không được tái sử dụng cùng một `view` mà không tạo lại. Điều này sẽ dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`.
:::
```typescript showLineNumbers title="React Native (TSX)"

const view = await createFlowView(flow);

// Optional: handle flow events (close, purchase, restore, etc)
// view.setEventHandlers({ ... });

try {
  await view.present();
} catch (error) {
  // handle the error
}
```

:::important
Gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã thiết lập trước đó cho những sự kiện cụ thể đó.
:::
### Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\}

Cấu hình cách flow được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`.

```typescript showLineNumbers
try {
  await view.present({ iosPresentationStyle: 'page_sheet' });
} catch (error) {
  // handle the error
}
```
## Sử dụng timer do developer tự định nghĩa \{#use-developer-defined-timer\}

Để sử dụng timer do developer tự định nghĩa trong ứng dụng mobile, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer mà bạn đã thiết lập trong Adapty dashboard. Điều này đảm bảo ứng dụng của bạn tự động cập nhật timer với giá trị chính xác — ví dụ như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, chẳng hạn như Ngày đầu năm mới, trừ đi thời gian hiện tại).
<Tabs>
<TabItem value="component" label="React component">
```typescript showLineNumbers title="React Native (TSX)"
const flowParams = {
  customTimers: { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) }
};

<AdaptyFlowView
  flow={flow}
  params={flowParams}
  // ... your event handlers
/>
```
</TabItem>
<TabItem value="modal" label="Modal presentation">
```typescript showLineNumbers title="React Native (TSX)"
const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) };

const view = await createFlowView(flow, { customTimers });
```
</TabItem>
</Tabs>
Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của bộ đếm thời gian do developer định nghĩa mà bạn đã thiết lập trong Adapty dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động bộ đếm với giá trị chính xác—chẳng hạn như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của bộ đếm, ví dụ như Năm Mới, trừ đi thời gian hiện tại).
## Hiển thị hộp thoại \{#show-dialog\}

Sử dụng phương thức này thay cho các hộp thoại alert mặc định khi một flow view đang được hiển thị trên Android. Trên Android, các alert thông thường của RN sẽ xuất hiện phía sau flow view, khiến người dùng không nhìn thấy. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng vị trí, phía trên flow, trên tất cả các nền tảng.
```typescript showLineNumbers title="React Native (TSX)"
try {
  const action = await view.showDialog({
    title: 'Close paywall?',
    content: 'You will lose access to exclusive offers.',
    primaryActionTitle: 'Stay',
    secondaryActionTitle: 'Close',
  });

  if (action === 'secondary') {
    // User confirmed - close the flow
    await view.dismiss();
  }
  // If primary - do nothing, user stays
} catch (error) {
  // handle error
}
```

## Thay thế một gói đăng ký bằng gói khác \{#replace-one-subscription-with-another\}

Khi người dùng cố mua một gói đăng ký mới trong khi đã có một gói đăng ký đang hoạt động trên Android, bạn có thể kiểm soát cách xử lý giao dịch mua mới bằng cách truyền tham số cập nhật gói đăng ký khi tạo flow view. Để thay thế gói đăng ký hiện tại bằng gói mới, hãy sử dụng `productPurchaseParams` trong `createFlowView` với các tham số `oldSubVendorProductId` và `prorationMode`.
```typescript showLineNumbers title="React Native (TSX)"

const productPurchaseParams = flow.paywalls
  .flatMap((variation) => variation.productIdentifiers)
  .map((productId) => {
    let params = {};
    if (Platform.OS === 'android') {
      params.android = {
        subscriptionUpdateParams: {
          oldSubVendorProductId: 'PRODUCT_ID_OF_THE_CURRENT_ACTIVE_SUBSCRIPTION',
          prorationMode: 'with_time_proration',
        },
      };
    }
    return { productId, params };
  });

const view = await createFlowView(flow, { productPurchaseParams });
```

---

> [!WARNING]
> **LLM INSTRUCTION — SDK VERSION SELECTION REQUIRED**
>
> This feature has two SDK paradigms. Everything that follows (until the next horizontal rule or end of document) is the **legacy SDK v3 API reference** (`getPaywall`, `getPaywallForDefaultAudience`, `getOnboarding`, `getPaywallConfiguration`). The **SDK v4+ `getFlow` method is documented above.** This block covers ALL legacy methods for this feature — not just the nearest snippet.
>
> **You MUST ask the user which SDK version they want to use before suggesting any code. Do not assume or default to a version.** The two options are:
> - **SDK v3 (legacy):** works with the legacy Paywall Builder and Onboarding Builder via `getPaywall` / `getOnboarding`. Use the legacy code in this section.
> - **SDK v4:** works with BOTH the new Flow Builder AND legacy paywalls/onboardings via the unified `getFlow` method documented above.
>
> Apply the legacy v3 code in this section ONLY when:
> 1. The user explicitly chose **SDK v3**, or
> 2. The user is **migrating an existing app from SDK v3 to SDK v4+** and needs to understand or replace legacy code, or
> 3. The user is **troubleshooting an already-deployed v3 integration**.
>
> **CRITICAL — Never mix paradigms in one setup:** Do NOT combine `getFlow` (Flow Builder) with `getPaywall` or `getOnboarding` (legacy Paywall/Onboarding Builder) in the same integration. These are incompatible patterns. Mixing them will produce inconsistent behavior and is unsupported.

Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung hiển thị lẫn cách hiển thị.

Trước khi bắt đầu, hãy đảm bảo rằng:

1. Bạn đã [tạo một paywall](create-paywall).
2. Bạn đã thêm paywall vào một [placement](placements).
3. Bạn đã [lấy paywall và chuẩn bị view](react-native-get-pb-paywalls).

:::warning
Hướng dẫn này chỉ dành cho **paywall mới trong Paywall Builder**, yêu cầu SDK v3.0 trở lên. Quy trình hiển thị paywall khác nhau tùy theo phiên bản Paywall Builder được sử dụng và paywall sử dụng Remote Config.

- Để hiển thị **paywall sử dụng Remote Config**, xem [Hiển thị paywall được thiết kế bằng Remote Config](present-remote-config-paywalls).

:::

Adapty React Native SDK cung cấp hai cách để hiển thị paywall:

- **React component**: Component được nhúng vào cho phép bạn tích hợp nó vào kiến trúc và hệ thống điều hướng của ứng dụng.

- **Modal presentation**
## React component \{#react-component\}

:::note
Cách tiếp cận **React component** yêu cầu SDK phiên bản 3.14.0 trở lên.
:::

Để nhúng một paywall vào cây component hiện có của bạn, hãy sử dụng component `AdaptyPaywallView` trực tiếp trong hệ thống phân cấp component React Native. Component nhúng này cho phép bạn tích hợp nó vào kiến trúc và hệ thống điều hướng của ứng dụng.
:::note
Trên Android, nếu paywall không kéo dài ra phía sau thanh trạng thái, một lớp phủ trực quan có thể xuất hiện ở phía trên. Chúng tôi khuyến nghị bạn tắt tính năng này cho các paywall của mình. Xem [Lớp phủ trực quan ở đầu paywall (Android)](#visual-overlay-at-the-top-of-the-paywall-android).
:::
```typescript showLineNumbers title="React Native (TSX)"

function MyPaywall({ paywall }) {
  const paywallParams = useMemo(() => ({
    loadTimeoutMs: 3000,
  }), []);

  const onCloseButtonPress = useCallback<EventHandlers['onCloseButtonPress']>(() => {}, []);
  const onProductSelected = useCallback<EventHandlers['onProductSelected']>((productId) => {}, []);
  const onPurchaseStarted = useCallback<EventHandlers['onPurchaseStarted']>((product) => {}, []);
  const onPurchaseCompleted = useCallback<EventHandlers['onPurchaseCompleted']>((purchaseResult, product) => {}, []);
  const onPurchaseFailed = useCallback<EventHandlers['onPurchaseFailed']>((error, product) => {}, []);
  const onRestoreStarted = useCallback<EventHandlers['onRestoreStarted']>(() => {}, []);
  const onRestoreCompleted = useCallback<EventHandlers['onRestoreCompleted']>((profile) => {}, []);
  const onRestoreFailed = useCallback<EventHandlers['onRestoreFailed']>((error) => {}, []);
  const onPaywallShown = useCallback<EventHandlers['onPaywallShown']>(() => {}, []);
  const onRenderingFailed = useCallback<EventHandlers['onRenderingFailed']>((error) => {}, []);
  const onLoadingProductsFailed = useCallback<EventHandlers['onLoadingProductsFailed']>((error) => {}, []);
  const onUrlPress = useCallback<EventHandlers['onUrlPress']>((url) => {}, []);
  const onCustomAction = useCallback<EventHandlers['onCustomAction']>((actionId) => {}, []);
  const onWebPaymentNavigationFinished = useCallback<EventHandlers['onWebPaymentNavigationFinished']>(() => {}, []);

  return (
    <AdaptyPaywallView
      paywall={paywall}
      params={paywallParams}
      style={styles.paywall}
      onCloseButtonPress={onCloseButtonPress}
      onProductSelected={onProductSelected}
      onPurchaseStarted={onPurchaseStarted}
      onPurchaseCompleted={onPurchaseCompleted}
      onPurchaseFailed={onPurchaseFailed}
      onRestoreStarted={onRestoreStarted}
      onRestoreCompleted={onRestoreCompleted}
      onRestoreFailed={onRestoreFailed}
      onPaywallShown={onPaywallShown}
      onRenderingFailed={onRenderingFailed}
      onLoadingProductsFailed={onLoadingProductsFailed}
      onCustomAction={onCustomAction}
      onUrlPress={onUrlPress}
      onWebPaymentNavigationFinished={onWebPaymentNavigationFinished}
    />
  );
}
```

## Trình bày dạng modal \{#modal-presentation\}

Để hiển thị paywall dưới dạng màn hình độc lập, hãy sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createPaywallView`](react-native-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo một `view` mới.

:::warning
Không được phép tái sử dụng cùng một `view` mà không tạo lại. Việc này sẽ dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`.
:::
```typescript showLineNumbers title="React Native (TSX)"

const view = await createPaywallView(paywall);

// Optional: handle paywall events (close, purchase, restore, etc)
// view.setEventHandlers({ ... });

try {
  await view.present();
} catch (error) {
  // handle the error
}
```

:::important
Gọi `setEventHandlers` nhiều lần sẽ ghi đè các handler bạn đã cung cấp, thay thế cả handler mặc định lẫn các handler đã được thiết lập trước đó cho những sự kiện cụ thể đó.
:::
### Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\}

Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `'full_screen'` (mặc định) hoặc `'page_sheet'`.

```typescript showLineNumbers
try {
  await view.present({ iosPresentationStyle: 'page_sheet' });
} catch (error) {
  // handle the error
}
```
## Sử dụng timer do lập trình viên định nghĩa \{#use-developer-defined-timer\}

Để sử dụng timer do lập trình viên định nghĩa trong ứng dụng di động, hãy dùng `timerId`, trong ví dụ này là `CUSTOM_TIMER_NY` — **Timer ID** của timer do lập trình viên định nghĩa mà bạn đã thiết lập trên Adapty dashboard. Điều này đảm bảo ứng dụng cập nhật động giá trị của timer với giá trị chính xác — chẳng hạn `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, ví dụ như ngày đầu năm mới, trừ đi thời gian hiện tại).
<Tabs>
<TabItem value="component" label="React component">
```typescript showLineNumbers title="React Native (TSX)"
const paywallParams = {
  customTimers: { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) }
};

<AdaptyPaywallView
  paywall={paywall}
  params={paywallParams}
  // ... your event handlers
/>
```
</TabItem>
<TabItem value="modal" label="Modal presentation">
```typescript showLineNumbers title="React Native (TSX)"
const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) };

const view = await createPaywallView(paywall, { customTimers });
```
</TabItem>
</Tabs>
Trong ví dụ này, `CUSTOM_TIMER_NY` là **Timer ID** của timer do developer định nghĩa mà bạn đã cài đặt trong Adapty dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động timer với giá trị chính xác—chẳng hạn như `13d 09h 03m 34s` (được tính bằng thời điểm kết thúc của timer, như Ngày Đầu Năm Mới, trừ đi thời gian hiện tại).
## Hiển thị dialog \{#show-dialog\}

Sử dụng phương thức này thay cho các hộp thoại alert thông thường khi một paywall view đang hiển thị trên Android. Trên Android, các alert RN thông thường sẽ xuất hiện phía sau paywall view, khiến người dùng không thể nhìn thấy chúng. Phương thức này đảm bảo dialog được hiển thị đúng cách, phía trên paywall trên mọi nền tảng.
```typescript showLineNumbers title="React Native (TSX)"
try {
  const action = await view.showDialog({
    title: 'Close paywall?',
    content: 'You will lose access to exclusive offers.',
    primaryActionTitle: 'Stay',
    secondaryActionTitle: 'Close',
  });

  if (action === 'secondary') {
    // User confirmed - close the paywall
    await view.dismiss();
  }
  // If primary - do nothing, user stays
} catch (error) {
  // handle error
}
```

## Thay thế một gói đăng ký bằng gói khác \{#replace-one-subscription-with-another\}

Khi người dùng cố mua một gói đăng ký mới trong khi đang có gói đăng ký khác đang hoạt động trên Android, bạn có thể kiểm soát cách xử lý giao dịch mua mới bằng cách truyền các tham số cập nhật gói đăng ký khi tạo paywall view. Để thay thế gói đăng ký hiện tại bằng gói mới, hãy sử dụng `productPurchaseParams` trong `createPaywallView` với các tham số `oldSubVendorProductId` và `prorationMode`.
```typescript showLineNumbers title="React Native (TSX)"

const productPurchaseParams = paywall.productIdentifiers.map((productId) => {
  let params = {};
  if (Platform.OS === 'android') {
    params.android = {
      subscriptionUpdateParams: {
        oldSubVendorProductId: 'PRODUCT_ID_OF_THE_CURRENT_ACTIVE_SUBSCRIPTION',
        prorationMode: 'with_time_proration',
      },
    };
  }
  return { productId, params };
});

const view = await createPaywallView(paywall, { productPurchaseParams });
```

## Khắc phục sự cố \{#troubleshooting\}
### Lớp phủ hình ảnh ở đầu paywall (Android) \{#visual-overlay-at-the-top-of-the-paywall-android\}

:::note
Cài đặt này được hỗ trợ từ React Native SDK 3.15.5 trở lên và chỉ khả dụng trong các dự án React Native thuần (bare).

Nếu bạn đang sử dụng Expo managed workflow, bạn không thể thêm trực tiếp Android resource này. Để áp dụng cài đặt này, bạn cần tạo một Expo config plugin tùy chỉnh để thêm Android resource tương ứng và đăng ký nó trong `app.config.js`. Điều này là bắt buộc vì Expo quản lý native Android project thay cho bạn.
:::
Nếu `AdaptyPaywallView` không mở rộng ra phía sau thanh trạng thái, một lớp phủ trực quan vẫn có thể xuất hiện ở phía trên. Để xóa nó, hãy thêm tài nguyên boolean sau vào ứng dụng của bạn:

1. Truy cập vào `android/app/src/main/res/values`. Nếu chưa có file `bools.xml`, hãy tạo mới.

2. Thêm tài nguyên sau:

```xml
<resources>
    <bool name="adapty_paywall_enable_safe_area_paddings">false</bool>
</resources>
```

Lưu ý rằng các thay đổi này áp dụng toàn cục cho tất cả các paywall trong ứng dụng của bạn.

---