# React Native and Expo

Track screens, goals and signed-in users in React Native and Expo apps, attributed to the deep link or Play Store campaign behind each install.

`@productlense/react-native` sends screen views, goals and `identify` from your app to ProductLense. It has no native modules, so it works in Expo Go, a development build and bare React Native, and it is about 3 KB gzipped. Every report works for app traffic the way it does for the web: the Hostname breakdown shows your bundle id next to your website, so you can see web and app side by side.

## 1. Add your app ID

In ProductLense, open **Settings → General → App IDs** and add your iOS bundle identifier and Android application id, for example `com.example.app`. Events from any other app id are rejected, the same way hostnames protect your website.

## 2. Install

```bash title="Expo"
npx expo install @productlense/react-native @react-native-async-storage/async-storage
```

```bash title="Bare React Native"
npm install @productlense/react-native @react-native-async-storage/async-storage
```

AsyncStorage keeps the install id, the opt-out choice and unsent events across launches. Without it, ProductLense still works but counts each launch as a new install.

## 3. Start ProductLense once

```tsx title="App.tsx" {1-2,5-9}
import AsyncStorage from '@react-native-async-storage/async-storage';
import ProductLense from '@productlense/react-native';
import * as Application from 'expo-application';

ProductLense.init({
  siteId: 'YOUR_SITE_ID',
  appId: 'com.example.app',
  storage: AsyncStorage,
  installReferrer: () => Application.getInstallReferrerAsync(), // Android only; optional
});
```

`siteId` is on **Settings → Snippet**. Call `init` at module level or in your root component, before the first screen renders. Every other call made before `init` is ignored, and no call ever throws.

## 4. Track screens

### React Navigation

```tsx title="App.tsx" {3,8}
import { NavigationContainer, createNavigationContainerRef } from '@react-navigation/native';
import ProductLense from '@productlense/react-native';
import { useProductLenseNavigation } from '@productlense/react-native/navigation';

const navigationRef = createNavigationContainerRef();

export default function App() {
  useProductLenseNavigation(navigationRef, ProductLense);
  return <NavigationContainer ref={navigationRef}>{/* screens */}</NavigationContainer>;
}
```

Each route change is one screen view named after the route, so a `Settings` route shows as the page `/Settings`.

### Expo Router

```tsx title="app/_layout.tsx" {3,6}
import { Stack } from 'expo-router';
import ProductLense from '@productlense/react-native';
import { useProductLenseScreens } from '@productlense/react-native/expo-router';

export default function RootLayout() {
  useProductLenseScreens(ProductLense);
  return <Stack />;
}
```

Screens are named after the route path, such as `/settings/billing`.

### By hand

```ts
ProductLense.screen('Checkout');
```

## 5. Goals and users

```ts
ProductLense.track('signup', { plan: 'pro' }); // a goal, with up to 2 KB of flat props
ProductLense.identify(user.id, { name: user.name, email: user.email });
```

Call `identify` after sign-in with the same user id your billing provider stores on the customer. App visits, web visits and payments then join up into one customer, so a subscription bought on your website is credited to the campaign that brought the app install. See [how payments are attributed](/docs/revenue/attribution).

## In-app purchases

Connect [RevenueCat](/docs/revenue/revenuecat), then pass ProductLense's install id to it once, after both SDKs are configured:

```ts
import Purchases from 'react-native-purchases';

Purchases.setAttributes(await ProductLense.revenueCatAttributes());
```

This sets the `pl_install_id` subscriber attribute, so each App Store or Google Play purchase lands on the channel that brought the install, even before the user signs in. Payments your backend takes through Stripe, Razorpay or any other [billing connection](/docs/revenue/connect-billing) are attributed through `identify`.

## Attribution

- **Deep links**: links that open your app (`example://open?utm_source=newsletter`, universal links and app links) are read automatically. Only `utm_*`, `ref`, `via`, `source` and click ids leave the device; tokens and other parameters do not.
- **Play Store**: on Android, pass `installReferrer` as above. The `utm_*` tags in your Play Store link are applied to the first screen of the install.
- **App Store**: Apple does not share a referrer. Installs from the App Store count as Direct unless they open through a tagged universal link.

Attribution then follows the same first-touch rules as the web; see [How ProductLense counts](/docs/get-started/how-productlense-counts#channels).

## Privacy

- On first launch the SDK creates a random install id. It is not the advertising id (IDFA or AAID), and ProductLense never reads device identifiers, so no App Tracking Transparency prompt is needed for ProductLense.
- ProductLense stores a hash of the install id, never the id itself. It stays the same until the app is deleted or the user opts out, which is what lets reports count returning users and retention.
- `ProductLense.optOut()` stops collection, deletes unsent events and forgets the install id. `ProductLense.optIn()` resumes with a new id. Offer it in your privacy settings if your users expect it.
- Apple's App Store privacy label: declare **Product Interaction** (screen views and goals) as **Analytics**, not linked to the user's identity unless you call `identify`, and **not used for tracking**.

## Delivery

Events are sent in batches every 10 seconds, when 20 are waiting and when the app goes to the background. Offline, they wait on the device (up to 500) and are retried with backoff. The SDK only uses `fetch`, so a proxy works the same as for the web: pass `apiHost: 'https://stats.example.com'`.

## Verify your installation

1. Run the app on a device or simulator and open a few screens.
2. Open the Live page in the ProductLense dashboard. Your device appears within a few seconds.
3. Open **Events**: each screen is a `pageview` row with your bundle id as the hostname and `iOS app` or `Android app` as the browser.
4. Pass `debug: true` to `init` to log dropped events and failed sends.

If nothing appears, check that the app id is listed in Settings and follow [Verify your installation](/docs/install/verify).
