Skip to main content
To show a flow you give FlowPilot a placement key. The SDK resolves which flow to show (cache, network, experiment variant, audience), renders it natively, and hands you back an outcome when the user finishes. There are two ways to present, plus a non-throwing resolver for must-not-fail entry points like onboarding.
The SDK must be configured first. A present or resolve before FlowPilot.configure(...) throws (imperative presentPlacement surfaces it as an error outcome). See Configuration.

The presentation APIs at a glance

APIReturnsOn no flowUse when
presentPlacement(key)Promise<FlowPresentationResult>resolves with outcome: 'error'You want a modal shown imperatively and the outcome back.
createSession(key)Promise<FlowSession>rejects (throws)You render the flow yourself with <FlowPilotPresenter />.
resolveSession(key)Promise<FlowSession | null>resolves to nullOnboarding and other “must not fail” entry points.

Imperative: present a modal

presentPlacement(placementId) resolves the placement, opens a full-screen modal, and resolves a promise once the user completes or dismisses the flow. It never rejects for predictable failures (network error, missing placement, not configured); it resolves with an error outcome instead, so a single switch covers every case.
import { FlowPilot } from '@flowpilotjs/react-native-sdk';

async function startPaywall() {
  const result = await FlowPilot.presentPlacement('paywall_main');

  switch (result.outcome) {
    case 'completed':
      // The user finished the flow.
      break;
    case 'dismissed':
      // The user closed the flow early.
      break;
    case 'error':
      console.warn('FlowPilot could not present a flow:', result.error?.message);
      break;
  }
}
presentPlacement takes only the placement key. There is no per-present options argument in this build (no additional context, no presentation-style override). To vary targeting, set context in configuration; to control the modal, use the declarative path below.

Declarative: render a session

For control over safe-area handling, status bar, or embedding a flow inside your own view tree, build a FlowSession with createSession and render it with <FlowPilotPresenter />. createSession(placementId) fetches the placement (using the cache if available) and returns the session unstarted, so you can wire it up before the first screen shows. Call session.start() to begin. It throws when no presentable flow exists, so handle that (or use resolveSession, below).
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 PaywallTrigger() {
  const [session, setSession] = useState<FlowSession | null>(null);
  const insets = useSafeAreaInsets();

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

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

<FlowPilotPresenter /> props

The component wraps the renderer in a full-screen React Native Modal. Pass session={null} to hide it.
session
FlowSession | null
required
The session to render. null hides the presenter.
onComplete
(result: FlowPresentationResult) => void
Called once when the flow completes or is dismissed. Set session back to null here to close the modal.
safeAreaInsets
{ 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.
animationType
'none' | 'slide' | 'fade'
default:"'slide'"
The modal’s present/dismiss animation (passed straight to React Native’s Modal).
presentationStyle
'fullScreen' | 'pageSheet' | 'formSheet' | 'overFullScreen'
default:"'fullScreen'"
The iOS modal presentation style (passed to Modal). Ignored on Android.
statusBarStyle
'default' | 'light-content' | 'dark-content'
Status bar style applied while the flow is presented.
fallback
React.ReactNode | (() => React.ReactNode)
Host UI rendered instead of the loading spinner if the presentation fails before any screen shows. See Host fallback.
onError
(error: Error) => void
Called once if the presentation enters the error state.
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.

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(placementId) is the non-throwing resolver. It walks the full fail-safe chain and returns a ready FlowSession, or null when nothing is presentable, so you can render your own native UI instead.
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):
<FlowPilotPresenter
  session={session}
  onComplete={handleComplete}
  fallback={<MyNativeOnboarding />}
  onError={(e) => console.warn('flow presentation failed', e)}
/>
See the full fail-safe chain in Caching and Offline and bundled flows.

Common mistakes

  • Forgetting session.start(). createSession returns the session unstarted. Without start(), the presenter shows nothing. (The imperative presentPlacement starts it for you.)
  • Not clearing the session on completion. With the declarative path, set session back to null in onComplete, or the modal stays up.
  • Not handling the throw from createSession. 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 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.

Troubleshooting

The imperative call returned error, or createSession threw / resolveSession returned null:
SymptomLikely causeFix
error outcome, code FLOW_NOT_FOUNDNo published flow attached to the placement, or it is pausedAttach and publish a flow, set the placement active. See Placements.
Nothing resolves, offlineDevice offline with no cache and no bundled defaultShip a bundled default flow or pre-warm with prefetch.
error outcome, code TIMEOUTResolve exceeded resolveTimeout (default 4s) and nothing was cachedThe SDK already falls back to cache/bundled; raise resolveTimeout only if needed.
Resolves on one device, not anotherAudience targeting excludes this user/contextCheck the placement’s audience rules and the context you pass.
Always empty for one appWrong appId, the placement belongs to another appConfigure the appId that owns the placement.
See Error handling for every error code.