npm package: @layers/react-native
Description: Layers Analytics React Native SDK — thin wrapper over Rust core via WASM
Author: Layers Team
Latest version: 2.2.0

# Layers React Native SDK

`@layers/react-native` is the Layers analytics SDK for React Native apps. It provides event tracking, screen tracking, user identification, App Tracking Transparency (ATT), SKAdNetwork (SKAN), deep link handling, clipboard-based deferred deep links, consent management, and automatic lifecycle and connectivity handling.

Use this package for **bare React Native** projects. For **Expo managed workflow** projects, use [`@layers/expo`](https://www.npmjs.com/package/@layers/expo) instead.

## Requirements

- React Native 0.70.0+
- React 18.0+
- iOS 14.0+ / Android API 21+

## Installation

```bash
npm install @layers/react-native
# or
yarn add @layers/react-native
```

### Recommended Peer Dependencies

```bash
npm install @react-native-async-storage/async-storage @react-native-community/netinfo
```

These are optional but strongly recommended:

- **@react-native-async-storage/async-storage** (>=1.21.0) -- Enables persistent event storage across app restarts. Without it, events are stored in memory only.
- **@react-native-community/netinfo** (>=11.0.0) -- Enables automatic flush when the device reconnects to the network.
- **@react-native-clipboard/clipboard** (>=1.13.0) -- Enables clipboard attribution for iOS deferred deep links.

### iOS Setup

```bash
cd ios && pod install
```

Add `NSUserTrackingUsageDescription` to your `Info.plist` if you plan to use ATT.

## Quick Start

```typescript
import { LayersReactNative } from '@layers/react-native';

// Create and initialize
const layers = new LayersReactNative({
  appId: 'your-app-id',
  environment: 'production'
});
await layers.init();

// Track events
layers.track('button_tapped', { button_name: 'signup' });

// Track screen views
layers.screen('Home');

// Identify users
layers.setAppUserId('user_123');
```

## Configuration

### LayersRNConfig

```typescript
interface LayersRNConfig {
  appId: string;
  environment: 'development' | 'staging' | 'production';
  appUserId?: string;
  enableDebug?: boolean; // default: false
  baseUrl?: string; // default: "https://in.layers.com"
  flushIntervalMs?: number; // default: 30000
  flushThreshold?: number; // default: 10
  maxQueueSize?: number; // default: 1000
  autoTrackAppOpen?: boolean; // default: true
  autoTrackDeepLinks?: boolean; // default: true
}
```

| Option               | Type          | Default                   | Description                                      |
| -------------------- | ------------- | ------------------------- | ------------------------------------------------ |
| `appId`              | `string`      | _required_                | Your Layers application identifier.              |
| `environment`        | `Environment` | _required_                | `'development'`, `'staging'`, or `'production'`. |
| `appUserId`          | `string`      | `undefined`               | Optional user ID to set at construction time.    |
| `enableDebug`        | `boolean`     | `false`                   | Enable verbose console logging.                  |
| `baseUrl`            | `string`      | `"https://in.layers.com"` | Custom ingest API endpoint.                      |
| `flushIntervalMs`    | `number`      | `30000`                   | Automatic flush interval in milliseconds.        |
| `flushThreshold`     | `number`      | `10`                      | Queue size that triggers an automatic flush.     |
| `maxQueueSize`       | `number`      | `1000`                    | Maximum events in the queue before dropping.     |
| `autoTrackAppOpen`   | `boolean`     | `true`                    | Automatically track `app_open` on init.          |
| `autoTrackDeepLinks` | `boolean`     | `true`                    | Automatically track `deep_link_opened` events.   |

## Core API

### Constructor & Initialization

```typescript
const layers = new LayersReactNative(config: LayersRNConfig);
await layers.init();
```

The constructor creates the SDK instance with an in-memory queue. Calling `init()` upgrades to AsyncStorage-backed persistence, collects device info, fetches remote config, reads clipboard attribution (iOS), fires `app_open`, and sets up auto-tracking.

You can call `track()` and `screen()` before `init()` completes -- events are queued in memory.

### Event Tracking

```typescript
track(eventName: string, properties?: EventProperties): void
```

```typescript
layers.track('purchase_completed', {
  product_id: 'sku_123',
  price: 9.99,
  currency: 'USD'
});
```

### Screen Tracking

```typescript
screen(screenName: string, properties?: EventProperties): void
```

```typescript
layers.screen('ProductDetail', { product_id: 'sku_123' });
```

### User Identity

```typescript
// Set the app user ID (set-user-once: ignored if already set)
setAppUserId(appUserId: string): void

// Clear the current user ID (allows setting a new one)
clearAppUserId(): void

// Get the current user ID
getAppUserId(): string | undefined

// Set user properties
async setUserProperties(properties: UserProperties): Promise<void>
```

```typescript
// After login
layers.setAppUserId('user_123');
await layers.setUserProperties({
  email: 'user@example.com',
  plan: 'premium'
});

// On logout
layers.clearAppUserId();
```

> **Set-user-once semantics**: Once `setAppUserId()` is called, subsequent calls are ignored until `clearAppUserId()` is called. This prevents accidental user ID changes.

### Consent Management

```typescript
async setConsent(consent: ConsentState): Promise<void>
getConsentState(): ConsentState
```

```typescript
interface ConsentState {
  analytics?: boolean;
  advertising?: boolean;
}
```

```typescript
// User accepts all tracking
await layers.setConsent({ analytics: true, advertising: true });

// User denies advertising
await layers.setConsent({ analytics: true, advertising: false });

// Read current consent
const consent = layers.getConsentState();
```

### Flush & Shutdown

```typescript
// Flush queued events to the server
async flush(): Promise<void>

// Shut down the SDK (removes listeners, stops timers)
shutdown(): void
```

### Session & Device

```typescript
// Get the current session ID
getSessionId(): string

// Override device context fields
setDeviceInfo(deviceInfo: DeviceContext): void
```

### Error Handling

```typescript
// Register an error listener
on(event: 'error', listener: (error: Error) => void): this

// Remove an error listener
off(event: 'error', listener: (error: Error) => void): this
```

```typescript
layers.on('error', (error) => {
  console.error('Layers error:', error.message);
  // Forward to your crash reporting service
});
```

## App Tracking Transparency (ATT) -- iOS

### Integrated ATT (Recommended)

The `requestTrackingPermission()` method on the SDK instance handles the full ATT flow:

```typescript
const status = await layers.requestTrackingPermission();
// Returns: 'authorized' | 'denied' | 'restricted' | 'not_determined'
```

This method automatically:

1. Shows the ATT dialog (or uses `expo-tracking-transparency` if available)
2. Collects IDFA if authorized
3. Updates device context with IDFA and ATT status
4. Sets advertising consent based on the result

### Standalone ATT Functions

For more granular control, use the exported functions:

```typescript
import {
  getATTStatus,
  getAdvertisingId,
  getVendorId,
  isATTAvailable,
  requestTrackingAuthorization
} from '@layers/react-native';

const status = await getATTStatus();
const isAvailable = await isATTAvailable();
const idfa = await getAdvertisingId(); // null if not authorized
const idfv = await getVendorId(); // always available on iOS
```

### ATTStatus

```typescript
type ATTStatus = 'not_determined' | 'restricted' | 'denied' | 'authorized';
```

> **Important**: Add `NSUserTrackingUsageDescription` to your `Info.plist`.

## SKAdNetwork (SKAN) -- iOS

SKAN is auto-configured from the server's remote config. The SDK creates a `SKANManager` instance and automatically forwards every `track()` call through the SKAN rule engine.

### Accessing the Auto-Configured Manager

```typescript
const skanManager = layers.getSkanManager();
if (skanManager) {
  const metrics = skanManager.getMetrics();
  console.log('SKAN value:', metrics.currentValue);
  console.log('SKAN preset:', metrics.currentPreset);
}
```

### Manual SKAN Configuration

If you need to configure SKAN manually instead of relying on remote config:

```typescript
import { SKANManager } from '@layers/react-native';

const skan = new SKANManager((data) => {
  console.log(`Conversion value updated: ${data.previousValue} -> ${data.newValue}`);
});

// Use a built-in preset
skan.setPreset('subscriptions'); // or 'engagement' or 'iap'
await skan.initialize();

// Or define custom rules
skan.setCustomRules([
  {
    eventName: 'purchase_success',
    conditions: { revenue: { '>=': 10 } },
    conversionValue: 63,
    coarseValue: 'high',
    priority: 10
  },
  {
    eventName: 'trial_start',
    conversionValue: 20,
    priority: 5
  }
]);
await skan.initialize();

// Process events manually
await skan.processEvent('purchase_success', { revenue: 49.99 });
```

### SKANConversionRule

```typescript
interface SKANConversionRule {
  eventName: string;
  conditions?: Record<string, unknown>; // Operator-based: { '>': 10, '<': 100 }
  conversionValue: number; // 0-63 (SKAN 3.0) or 0-7 (SKAN 4.0)
  coarseValue?: 'low' | 'medium' | 'high'; // SKAN 4.0 only
  lockWindow?: boolean; // SKAN 4.0 only
  priority?: number;
  description?: string;
}
```

### SKANMetrics

```typescript
interface SKANMetrics {
  isSupported: boolean;
  version: string;
  currentValue: number | null;
  currentPreset: string | null;
  ruleCount: number;
  evaluationCount: number;
}
```

### Available Presets

- **`subscriptions`** -- Optimized for subscription apps (trial start, subscription start/renew)
- **`engagement`** -- Optimized for engagement-driven apps (content views, sessions, bookmarks)
- **`iap`** -- Optimized for in-app purchase revenue tracking (purchase tiers by revenue)

## Deep Links

### Auto-Tracking (Default)

When `autoTrackDeepLinks` is `true` (default), the SDK automatically tracks a `deep_link_opened` event for every incoming deep link. The event includes the full URL, scheme, host, path, and all query parameters (UTM, click IDs) as flat top-level properties.

### Manual Deep Link Handling

```typescript
import { parseDeepLink, setupDeepLinkListener } from '@layers/react-native';

const unsubscribe = setupDeepLinkListener((data) => {
  console.log('Deep link:', data.url);
  console.log('Host:', data.host);
  console.log('Path:', data.path);
  console.log('UTM Source:', data.queryParams.utm_source);
  console.log('Click ID:', data.queryParams.fbclid);
});

// Later: unsubscribe()
```

The listener handles both:

- **Initial URL** (cold start): Checks `Linking.getInitialURL()` on setup.
- **Subsequent URLs** (warm start): Listens to `Linking` `url` events.

### parseDeepLink

```typescript
function parseDeepLink(url: string): DeepLinkData | null;
```

```typescript
interface DeepLinkData {
  url: string;
  scheme: string;
  host: string;
  path: string;
  queryParams: Record<string, string>;
  timestamp: number;
}
```

## Clipboard Attribution -- iOS

On iOS, the SDK reads the clipboard on first launch (during `init()`) for a Layers click URL. If found, the click URL and click ID are included as properties on the `app_open` event. This is controlled by the server's remote config (`clipboard_attribution_enabled`).

For manual reading:

```typescript
import { readClipboardAttribution } from '@layers/react-native';

const data = await readClipboardAttribution();
if (data) {
  console.log('Click URL:', data.clickUrl);
  console.log('Click ID:', data.clickId);
}
```

```typescript
interface ClipboardAttribution {
  clickUrl: string;
  clickId: string;
}
```

Requires `@react-native-clipboard/clipboard` as a peer dependency.

## Expo Router Integration

For automatic screen tracking with Expo Router:

```typescript
import { useLayersExpoRouterTracking } from '@layers/react-native';
import { usePathname, useGlobalSearchParams } from 'expo-router';

function RootLayout() {
  useLayersExpoRouterTracking(layers, usePathname, useGlobalSearchParams);

  return <Stack />;
}
```

This hook automatically tracks a `screen` event every time the Expo Router pathname changes, including route parameters as event properties.

## Install ID

The SDK generates and persists a unique install ID via AsyncStorage:

```typescript
import { getOrSetInstallId } from '@layers/react-native';

const installId = await getOrSetInstallId();
```

This ID persists across app sessions and is included in the device context.

## Automatic Behaviors

- **app_open event**: Tracked on `init()` with clipboard attribution (iOS).
- **deep_link_opened event**: Tracked automatically for all incoming deep links.
- **Background flush**: Events are flushed when the app goes to background/inactive.
- **Foreground flush**: Events are flushed when the app becomes active.
- **Network reconnect flush**: Events are flushed when the device reconnects (requires `@react-native-community/netinfo`).
- **Periodic flush**: Events are flushed on a timer (configurable).
- **Remote config**: Server configuration is fetched during init.
- **SKAN auto-config**: SKAN preset/rules from remote config are applied automatically (iOS).
- **Device context**: Platform, OS version, device model, locale, screen size, timezone, IDFV (iOS), and install ID are collected automatically.
- **Event persistence**: Events are persisted via AsyncStorage (if available) and rehydrated on restart.

## TypeScript Types

All types are exported from the package:

```typescript
import type {
  ATTStatus,
  ClipboardAttribution,
  ConsentState,
  DeepLinkData,
  DeviceContext,
  Environment,
  ErrorListener,
  EventProperties,
  LayersRNConfig,
  SKANConversionRule,
  SKANMetrics,
  SKANPresetConfig,
  UserProperties
} from '@layers/react-native';
```