> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Presenting placements

> Render a flow for a placement key with the FlowPilotPresenter component, plus the never-fails host fallback.

To show a flow you give FlowPilot a placement key. The SDK resolves which flow to show (cache, network, experiment variant, audience), and you render it with the `<FlowPilotPresenter />` component. On the Expo SDK you always present a flow **declaratively**: you resolve a `FlowSession` and render it through the presenter component. There is a non-throwing resolver (`resolveSession`) for must-not-fail entry points like onboarding, and a throwing variant (`createSession`) when you want to handle the no-flow case yourself.

<Warning>
  **`FlowPilot.presentPlacement()` is deprecated on the Expo SDK and does not render any UI.** React Native cannot present UI without a mounted component, and the Expo SDK has no global presentation host, so the call logs a deprecation warning and returns an `error` result immediately without presenting. Use the declarative pattern on this page. See [Deprecated: presentPlacement](#deprecated-presentplacement). (On the iOS SDK, imperative `presentPlacement` is supported.)
</Warning>

<Note>
  The SDK must be configured first. A resolve before `FlowPilot.configure(...)` throws. See [Configuration](/expo-sdk/configuration).
</Note>

## The presentation APIs at a glance

| API                     | Returns                           | On no flow           | Use when                                                         |
| ----------------------- | --------------------------------- | -------------------- | ---------------------------------------------------------------- |
| `resolveSession(key)`   | `Promise<FlowSession \| null>`    | resolves to `null`   | Onboarding and other "must not fail" entry points (recommended). |
| `createSession(key)`    | `Promise<FlowSession>`            | **rejects** (throws) | You want to handle the no-flow case yourself with a `try/catch`. |
| `presentPlacement(key)` | `Promise<FlowPresentationResult>` | (deprecated)         | Do not use on Expo: it renders nothing. See below.               |

You render whichever session you get through `<FlowPilotPresenter session={...} />`.

## Present a flow

`resolveSession(placementId)` is the recommended entry point. It walks the full fail-safe chain (cache, network, bundled default) and returns a ready `FlowSession`, or `null` when nothing is presentable, **without throwing**. Render the session with `<FlowPilotPresenter />`, and render your own native UI when it is `null`.

```tsx theme={null}
import { useEffect, useState } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import {
  FlowPilot,
  FlowPilotPresenter,
  type FlowSession,
} from '@flowpilotjs/react-native-sdk';

export function Onboarding() {
  const [session, setSession] = useState<FlowSession | null>(null);
  const insets = useSafeAreaInsets();

  useEffect(() => {
    // resolveSession is non-throwing: returns null when nothing is presentable.
    FlowPilot.resolveSession('onboarding').then(setSession);
  }, []);

  if (!session) return <MyNativeOnboarding />; // host fallback

  return (
    <FlowPilotPresenter
      session={session}
      safeAreaInsets={insets}
      onComplete={({ outcome }) => {
        console.log('Outcome:', outcome);
        setSession(null); // clear to dismiss the modal
      }}
      fallback={<MyNativeOnboarding />}
    />
  );
}
```

`resolveSession` returns the session already resolved and **started by the presenter** when rendered. You do not call `session.start()` yourself with this pattern; the presenter drives the session. (If you build a session with `createSession` and render the lower-level `FlowPresenter` directly, you are responsible for `start()`.)

### `<FlowPilotPresenter />` props

The component wraps the renderer in a full-screen React Native `Modal`. Pass `session={null}` to hide it.

<ParamField path="session" type="FlowSession | null" required>
  The session to render. `null` hides the presenter.
</ParamField>

<ParamField path="onComplete" type="(result: FlowPresentationResult) => void">
  Called once when the flow completes or is dismissed, with a `FlowPresentationResult` (`{ outcome, error? }`). Set `session` back to `null` here to close the modal.
</ParamField>

<ParamField path="safeAreaInsets" type="{ top: number; bottom: number; left: number; right: number }">
  Safe-area insets, normally from `useSafeAreaInsets()`. The renderer lays out around the notch and home indicator with these.
</ParamField>

<ParamField path="animationType" type="'none' | 'slide' | 'fade'" default="'slide'">
  The modal's present/dismiss animation (passed straight to React Native's `Modal`).
</ParamField>

<ParamField path="presentationStyle" type="'fullScreen' | 'pageSheet' | 'formSheet' | 'overFullScreen'" default="'fullScreen'">
  The iOS modal presentation style (passed to `Modal`). Ignored on Android.
</ParamField>

<ParamField path="statusBarStyle" type="'default' | 'light-content' | 'dark-content'">
  Status bar style applied while the flow is presented.
</ParamField>

<ParamField path="fallback" type="React.ReactNode | (() => React.ReactNode)">
  Host UI rendered instead of the loading spinner if the presentation fails before any screen shows. See [Host fallback](#host-fallback-must-not-fail).
</ParamField>

<ParamField path="onError" type="(error: Error) => void">
  Called once if the presentation enters the error state.
</ParamField>

<Note>
  For advanced embedding without a modal, the lower-level `FlowPresenter` component (and its `FlowPresenterProps`) is also exported. `FlowPilotPresenter` is `FlowPresenter` wrapped in a `Modal`; reach for `FlowPresenter` only when you need to host the flow inside your own container. Most apps use `FlowPilotPresenter`.
</Note>

### `createSession`: the throwing variant

When you would rather handle the no-flow case with a `try/catch` than a `null` check, use `createSession(placementId)`. It fetches the placement (using the cache if available) and returns the session **unstarted**, then **throws** when no presentable flow exists.

```tsx theme={null}
useEffect(() => {
  let cancelled = false;
  FlowPilot.createSession('paywall_main')
    .then((s) => {
      if (cancelled) return;
      setSession(s);
    })
    .catch((err) => {
      // No presentable flow. Decide what your app shows here.
      console.warn('No flow for paywall_main:', err);
    });
  return () => {
    cancelled = true;
  };
}, []);
```

There is no per-present options argument in this build (no additional context, no presentation-style override). To vary targeting, set `context` in [configuration](/expo-sdk/configuration); to control the modal, use the `<FlowPilotPresenter />` props above.

## Host fallback (must not fail)

For anything user-facing, like onboarding, make sure the user always sees something even when every fail-safe tier misses (offline, no cache, no bundled default).

`resolveSession` already gives you this: it returns `null` instead of throwing, so you can render your own native UI:

```tsx theme={null}
const session = await FlowPilot.resolveSession('onboarding');

return session
  ? <FlowPilotPresenter session={session} onComplete={handleComplete} />
  : <MyNativeOnboarding />;
```

You can also pass a `fallback` to the presenter. It renders instead of the loading spinner when a presentation fails before any screen shows (for example a navigation dead-end caught by the presentation watchdog):

```tsx theme={null}
<FlowPilotPresenter
  session={session}
  onComplete={handleComplete}
  fallback={<MyNativeOnboarding />}
  onError={(e) => console.warn('flow presentation failed', e)}
/>
```

See the full fail-safe chain in [Caching](/expo-sdk/caching) and [Offline and bundled flows](/expo-sdk/offline-bundled-flows).

## Deprecated: presentPlacement

`FlowPilot.presentPlacement(key)` is **deprecated on the Expo SDK and does not render a flow.** React Native cannot present UI without a mounted component, and the Expo SDK has no global presentation host, so the method logs a deprecation warning and resolves immediately with `{ outcome: 'error' }` without presenting anything. (Older builds instead ran the session headlessly, emitting `flow_start` and the entry `screen_view` but painting nothing, and never resolved for an interactive flow; either way, it cannot show a flow.)

Do not call it. Use `resolveSession` (or `createSession`) plus `<FlowPilotPresenter />`, shown above. (On the iOS SDK, which has a real presenting view controller, imperative `presentPlacement` is supported.)

## Common mistakes

* **Calling `presentPlacement` on Expo.** It is deprecated and renders nothing. Use `resolveSession` + `<FlowPilotPresenter />`.
* **Not clearing the session on completion.** Set `session` back to `null` in `onComplete`, or the modal stays up.
* **Using `createSession` on a must-not-fail path with no `catch`.** It rejects when there is no presentable flow. Add a `.catch`, or use `resolveSession`, which returns `null` instead.
* **Expecting a per-present context argument.** There is none. Set `context` at [configure](/expo-sdk/configuration) time.
* **Rendering `FlowPilotPresenter` without a `SafeAreaProvider`.** `useSafeAreaInsets()` returns zeros, so the flow can render under the status bar. Wrap your app in `SafeAreaProvider`. See [Installation](/expo-sdk/installation).

## Troubleshooting

`createSession` threw, `resolveSession` returned `null`, or the presenter entered its error state:

| Symptom                                                                 | Likely cause                                                          | Fix                                                                                                                |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `resolveSession` returns `null` / `onError` fires with `FLOW_NOT_FOUND` | No published flow attached to the placement, or it is paused          | Attach and publish a flow, set the placement active. See [Placements](/dashboard/placements).                      |
| Nothing resolves, offline                                               | Device offline with no cache and no bundled default                   | Ship a [bundled default flow](/expo-sdk/offline-bundled-flows) or pre-warm with [prefetch](/expo-sdk/prefetching). |
| `onError` with code `TIMEOUT`                                           | Resolve exceeded `resolveTimeout` (default 4s) and nothing was cached | The SDK already falls back to cache/bundled; raise `resolveTimeout` only if needed.                                |
| Resolves on one device, not another                                     | Audience targeting excludes this user/context                         | Check the placement's audience rules and the `context` you pass.                                                   |
| Always empty for one app                                                | Wrong `appId`, the placement belongs to another app                   | Configure the `appId` that owns the placement.                                                                     |

See [Error handling](/expo-sdk/error-handling) for every error code.

## Related pages

* [Results and outcomes](/expo-sdk/results-and-outcomes)
* [Prefetching and readiness](/expo-sdk/prefetching)
* [Variables and context](/expo-sdk/variables-and-context)
* [Error handling](/expo-sdk/error-handling)
* [Placements (dashboard)](/dashboard/placements)
* [SDK REST API (resolve contract)](/reference/sdk-rest-api)
