---
title: "Отображение флоу и пейволов — React Native"
description: "Показывайте флоу и пейволы пользователям вашего React Native приложения с помощью Adapty."
---

<MethodPromo method="getFlow" label="Display flows and paywalls" />
Если вы создали флоу или пейвол в Flow Builder, вам не нужно беспокоиться о том, как отрендерить его в коде мобильного приложения для отображения пользователю. Такой флоу содержит и то, что должно быть показано, и то, как именно это должно быть показано.

Прежде чем начать, убедитесь, что:

1. Вы [создали флоу или пейвол](create-paywall).
2. Вы добавили его в [плейсмент](placements).
3. Вы [получили флоу и подготовили представление](react-native-get-pb-paywalls).

:::warning
Этот гайд предназначен только для **флоу и пейволов Paywall Builder**, которые требуют SDK v4.0 или выше. Процесс отображения флоу отличается для пейволов на Remote Config.

- Для отображения **пейволов на Remote Config** см. [Отображение пейвола, созданного через Remote Config](present-remote-config-paywalls).

:::

Adapty React Native SDK предоставляет два способа отображения флоу и пейволов:

- **React-компонент**: встраиваемый компонент, который можно интегрировать в архитектуру и систему навигации вашего приложения.

- **Модальное представление**
## React-компонент \{#react-component\}

Чтобы встроить флоу в существующее дерево компонентов, используйте компонент `AdaptyFlowView` напрямую в иерархии React Native-компонентов. Встроенный компонент позволяет интегрировать его в архитектуру приложения и систему навигации.
:::tip
Компонент `AdaptyFlowView` создаёт своё представление в момент рендеринга — когда загружаются конфигурация и изображения. Чтобы предзагрузить их, вызовите [`createFlowView`](react-native-get-pb-paywalls#fetch-the-view-configuration) для того же флоу раньше в приложении. Тогда компонент использует кэшированные данные и рендерится без ожидания загрузки.
:::
```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}
    />
  );
}
```

## Модальное отображение \{#modal-presentation\}

Чтобы показать флоу как отдельный экран, используйте метод `view.present()` на объекте `view`, созданном методом [`createFlowView`](react-native-get-pb-paywalls#fetch-the-view-configuration). Каждый `view` можно использовать только один раз. Если нужно показать флоу повторно, вызовите `createFlowView` ещё раз, чтобы создать новый экземпляр `view`.

:::warning
Повторное использование одного и того же `view` без его пересоздания запрещено. Это приведёт к ошибке `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
Каждый повторный вызов `setEventHandlers` перезаписывает обработчики: заменяются как дефолтные, так и ранее установленные обработчики для указанных событий.
:::
### Настройка стиля отображения на iOS \{#configure-ios-presentation-style\}

Настройте способ отображения флоу на iOS, передав параметр `iosPresentationStyle` в метод `present()`. Параметр принимает значения `'full_screen'` (по умолчанию) или `'page_sheet'`.

```typescript showLineNumbers
try {
  await view.present({ iosPresentationStyle: 'page_sheet' });
} catch (error) {
  // handle the error
}
```
## Использование таймеров, определённых разработчиком \{#use-developer-defined-timer\}

Чтобы использовать таймеры, определённые разработчиком, в мобильном приложении, используйте `timerId` — в данном примере `CUSTOM_TIMER_NY`, **Timer ID** таймера, заданного в дашборде Adapty. Это обеспечивает динамическое обновление таймера с правильным значением — например, `13d 09h 03m 34s` (вычисляется как время окончания таймера, например Новый год, минус текущее время).
<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>
В этом примере `CUSTOM_TIMER_NY` — это **Timer ID** таймера, заданного разработчиком в дашборде Adapty. `timerResolver` обеспечивает динамическое обновление таймера с нужным значением — например, `13d 09h 03m 34s` (вычисляется как разница между временем окончания таймера, например Новым годом, и текущим временем).
## Показ диалогов \{#show-dialog\}

Используйте этот метод вместо нативных диалогов предупреждений, когда на Android отображается флоу. На Android обычные RN-алерты появляются позади флоу и становятся невидимыми для пользователей. Этот метод обеспечивает корректное отображение диалога поверх флоу на всех платформах.
```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
}
```

## Замена одной подписки на другую \{#replace-one-subscription-with-another\}

Когда пользователь пытается приобрести новую подписку, пока на Android активна другая, вы можете управлять тем, как должна обрабатываться новая покупка, — для этого передайте параметры обновления подписки при создании представления флоу. Чтобы заменить текущую подписку новой, используйте `productPurchaseParams` в `createFlowView` с параметрами `oldSubVendorProductId` и `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.

Если вы настроили пейвол с помощью Paywall Builder, вам не нужно беспокоиться о его отображении в коде мобильного приложения — всё уже задано внутри самого пейвола: и содержимое, и способ отображения.

Прежде чем начать, убедитесь, что:

1. Вы [создали пейвол](create-paywall).
2. Вы добавили пейвол в [плейсмент](placements).
3. Вы [получили пейвол и подготовили представление](react-native-get-pb-paywalls).

:::warning
Это руководство предназначено только для **пейволов, созданных в новом Paywall Builder**, которые требуют SDK v3.0 или выше. Процесс отображения пейволов различается для пейволов, созданных в разных версиях Paywall Builder, и для Remote Config пейволов.

- Чтобы отобразить **Remote Config пейволы**, см. [Отображение пейвола на основе Remote Config](present-remote-config-paywalls).

:::

Adapty React Native SDK предоставляет два способа отображения пейволов:

- **React-компонент**: встроенный компонент, который можно интегрировать в архитектуру и систему навигации вашего приложения.

- **Модальное окно**
## React-компонент \{#react-component\}

:::note
Подход на основе **React-компонента** требует SDK версии 3.14.0 или выше.
:::

Чтобы встроить пейвол в существующее дерево компонентов, используйте компонент `AdaptyPaywallView` непосредственно в иерархии React Native компонентов. Встроенный компонент позволяет интегрировать его в архитектуру приложения и систему навигации.
:::note
На Android, если пейвол не перекрывает строку состояния, вверху может появиться визуальный оверлей. Рекомендуем отключить его для своих пейволов. См. [Визуальный оверлей в верхней части пейвола (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}
    />
  );
}
```

## Модальное отображение \{#modal-presentation\}

Чтобы показать пейвол как отдельный экран, используйте метод `view.present()` на объекте `view`, созданном методом [`createPaywallView`](react-native-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Каждый `view` можно использовать только один раз. Если нужно показать пейвол повторно, вызовите `createPaywallView` ещё раз, чтобы создать новый экземпляр `view`.

:::warning
Повторное использование одного и того же `view` без его пересоздания запрещено. Это приведёт к ошибке `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
Каждый вызов `setEventHandlers` перезаписывает ранее установленные обработчики — включая дефолтные и те, что вы задали раньше, для указанных событий.
:::
### Настройка стиля отображения на iOS \{#configure-ios-presentation-style\}

Настройте способ отображения пейвола на iOS, передав параметр `iosPresentationStyle` в метод `present()`. Параметр принимает значения `'full_screen'` (по умолчанию) или `'page_sheet'`.

```typescript showLineNumbers
try {
  await view.present({ iosPresentationStyle: 'page_sheet' });
} catch (error) {
  // handle the error
}
```
## Использование таймера, заданного разработчиком \{#use-developer-defined-timer\}

Чтобы использовать таймеры, заданные разработчиком, в мобильном приложении, используйте `timerId` — в данном примере `CUSTOM_TIMER_NY`, **Timer ID** таймера, заданного разработчиком, который вы установили в дашборде Adapty. Это позволяет приложению динамически обновлять таймер с правильным значением — например, `13d 09h 03m 34s` (рассчитывается как время окончания таймера, например Новый год, минус текущее время).
<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>
В этом примере `CUSTOM_TIMER_NY` — это **Timer ID** таймера, заданного разработчиком в дашборде Adapty. `timerResolver` обеспечивает динамическое обновление таймера с нужным значением — например, `13d 09h 03m 34s` (вычисляется как время окончания таймера, например Новый год, минус текущее время).
## Отображение диалога \{#show-dialog\}

Используйте этот метод вместо нативных диалогов оповещений, когда на Android отображается пейвол. На Android обычные RN-алерты появляются позади пейвола и становятся невидимы для пользователей. Этот метод обеспечивает корректное отображение диалога поверх пейвола на всех платформах.
```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
}
```

## Замена одной подписки на другую \{#replace-one-subscription-with-another\}

Когда пользователь пытается купить новую подписку при наличии активной подписки на Android, вы можете управлять тем, как должная обрабатываться новая покупка, передав параметры обновления подписки при создании вью пейвола. Чтобы заменить текущую подписку на новую, используйте `productPurchaseParams` в `createPaywallView` с параметрами `oldSubVendorProductId` и `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 });
```

## Устранение неполадок \{#troubleshooting\}
### Визуальный оверлей в верхней части пейвола (Android) \{#visual-overlay-at-the-top-of-the-paywall-android\}

:::note
Этот параметр поддерживается начиная с React Native SDK 3.15.5 и доступен только в bare React Native-проектах.

Если вы используете управляемый воркфлоу Expo, добавить этот Android-ресурс напрямую не получится. Чтобы применить этот параметр, необходимо создать кастомный Expo config plugin, который добавляет соответствующий Android-ресурс, и зарегистрировать его в app.config.js. Это обязательно, поскольку Expo управляет нативным Android-проектом за вас.
:::
Если `AdaptyPaywallView` не растягивается за пределы строки состояния, над ней всё равно может появляться визуальный оверлей. Чтобы убрать его, добавьте следующий булев ресурс в приложение:

1. Перейдите в `android/app/src/main/res/values`. Если файла `bools.xml` нет — создайте его.

2. Добавьте следующий ресурс:

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

Обратите внимание: изменения применяются глобально ко всем пейволам в приложении.

---