React Native
Use the FounderHQ React Native SDK.
Capture screens, app lifecycle, and your own events from a React Native or Expo app.
Before you start
You need a publishable key (fhq_pk_...) for the brand you want to
measure. See Getting started.
Install
npm install @founderhq/events-react-native @react-native-async-storage/async-storageThe 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:
npx expo install expo-application expo-device expo-localizationInitialize
Create one client and share it across the app.
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.
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
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:
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:
import { createReactNavigationTracker } from "@founderhq/events-react-native";
const tracker = createReactNavigationTracker(events, navigationRef);
<NavigationContainer
ref={navigationRef}
onReady={tracker.onReady}
onStateChange={tracker.onStateChange}
/>;Expo Router:
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
events.identify("user_42", { email: "jane@acme.com" });Pass an account in the third argument to change identity and account together:
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
events.setAccount("workspace_123");
events.setAccountProperties({ plan: "growth", seats: 12 });
events.clearAccount(); // resetAccounts() does the samesetAccount 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.
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.
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.
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 |
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.
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:
<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 for the wire format, the reserved event names, and the campaign properties a deep link can carry.