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

# Mobile

> Track user events in your mobile apps with the Formo React Native SDK. Measure what matters onchain with full device context and wallet event tracking.

The Formo React Native SDK is designed for mobile apps and implements the standard [Events API](/data/events/overview#events-api) with rich mobile context including device information, network status, and app metadata.

## Installation

Install the SDK and its required peer dependency:

```bash theme={null}
npm install @formo/analytics-react-native @react-native-async-storage/async-storage
```

For automatic device info detection (version, build number, device model), install one of:

```bash theme={null}
# Expo projects (recommended)
npx expo install expo-application expo-device

# Bare React Native projects
npm install react-native-device-info
```

If neither is installed, provide app metadata via the `app` option instead.

For **Android install attribution** (knowing which site or campaign a user came from before installing), also install:

```bash theme={null}
npm install react-native-play-install-referrer
```

This one requires a native rebuild of your Android app. Without it the SDK still works; install attribution is simply skipped, and a warning is logged on startup. See [Web-to-mobile attribution](#web-to-mobile-attribution).

### iOS Setup (bare React Native only)

If you're using bare React Native (not Expo), run pod install after adding native dependencies:

```bash theme={null}
cd ios && pod install
```

<Note>
  Expo projects handle native linking automatically, so no `pod install` is needed.
</Note>

## Quick Start

Wrap your app with the `FormoAnalyticsProvider`:

```tsx theme={null}
import AsyncStorage from '@react-native-async-storage/async-storage';
import { FormoAnalyticsProvider } from '@formo/analytics-react-native';

function App() {
  return (
    <FormoAnalyticsProvider
      writeKey="<YOUR_WRITE_KEY>"
      asyncStorage={AsyncStorage}
    >
      <YourApp />
    </FormoAnalyticsProvider>
  );
}
```

For apps using [Wagmi](https://wagmi.sh), enable native integration for automatic wallet event tracking:

```tsx theme={null}
import AsyncStorage from '@react-native-async-storage/async-storage';
import { WagmiProvider, createConfig } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { FormoAnalyticsProvider } from '@formo/analytics-react-native';
import { mainnet } from 'wagmi/chains';

const wagmiConfig = createConfig({
  chains: [mainnet],
  // Required for React Native - MIPD uses browser APIs (window.addEventListener)
  // that don't exist in the React Native environment
  multiInjectedProviderDiscovery: false,
  // ... your connectors and transports
});

const queryClient = new QueryClient();

function App() {
  return (
    <WagmiProvider config={wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        <FormoAnalyticsProvider
          writeKey="<YOUR_WRITE_KEY>"
          asyncStorage={AsyncStorage}
          options={{
            wagmi: {
              config: wagmiConfig,
              queryClient: queryClient,
            },
          }}
        >
          <YourApp />
        </FormoAnalyticsProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}
```

## Code examples

<CardGroup>
  <Card title="React Native" icon="file-code" href="https://github.com/getformo/examples/tree/main/with-react-native">
    examples/react-native
  </Card>
</CardGroup>

## Track screen views

Use the [`screen()`](/data/events/page) method to track screen views, the mobile equivalent of page views:

```tsx theme={null}
import { useFormo } from '@formo/analytics-react-native';
import { useEffect } from 'react';

function WalletScreen() {
  const formo = useFormo();

  useEffect(() => {
    formo.screen('Wallet', 'Main');
  }, [formo]);

  return <View>...</View>;
}
```

The `screen()` method signature is:

```tsx theme={null}
formo.screen(name: string, category?: string, properties?: object)
```

<Note>
  Include `formo` in the dependency array. The SDK initializes asynchronously, so including it ensures the screen event fires once initialization completes.
</Note>

Screen views are sent as `page` events so they flow through the same analytics as web page views. The screen name is recorded in a `page_url` that mirrors a web URL:

```
formo.screen('Wallet')              →  app://com.acme.wallet/Wallet
formo.screen('/tabs/leaderboard')   →  app://com.acme.wallet/tabs/leaderboard
```

Your **bundle ID** takes the place of the hostname and the **screen name** takes the place of the path — structurally the same as `https://app.example.com/wallet`. Formo therefore reads them with the same URL parsing it uses for web: the bundle ID becomes the `origin` and the screen becomes the `page_path`, so screens appear alongside web pages in reports like Top Pages.

<Note>
  Requires SDK **1.0.1 or later**. Earlier versions sent `app://Wallet`, where the screen name occupies the hostname slot and there is no path at all — URL parsers return an empty path for that shape, so those screen views do not appear in Top Pages. Upgrading is the fix.
</Note>

A few consequences worth knowing:

* **Screen names containing `?` or `#` are percent-encoded**, so a name like `Checkout?coupon=X` cannot truncate the path. `/` is left alone, since router-style names are meant to be path segments.
* **The bundle ID is the grouping key**, not your app's display name. Renaming your app therefore does not split its history.
* Set `app.bundleId` explicitly (see [App metadata](#app-metadata)) if you run in **Expo Go** or on **React Native Web**. In Expo Go the native modules report Expo Go's own bundle ID, and on the web nothing resolves one at all.

### React Navigation

Automatically track all screen transitions:

```tsx theme={null}
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
import { useFormo } from '@formo/analytics-react-native';
import { useEffect, useRef, useState } from 'react';

function App() {
  const analytics = useFormo();
  const navigationRef = useNavigationContainerRef();
  const routeNameRef = useRef<string>();
  const [navigationReady, setNavigationReady] = useState(false);

  // Track the first screen. onStateChange does not fire for the initial
  // route, and calls made before the SDK finishes initializing are dropped,
  // so this re-runs once `analytics` changes to the initialized SDK.
  useEffect(() => {
    if (!navigationReady) return;
    const initialRouteName = navigationRef.getCurrentRoute()?.name;
    if (initialRouteName) analytics.screen(initialRouteName);
  }, [navigationReady, analytics]);

  return (
    <NavigationContainer
      ref={navigationRef}
      onReady={() => {
        routeNameRef.current = navigationRef.getCurrentRoute()?.name;
        setNavigationReady(true);
      }}
      onStateChange={() => {
        const previousRouteName = routeNameRef.current;
        const currentRouteName = navigationRef.getCurrentRoute()?.name;

        if (previousRouteName !== currentRouteName && currentRouteName) {
          analytics.screen(currentRouteName);
        }
        routeNameRef.current = currentRouteName;
      }}
    >
      {/* Your navigation stack */}
    </NavigationContainer>
  );
}
```

## Mobile lifecycle events

The SDK automatically tracks application lifecycle events following the Segment/RudderStack specification:

| Event | When | Properties |
| :- | :- | :- |
| `Application Installed` | First app launch (no stored version) | `version`, `build`, plus any captured attribution |
| `Application Updated` | App version or build changed since last launch | `version`, `build`, `previous_version`, `previous_build` |
| `Application Opened` | Every cold start and return from background | `version`, `build`, `from_background`, `url` (if deep linked) |
| `Application Backgrounded` | App transitions to background | `version`, `build` |
| `Deep Link Opened` | App launched or resumed via a deep or universal link | `url` |

Lifecycle events are enabled by default and require `asyncStorage` to be provided for accurate install/update detection. To disable:

```tsx theme={null}
options={{
  autocapture: {
    lifecycle: false,
  },
}}
```

### Opt-in lifecycle events

Two more are available but **off by default**, because enabling either changes how your app behaves:

| Event | When | Enable with |
| :- | :- | :- |
| `Application Foregrounded` | Background → active | `autocapture: { foregrounded: true }` |
| `Application Crashed` | Unhandled JavaScript error | `autocapture: { crashes: true }` |

`Application Foregrounded` marks the same transition as `Application Opened` with `from_background: true`, so enabling it doubles your foreground event volume without adding information. It exists for consumers that key on the Segment spec name directly — for example dashboards being migrated from Segment or RudderStack.

`Application Crashed` installs a global JavaScript error handler. Your previous handler always runs afterwards, so React Native's redbox and any crash reporter you already use keep working. It reports `message`, `name`, `stack` and `fatal`.

<Note>
  `autocapture: true` does **not** enable these two — they must be named individually. Crash tracking is **native only**: on React Native Web an uncaught error goes to `window.onerror` and never reaches the handler the SDK installs.
</Note>

### Push notification events

Push delivery is invisible to JavaScript without a native module, so these are not autocaptured. Call them from your own push handler:

```tsx theme={null}
const formo = useFormo();

// e.g. inside @react-native-firebase/messaging or expo-notifications callbacks
await formo.pushNotificationReceived({ campaign_id: 'spring', message_id: id });
await formo.pushNotificationTapped({ campaign_id: 'spring', message_id: id });
await formo.pushNotificationBounced({ campaign_id: 'spring', message_id: id });
```

They emit `Push Notification Received`, `Push Notification Tapped` and `Push Notification Bounced` respectively. Any properties are accepted; the Segment spec suggests `campaign_id`, `campaign_name`, `message_id`, `action`, `title` and `body`.

<Note>
  The SDK detects app version and build from `expo-application`, `react-native-device-info`, or the `app` option (in that order). If none are available, version and build will be empty strings.
</Note>

## Identify users

Call [`identify()`](/data/events/identify) after a user connects their wallet:

```tsx theme={null}
const formo = useFormo();

formo.identify({
  address: '0x1234...abcd',
  userId: 'optional-user-id',
  providerName: 'MetaMask',
});
```

<Note>
  When using Wagmi integration, wallet connections are automatically tracked. You only need to call `identify()` manually if you want to associate additional user data or use a custom user ID.
</Note>

## Track custom events

Track custom events with the [`track()`](/data/events/track) method:

```tsx theme={null}
const formo = useFormo();

// Basic custom event
formo.track('Swap Completed', {
  from_token: 'ETH',
  to_token: 'USDC',
  amount: '1.5',
});

// With reserved properties for analytics
formo.track('NFT Minted', {
  collection: 'founders-pass',
  tokenId: '001',
  revenue: 99.99,   // Reserved: revenue tracking
});

formo.track('Quest Completed', {
  questId: 'first_swap',
  points: 500,      // Reserved: points tracking
});

formo.track('Swap Executed', {
  fromToken: 'ETH',
  toToken: 'USDC',
  volume: 1.5,      // Reserved: volume tracking
});
```

## Deep link attribution

The SDK captures deep links automatically. On startup it reads the URL the app was opened with, and it listens for links that arrive while the app is running. For each link it records a [`Deep Link Opened`](#mobile-lifecycle-events) event and stores these parameters as the traffic source for the events that follow:

* UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`)
* Referral codes (`ref`, `referral`, `refcode`, `referrer_code`)

No extra code is needed. Put the parameters in your campaign links, for example: `myapp://home?utm_source=twitter&utm_campaign=launch&ref=friend123`

Parameters merge field by field: a later link without UTM parameters, such as `myapp://home`, does not erase the attribution that an earlier link stored. To turn capture off, see [Attribution](#attribution) and [Autocapture](#autocapture).

### Set the traffic source manually

If you receive links outside React Native's `Linking` API, for example from a push notification payload or a third-party deep linking SDK, pass the URL to `setTrafficSourceFromUrl()`:

```tsx theme={null}
const formo = useFormo();

formo.setTrafficSourceFromUrl('myapp://home?utm_source=push&utm_campaign=winback');
```

## Web-to-mobile attribution

Deep link attribution tells you where a user came from once the app is *already installed*. Web-to-mobile attribution answers the earlier question: **which site or campaign led someone to install the app in the first place.**

### Android

Google Play passes a referrer through the install, so this is captured reliably. Point your marketing links at your Play Store listing with a `referrer` parameter:

```
https://play.google.com/store/apps/details?id=<your.package>&referrer=utm_source%3Dexample.com%26utm_campaign%3Dspring
```

On first launch the SDK reads that value and attaches it to the [`Application Installed`](#mobile-lifecycle-events) event, and to the traffic source used by subsequent events:

```json theme={null}
{
  "event": "Application Installed",
  "properties": {
    "version": "1.1.0",
    "build": "42",
    "utm_source": "example.com",
    "utm_campaign": "spring"
  }
}
```

Requires `react-native-play-install-referrer` and a native rebuild; see [Installation](#optional-dependencies). The lookup runs once, on the first launch after install.

### iOS

<Note>
  Not supported. Apple does not expose an install-referrer API, so an install cannot be attributed to a referring website from within the SDK. This requires a third-party attribution service such as Branch or AppsFlyer. On iOS this capture is a no-op; deep link attribution still works normally.
</Note>

## Configuration

### Provider props

| Prop | Type | Required | Description |
| :- | :- | :- | :- |
| `writeKey` | String | Yes | Your Formo project write key. |
| `asyncStorage` | AsyncStorage | Yes | AsyncStorage instance for persistent storage. |
| `options` | Object | No | Configuration options, including attribution (see below). |
| `disabled` | Boolean | No | Disable the SDK entirely. |
| `onReady` | Function | No | Callback when SDK is initialized. |
| `onError` | Function | No | Callback when initialization fails. |

### Example

```tsx theme={null}
<FormoAnalyticsProvider
  writeKey="<YOUR_WRITE_KEY>"
  asyncStorage={AsyncStorage}
  options={{
    // Wagmi integration
    wagmi: {
      config: wagmiConfig,
      queryClient: queryClient,
    },

    // App metadata (enriches all events)
    app: {
      name: 'MyDeFiApp',
      version: '2.1.0',
      build: '42',
      bundleId: 'com.example.mydefiapp',
    },

    // Batching configuration
    flushAt: 20,              // Batch size (default: 20, max: 20)
    flushInterval: 30000,     // Auto-flush interval in ms (default: 30s)

    // Retry configuration
    retryCount: 3,            // Retry attempts (default: 3, max: 5)
    maxQueueSize: 500000,     // Max queue size in bytes (default: 500KB)

    // Tracking control
    tracking: true,           // Enable/disable tracking

    // Autocapture control
    autocapture: true,        // Enable/disable wallet autocapture

    // Attribution control
    attribution: {
      deeplinks: true,        // capture UTM/ref from deep links
      installReferrer: true,  // Play Install Referrer lookup (Android only)
    },

    // Logging
    logger: {
      enabled: true,
      levels: ['error', 'warn', 'info'],
    },

    // Custom API endpoint
    apiHost: 'https://your-proxy.com/api/ingest',
  }}
>
```

### App metadata

The SDK automatically detects app information from your app's native configuration:

* `app_name` - from your app's display name
* `app_version` - from your app's version (e.g., `2.1.0`)
* `app_build` - from your app's build number (e.g., `42`)
* `app_bundle_id` - from your bundle/package identifier

To override the auto-detected values, provide custom app information:

```tsx theme={null}
options={{
  app: {
    name: 'MyDeFiApp',        // Override detected app name
    version: '2.1.0',         // Override detected version
    build: '42',              // Override detected build
    bundleId: 'com.example.mydefiapp',
  },
}}
```

### Tracking control

Control tracking behavior for different environments or chains:

```tsx theme={null}
// Disable tracking entirely
options={{
  tracking: false,
}}

// Exclude specific chains (e.g., testnets)
options={{
  tracking: {
    excludeChains: [5, 11155111], // Goerli, Sepolia
  },
}}
```

### Autocapture

Control which wallet events are automatically captured:

```tsx theme={null}
// Enable all autocapture (default)
options={{
  autocapture: true,
}}

// Disable specific events
options={{
  autocapture: {
    connect: true,
    disconnect: true,
    signature: false,    // Disable signature tracking
    transaction: false,  // Disable transaction tracking
    chain: true,
    lifecycle: true,     // Application lifecycle events
    deepLinks: true,     // Deep Link Opened
  },
}}

// Disable all autocapture
options={{
  autocapture: false,
}}
```

Two options are **off** unless named explicitly, because enabling either changes how the app behaves. `autocapture: true` does not turn them on:

```tsx theme={null}
options={{
  autocapture: {
    foregrounded: true,  // Application Foregrounded (doubles foreground volume)
    crashes: true,       // Application Crashed (installs a global error handler)
  },
}}
```

| Option | Default | Controls |
| :- | :- | :- |
| `connect`, `disconnect`, `signature`, `transaction`, `chain` | `true` | Wallet events |
| `lifecycle` | `true` | `Application Installed` / `Updated` / `Opened` / `Backgrounded` |
| `deepLinks` | `true` | `Deep Link Opened` |
| `foregrounded` | **`false`** | `Application Foregrounded` |
| `crashes` | **`false`** | `Application Crashed` |

<Note>
  `autocapture.deepLinks` controls only the **event**. Parsing a deep link's UTM parameters into event context is separate, under [`attribution.deeplinks`](#attribution). Turning either off leaves the other working.
</Note>

### Attribution

Control [deep link attribution](#deep-link-attribution) and [web-to-mobile attribution](#web-to-mobile-attribution) capture. Both sources are on by default and can be turned off individually:

```tsx theme={null}
options={{
  attribution: {
    deeplinks: true,        // capture UTM/ref from deep links
    installReferrer: false, // skip the Play Install Referrer lookup
  },
}}
```

Passing `attribution: false` disables both.

### Logging

Enable debug logging during development:

```tsx theme={null}
options={{
  logger: {
    enabled: __DEV__,
    levels: ['error', 'warn', 'info', 'debug'],
  },
}}
```

| Log Level | Description |
| :- | :- |
| `error` | Error messages only. |
| `warn` | Warning and error messages. |
| `info` | Informative messages about normal operation. |
| `debug` | Detailed diagnostic information. |
| `log` | General log messages. |

### Ready callback

Execute code when the SDK is fully initialized:

```tsx theme={null}
<FormoAnalyticsProvider
  writeKey="<YOUR_WRITE_KEY>"
  asyncStorage={AsyncStorage}
  onReady={(sdk) => {
    console.log('Formo SDK ready!');
    // Auto-identify or perform other initialization
  }}
  onError={(error) => {
    console.error('Formo SDK failed to initialize:', error);
  }}
>
```

## Consent management

Comply with privacy regulations using built-in consent management:

```tsx theme={null}
const formo = useFormo();

// Check if user has opted out
if (formo.hasOptedOutTracking()) {
  console.log('User has opted out');
}

// Opt out of tracking (stops all tracking, clears queue)
formo.optOutTracking();

// Opt back into tracking
formo.optInTracking();
```

<Tip>
  When opting out, track the opt-out event *before* calling `optOutTracking()` so it gets recorded. When opting in, call `optInTracking()` first, then track the opt-in event.
</Tip>

## Wagmi integration

When Wagmi integration is enabled, the SDK automatically tracks:

| Event Type | Without QueryClient | With QueryClient |
| :- | :- | :- |
| Connect | Tracked | Tracked |
| Disconnect | Tracked | Tracked |
| Chain Change | Tracked | Tracked |
| Signatures | Not tracked | Tracked |
| Transactions | Not tracked | Tracked |

<Tip>
  Use the same `QueryClient` instance for both Wagmi and Formo to avoid creating multiple cache instances.
</Tip>

## Mobile context

The SDK automatically enriches every event with mobile-specific context:

| Field | Description |
| :- | :- |
| `os_name` | Operating system (iOS, Android). |
| `os_version` | OS version number. |
| `device_model` | Device model (iPhone 14 Pro, Pixel 8). |
| `device_manufacturer` | Device manufacturer (Apple, Google, Samsung). |
| `device_type` | Device type (mobile, tablet). |
| `user_agent` | Device user agent. Taken from the platform when available, otherwise synthesized from the device and OS above. Formo resolves `device` and `os` from it — and reports `browser` as `unknown`, since the synthesized string deliberately carries no browser token. |
| `screen_width` | Screen width in logical points (not raw pixels — see `screen_density`). |
| `screen_height` | Screen height in logical points. |
| `screen_density` | Pixel density (devicePixelRatio). |
| `locale` | Device language setting. |
| `timezone` | Device timezone. |
| `network_wifi` | Whether connected to WiFi. |
| `network_cellular` | Whether connected to cellular. |
| `network_carrier` | Mobile carrier name (when available). |
| `app_name` | Your app name. |
| `app_version` | Your app version. |
| `app_build` | Your app build number. |

## Session management

The SDK assigns a `session_id` to every event. A session groups one user's burst of activity into a single visit, and powers the Sessions, Session duration, and Bounce rate metrics on your dashboard. No configuration is required.

| Behavior | Detail |
| :- | :- |
| Session start | The first event after install, or the first event after a session expires. |
| Session timeout | 30 minutes of inactivity. The next event after that starts a new session. |
| Persistence | A session survives app restarts; it expires on inactivity, not on close. |
| Reset | `reset()` keeps the current session. `optOutTracking()` ends it and clears the device identity. |

<Note>
  This differs from the web SDK, where `session_id` is derived server-side as a daily-changing value. That derivation relies on the request's origin, IP and browser user agent (signals a native app does not provide meaningfully), so the mobile SDK supplies its own session instead.
</Note>

Call `reset()` when a user logs out:

```tsx theme={null}
const formo = useFormo();

// Clear the user and wallet identity. The device identity, the session
// and the stored attribution are kept.
formo.reset();
```

## Manual event flushing

Force flush pending events (useful before app backgrounding):

```tsx theme={null}
const formo = useFormo();

await formo.flush();
```

<Note>
  The SDK automatically flushes events when the app goes to background.
</Note>

## Verification

To verify the SDK is working:

1. Enable debug logging in development
2. Trigger a screen view or custom event
3. Check the console for event logs
4. Verify events appear in the [Activity page](https://app.formo.so) on your Formo dashboard

## Peer dependencies

| Package | Version | Required |
| :- | :- | :- |
| `react` | >=18.0.0 | Yes |
| `react-native` | >=0.70.0 | Yes |
| `@react-native-async-storage/async-storage` | >=1.17.0 | Yes |
| `wagmi` | >=2.0.0 | No (for Wagmi integration) |
| `@tanstack/react-query` | >=5.0.0 | No (for signature/transaction tracking) |
| `expo-application` | >=5.0.0 | No (for auto version/build detection in Expo) |
| `expo-device` | >=5.0.0 | No (for device info in Expo) |
| `react-native-device-info` | >=10.0.0 | No (for auto version/build detection in bare RN) |
| `react-native-play-install-referrer` | >=1.1.8 | No (for Android web-to-mobile install attribution) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.