# Prepare Journeys in React Native (https://www.getfounderhq.com/docs/journeys/mobile-preparation-react-native)

`JourneyHost` prepares a Journey before the person opens it. It downloads the
published configuration while one persistent renderer shell starts in parallel,
then reveals that same WebView when you call `present`.

<Callout type="info" title="Before you start">
Install `@founderhq/journeys-react-native` 0.8.1 or later. Preparation
arrived in 0.7.0; `JourneyView` keeps its existing API.
</Callout>

## Install

```sh
npx expo install react-native-webview react-native-safe-area-context expo-haptics
npm install @founderhq/journeys-react-native@^0.8.1
```

Bare React Native apps need `react-native-webview` and `react-native-safe-area-context` and import from the package
root. On Android, also allow Journey haptic feedback in
`android/app/src/main/AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.VIBRATE" />
```

Expo apps import from `/expo` to get Expo haptics by default. Expo normally
adds the Android vibration permission through native manifest merging.

## Mount one persistent host

Mount `JourneyHost` once beside your navigator. Keep it in the tree while the
app is running. It is a normal React Native view, not a modal, so it inherits
the size of its parent and does not create a second window.

```tsx
import {
  JourneyHost,
  type JourneyHostRef,
} from "@founderhq/journeys-react-native/expo";
import { useRef } from "react";
import { Pressable, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

const onboarding = {
  journeyId: "journey_123",
  identity: { externalId: "usr_1042" },
  storageKey: "onboarding",
};

export function App() {
  const journeys = useRef<JourneyHostRef>(null);
  const insets = useSafeAreaInsets();

  async function prepareOnboarding() {
    await journeys.current?.prepare(onboarding);
  }

  async function openOnboarding() {
    // This also prepares when prepareOnboarding has not run yet.
    await journeys.current?.present(onboarding);
  }

  return (
    <View style={{ flex: 1 }}>
      <AppNavigator
        onOnboardingLikely={prepareOnboarding}
        onOpenOnboarding={openOnboarding}
      />

      <JourneyHost
        ref={journeys}
        apiKey={process.env.EXPO_PUBLIC_FOUNDERHQ_KEY!}
        onEvent={(event) => {
          if (event.type === "complete") {
            journeys.current?.dismiss();
          }
        }}
      >
        <Pressable
          accessibilityLabel="Close Journey"
          accessibilityRole="button"
          onPress={() => journeys.current?.dismiss()}
          style={{ position: "absolute", right: 12, top: insets.top + 8 }}
        >
          <Text>Close</Text>
        </Pressable>
      </JourneyHost>
    </View>
  );
}
```

The parent must have a real size, normally `flex: 1`. The host fills that parent.
Your app continues to own status-bar, navigation-bar, and safe-area policy. Pass
native controls as `JourneyHost` children when they must appear above the
Journey, and inset those controls with `react-native-safe-area-context`.

Do not mount a second `JourneyHost` or a separate `JourneyView` at the same
time. One persistent host owns one renderer WebView.

## Prepare at the useful moment

Call `prepare` when intent becomes likely: after sign-in, when the preceding
screen opens, or when the person enters the part of the app that can launch the
Journey. Preparation is safe to repeat. Concurrent calls for the same inputs
share one request.

```tsx
const prepared = await journeys.current?.prepare({
  journeyId: "journey_123",
  identity: {
    externalId: account.id,
    email: account.email,
  },
  initialAnswers: { company_size: account.companySize },
  capture: {
    context: { source: "mobile-onboarding" },
  },
});

console.log(prepared?.revisionId, prepared?.refreshAt);
```

`prepare` resolves when the Journey is ready. `present` resolves when it is
visible; completion and purchase intents arrive through `onEvent`, so there is
no presentation promise waiting for the Journey to finish.

After `dismiss`, the host keeps the downloaded configuration and prepares a
fresh client session in the same WebView. Answers and navigation from the last
presentation are not reused unless your own `storageKey` policy restores them.

## Freshness and publication changes

A successful preparation is fresh for at least 10 minutes. FounderHQ returns a
refresh interval that the SDK clamps to 10–30 minutes. The host schedules a
refresh while foregrounded, including during an active Journey, and checks
again when the app resumes.

A ready preparation opens immediately through 30 minutes after its last
successful fetch. When refresh is due, presentation happens first and the
refresh runs asynchronously. Content older than 30 minutes requires a
successful fetch before a new presentation. The displayed configuration and
answers remain stable during an active Journey, even beyond that window;
updated content is staged for the next presentation.

Only validated configuration or an authenticated unchanged response matching
cached JSON renews freshness. Failed requests do not renew it or create an
automatic retry loop. Present, resume, and explicit retry can try again after
backoff. Requests coalesce and honor `Retry-After`. Loading has a cumulative
15-second foreground deadline; time in the background does not consume it.

A definitive 401, 403, or confirmed Journey 404 discards preparations and stops
active interaction. Explicit prepare, present, or **Try again** can freshly
authorize the same inputs after access is restored. Cached content is never
reused after a denial. Downloaded content may remain displayed offline until
the device receives a denial. Restarting the app requires fresh authorization
for new presentations while pending answers retain their original attribution.

## Input and account changes

The host invalidates preparation when any of these values change:

- API key, FounderHQ base URL, or renderer URL
- Journey ID or local test configuration
- identity, initial answers, or initial options
- capture settings or capture callbacks
- storage key or theme

Changing the host API key or URL disposes the old credential scope
automatically. Call `dispose` when the signed-in account changes, even when two
accounts happen to use the same key and Journey ID.

```tsx
await auth.signOut();
journeys.current?.dispose();
```

`dispose` drops the cached preparation and renderer content. The SDK also
disposes an unused renderer on an OS memory warning. A later `prepare` or `present(input)` starts a
new renderer shell.

Use 0.7.1 or later for immediate `dispose(); present(input)` or
`dispose(); prepare(input)` calls. It recreates the renderer shell even when
React batches disposal and the next request into one update.

## Capture after dismissal or restart

Capture batches can finish after a Journey is dismissed or a new session is
prepared. Every batch keeps the session, revision, visitor, identity, and
context recorded when its events occurred. The native host forwards that body
unchanged; it never relabels delayed events with the current presentation.

Failed batches remain eligible for replay across a renderer restart. A custom
capture transport receives the same immutable request body:

```tsx
await journeys.current?.prepare({
  journeyId: "journey_123",
  capture: {
    transport: async ({ url, method, body }) => {
      const response = await yourNetworkClient.request({ url, method, body });
      return response.ok;
    },
  },
});
```

## Retry and customize the host

The default loading state is an accessible theme-colored ring with no status
copy. It respects the device's reduced-motion setting. The default error state
uses a short generic message and a retry button.

Use `indicatorColor` to match your app. Use `errorComponent` when you need your
own error surface; its retry callback follows the same 15-second deadline and
freshness rules.

```tsx
<JourneyHost
  ref={journeys}
  apiKey="fhq_pk_XXXX"
  indicatorColor="#7c3aed"
  errorComponent={(error, retry) => (
    <YourErrorCard
      canRetry={error.recoverable}
      onRetry={retry}
    />
  )}
  onStatusChange={(status) => {
    // idle, preparing, prepared, presenting, visible, error, or blocked
  }}
/>
```

You can also provide `onOpenURL`, `onDiscountCodeApply`, `haptics`, and
`onError`. The ref retains `goNext`, `goBack`, `goToStep`, `setAnswer`, and
`flushCapture` for native controls.

## Keep the direct view when preparation is unnecessary

`JourneyView` still works for a screen that naturally mounts before the person
expects content. It validates access, fetches the published configuration, and
renders directly:

```tsx
import { JourneyView } from "@founderhq/journeys-react-native/expo";

<JourneyView
  apiKey="fhq_pk_XXXX"
  journeyId="journey_123"
  identity={{ externalId: "usr_1042" }}
  style={{ flex: 1 }}
/>
```

The persistent host also falls back safely when it encounters a renderer from
before preparation support. It fetches and authorizes the configuration ahead
of time, but does not initialize a hidden Journey. `present` initializes it as
visible, which avoids hidden analytics while preserving compatibility.
