# React Native (https://www.getfounderhq.com/docs/analytics/sdks/react-native)

Capture screens, app lifecycle, and your own events from a React Native or
Expo app.

<Callout title="Before you start">
You need a publishable key (`fhq_pk_...`) for the brand you want to
measure. See [Getting started](/analytics/getting-started).
</Callout>

## Install

```npm
npm install @founderhq/events-react-native @react-native-async-storage/async-storage
```

The SDK needs React Native 0.73 or later and AsyncStorage 1.21 or later.

For the Expo build, add the three optional packages it reads context from:

```bash
npx expo install expo-application expo-device expo-localization
```

## Initialize

Create one client and share it across the app.

```ts
import { FounderHqReactNativeClient } from "@founderhq/events-react-native";

export const events = new FounderHqReactNativeClient("fhq_pk_XXXX");
```

On Expo, use the Expo client instead. It adds app, device, and locale
context for you.

```ts
import { createFounderHqExpoClient } from "@founderhq/events-react-native/expo";

export const events = createFounderHqExpoClient("fhq_pk_XXXX");
```

The client loads stored state, applies your capture settings, starts app
lifecycle capture, and records the session start on its own.

## Capture events

```ts
events.capture("trial.started", { plan: "growth" });
```

`track` is an alias for `capture`. Both queue the work and return at once.

Every writing method has an `...AndWait` twin that resolves after the work
is applied: `captureAndWait`, `identifyAndWait`, `screenAndWait`,
`setAccountAndWait`, `clearAccountAndWait`, `resetAccountsAndWait`,
`setAccountPropertiesAndWait`, `setPersonPropertiesAndWait`, and
`resetAndWait`. Use them in tests, and before the app leaves a flow.
`readyForCapture()` resolves once startup and the queued calls are done.

Record a deep link when your app opens from one:

```ts
events.captureDeepLink(url);
```

`captureDeepLink` records the link without its query string, and keeps the
campaign parameters it recognizes.

## Track screens

Record a screen yourself with `events.screen("Pricing")`, or wire up your
router.

React Navigation:

```tsx
import { createReactNavigationTracker } from "@founderhq/events-react-native";

const tracker = createReactNavigationTracker(events, navigationRef);

<NavigationContainer
  ref={navigationRef}
  onReady={tracker.onReady}
  onStateChange={tracker.onStateChange}
/>;
```

Expo Router:

```ts
import { createExpoRouterTracker } from "@founderhq/events-react-native";

const trackPath = createExpoRouterTracker(events);
trackPath(pathname, params);
```

Both trackers skip a repeat of the screen the app is already on.

## Identify a contact

```ts
events.identify("user_42", { email: "jane@acme.com" });
```

Pass an account in the third argument to change identity and account
together:

```ts
await events.identifyAndWait("user_42", { email: "jane@acme.com" }, {
  account: { key: "workspace_123" },
});
```

Update contact properties on their own with `setPersonProperties(set,
setOnce)`. With the default `identified_only` mode, changes made before
`identify` stay on the device and go out with the identify call.

Call `reset()` on logout. It clears the identity and the account.

## Accounts

```ts
events.setAccount("workspace_123");
events.setAccountProperties({ plan: "growth", seats: 12 });
events.clearAccount(); // resetAccounts() does the same
```

`setAccount` also takes an object with `key`, `properties`, and
`contextToken`. Pass `account` to the constructor when the first automatic
event must already carry it. A queued event keeps the account it was
captured with. See
[Accounts and groups](/analytics/concepts/accounts-and-groups).

## Consent

| You call | What happens |
| --- | --- |
| `optIn()` | Capture is on |
| `optOut()` | Capture stops, and the queue is cleared |
| `isOptedOut()` | Returns `true` while capture is off |

Set `opt_out_by_default: true` to start silent until the person agrees. See
[Consent and privacy](/analytics/concepts/consent-and-privacy).

## Purchases

The client can tie an App Store, Google Play, or RevenueCat purchase to the
person who made it.

| Method | What it does |
| --- | --- |
| `purchaseAttribution()` | Returns the identifiers to pass to the store or to RevenueCat |
| `preparePurchase({ source })` | Records intent before checkout, and returns the purchase context |
| `observePurchase({ prepared, purchase })` | Reports the purchase that followed |
| `claimSubscription({ purchase, confirmation })` | Moves future revenue of an existing subscription |

The full flow, including what each store needs, is on
[Mobile purchase claims](/analytics/revenue/mobile-purchase-claims).

## Options

Pass these in the second argument to the constructor.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `host` | `string` | `https://i.getfounderhq.com` | Where events are sent |
| `flush_at` | `number` | `20` | Sends a batch once this many events are queued |
| `flush_interval_ms` | `number` | `10000` | Sends a batch on this timer. `0` turns the timer off |
| `storage` | adapter | AsyncStorage | Your own storage adapter |
| `person_profiles` | `"identified_only"` \| `"always"` \| `"never"` | `"identified_only"` | When contact property changes are applied |
| `opt_out_by_default` | `boolean` | `false` | Starts opted out |
| `capture_lifecycle` | `boolean` | `true` | App opened and backgrounded events |
| `capture_screens` | `boolean` | `true` | Screen events |
| `capture_sessions` | `boolean` | `true` | Session start events |
| `remote_config` | `boolean` | `true` | Reads capture settings you set in FounderHQ |
| `context` | `object` | none | Your own values, added to every event |
| `contextProvider` | `() => object` | none | The same, resolved at capture time |
| `account` | `string` \| object \| `null` | none | Installs account context before the first event |
| `revenueCat` | object | none | Your RevenueCat client, kept in step with the identity |
| `purchase_prepare_timeout_ms` | `number` | `3000` | How long `preparePurchase` waits before checkout goes ahead offline |
| `capture_element_interactions` | `boolean` | `false` | Taps and rage taps. See [Element interactions](#element-interactions) |
| `capture_element_text` | `boolean` | `true` | Records the words a control shows |
| `capture_rageclicks` | `boolean` | `true` | Rage taps, when element interactions are on |
| `auto_element_capture` | `boolean` | `true` | Watches taps from the app root |

## Lifecycle and sending

The client sends a batch when the queue reaches `flush_at`, on the flush
timer, and when the app goes to the background.

| Method | Returns | What it does |
| --- | --- | --- |
| `flush()` | `Promise<boolean>` | Sends queued events now |
| `close()` | `Promise<void>` | Flushes, then stops the timer and the lifecycle listener |
| `startLifecycleCapture()` | — | Re-attaches the lifecycle listener after `close()` |
| `getDistinctId()` | `string` | The current identity |
| `getSessionId()` | `string` | The current session |

## Element interactions

Set `capture_element_interactions` to `true`. The SDK then records
`$autocapture` when someone taps a control, and `$rageclick` when they tap the
same control three times in a second. Both are off by default.

```ts
const events = new FounderHqReactNativeClient("fhq_pk_...", {
  capture_element_interactions: true,
});
```

You add nothing else. The SDK watches taps from the root of every screen, so
it needs no code in your components. Your layout does not change, and a root
wrapper of your own keeps working.

Create the client in the module your entry file imports. A client created
inside a component starts too late to watch the root. Set
`auto_element_capture` to `false` in that case, and pass
`elementCaptureProps()` to a view yourself.

A `Modal` is the one place the root cannot see. React Native gives a modal its
own window, so taps inside it never reach a view outside it. Pass
`elementCaptureProps()` to the modal's own view:

```tsx
<Modal visible={open}>
  <View style={{ flex: 1 }} {...events.elementCaptureProps()}>
    <Checkout />
  </View>
</Modal>
```

Those props only watch. They never take the touch, so every control below
behaves as it did before.

Each event carries the control and up to four of its parents.

| Stored | Not stored |
| --- | --- |
| The component name, `testID`, and `accessibilityLabel` | Anything a person typed |
| The `accessibilityRole` | Tap coordinates |
| The words the control shows | The value, placeholder, or contents of a `TextInput` |

A tap on a `TextInput`, or on any component with `value`, `defaultValue`,
`placeholder`, `onChangeText`, or `secureTextEntry`, records nothing at all. A
field inside a button adds nothing to that button's text, so a Save button
around an input reads as `Save`.

Text is cut to 255 characters. Numbers that look like a card number or a
social security number are removed from it. Add `fhqNoCapture` to a component
to skip it and everything inside it. Set `capture_element_text` to `false` to
record the controls without their words.

Minified builds rename components. Set `testID` on the controls you want to
recognize in your reports.

## Remote config

The client reads your capture settings from FounderHQ at startup, caches
them on the device, and applies the cached copy on the next launch. It waits
up to 1.5 seconds for a fresh answer.

| Key | What it turns on and off |
| --- | --- |
| `capture_sessions` | Session start events |
| `capture_screens` | Screen events |
| `capture_lifecycle` | App opened and backgrounded events |
| `autocapture` | Taps and rage taps |
| `capture_rageclicks` | Rage taps only. Taps are still recorded |

Your settings in FounderHQ win. They can turn capture off for every install,
and on for an app that shipped with the wrong value. You cannot rebuild an app
people have already installed, so the dashboard decides. The options you pass
to the constructor apply until your settings arrive.

Events already queued under a key that turns off are dropped before they leave
the device.

Call `refreshRemoteConfig()` to fetch them again while the app runs. Call
`applyRemoteConfig(config)` to apply settings you supply yourself. Set
`remote_config: false` to skip the request.

## Next

Read the [protocol reference](/analytics/protocol-reference) for the wire
format, the reserved event names, and the campaign properties a deep link
can carry.
