npm package: @layers/expo
Description: Layers Analytics Expo SDK — convenience wrapper with config plugin
Author: Layers Team
Latest version: 2.2.0

# Layers Expo SDK

`@layers/expo` is the Layers analytics SDK for Expo managed workflow projects. It wraps `@layers/react-native` and adds Expo-specific integrations: a config plugin for native setup automation, `expo-tracking-transparency` for ATT, `expo-linking` for deep links, `expo-clipboard` for clipboard attribution, and React context/hooks for idiomatic usage.

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

## Requirements

- Expo SDK 50.0.0+
- React Native 0.73.0+
- React 18.0+

## Installation

```bash
npx expo install @layers/expo
```

### Optional Peer Dependencies

For full functionality, install these Expo packages:

```bash
npx expo install expo-tracking-transparency expo-linking expo-clipboard
```

- **expo-tracking-transparency** -- ATT permission dialog (iOS)
- **expo-linking** -- Deep link handling
- **expo-clipboard** -- Clipboard attribution for deferred deep links (iOS)

## Quick Start

### 1. Add the Config Plugin

In your `app.config.js` or `app.json`:

```js
export default {
  expo: {
    plugins: [
      [
        '@layers/expo',
        {
          ios: {
            attUsageDescription: 'We use this to show you relevant content.',
            urlSchemes: ['myapp'],
            associatedDomains: ['myapp.com']
          },
          android: {
            intentFilters: [
              { scheme: 'myapp', host: 'open' },
              { scheme: 'https', host: 'myapp.com', pathPrefix: '/app' }
            ]
          }
        }
      ]
    ]
  }
};
```

Then run prebuild:

```bash
npx expo prebuild
```

### 2. Initialize with the Provider

```tsx
import { LayersProvider } from '@layers/expo';

export default function App() {
  return (
    <LayersProvider
      config={{
        appId: 'your-app-id',
        environment: 'production'
      }}
      requestTracking={true}
      enableDeepLinks={true}
      onDeepLink={(data) => console.log('Deep link:', data.url)}
      onError={(error) => console.error('Layers error:', error)}
    >
      <MyApp />
    </LayersProvider>
  );
}
```

### 3. Use Hooks in Components

```tsx
import { useLayersScreen, useLayersTrack } from '@layers/expo';

function SignupButton() {
  const track = useLayersTrack();

  return <Button title="Sign Up" onPress={() => track('signup_click', { source: 'hero' })} />;
}

function ProfileScreen() {
  const screen = useLayersScreen();

  useEffect(() => {
    screen('Profile');
  }, []);

  return <View />;
}
```

## Config Plugin

The Layers Expo config plugin automates native project configuration via `npx expo prebuild`.

### Plugin Options

```typescript
interface LayersExpoPluginProps {
  ios?: {
    /** Custom ATT usage description for the permission dialog. */
    attUsageDescription?: string;
    /** URL schemes for deep linking (e.g., ['myapp']). */
    urlSchemes?: string[];
    /** Associated domains for Universal Links (e.g., ['myapp.com']). */
    associatedDomains?: string[];
    /** Additional SKAdNetwork identifiers to register. */
    skAdNetworkIds?: string[];
    /** Include default SKAdNetwork IDs (16 major ad networks). Default: true. */
    includeDefaultSKAdNetworkIds?: boolean;
  };
  android?: {
    /** Intent filters for deep linking. */
    intentFilters?: Array<{
      scheme: string; // e.g., 'myapp' or 'https'
      host?: string; // e.g., 'myapp.com'
      pathPrefix?: string; // e.g., '/app'
    }>;
  };
}
```

### What the Plugin Configures

**iOS (Info.plist)**:

- `NSUserTrackingUsageDescription` -- ATT usage description (defaults to a generic message if not provided)
- `SKAdNetworkItems` -- SKAdNetwork IDs from Meta, Google, TikTok, Snapchat, X, Unity, AppLovin, IronSource (16 default IDs) plus any custom IDs you specify
- `CFBundleURLTypes` -- Custom URL schemes for deep linking

**iOS (Entitlements)**:

- `com.apple.developer.associated-domains` -- Associated domains for Universal Links (auto-prefixed with `applinks:`)

**Android (AndroidManifest.xml)**:

- Intent filters on `.MainActivity` for deep link and App Link handling
- `android:autoVerify="true"` is set automatically for HTTPS intent filters

### Default SKAdNetwork IDs

The plugin includes 16 SKAdNetwork IDs by default from these networks:

- Meta/Facebook
- Google/YouTube
- TikTok
- Snapchat
- Twitter/X
- Unity Ads
- AppLovin
- IronSource

Set `includeDefaultSKAdNetworkIds: false` to disable defaults.

## React Provider & Hooks

### LayersProvider

```tsx
import { LayersProvider } from '@layers/expo';

<LayersProvider
  config={LayersRNConfig}
  requestTracking?: boolean     // Request ATT on mount. Default: false
  enableDeepLinks?: boolean     // Listen for deep links. Default: true
  onDeepLink?: (data) => void   // Deep link callback
  onError?: (error) => void     // Error callback (init + runtime)
>
  {children}
</LayersProvider>
```

The provider:

1. Creates a `LayersReactNative` instance
2. Calls `init()` with AsyncStorage persistence
3. Optionally requests ATT permission via `expo-tracking-transparency`
4. Optionally sets up deep link listening via `expo-linking`
5. Forwards runtime errors (from track/screen/flush) to `onError`
6. Shuts down the SDK on unmount

### useLayers

```typescript
function useLayers(): { isReady: boolean; layers: LayersReactNative | null };
```

Access the SDK context. Returns `isReady: false` until initialization completes.

```tsx
function MyComponent() {
  const { isReady, layers } = useLayers();

  if (!isReady) return <Text>Loading...</Text>;

  return <Button title="Track" onPress={() => layers?.track('button_press')} />;
}
```

### useRequiredLayers

```typescript
function useRequiredLayers(): LayersReactNative;
```

Returns the SDK instance, throwing an error if not yet initialized or used outside a `<LayersProvider>`. Use when your component requires the SDK to be available.

```tsx
function CheckoutButton() {
  const layers = useRequiredLayers();

  return (
    <Button
      title="Checkout"
      onPress={() => layers.track('checkout_started', { cart_value: 49.99 })}
    />
  );
}
```

### useLayersTrack

```typescript
function useLayersTrack(): (eventName: string, properties?: EventProperties) => void;
```

Returns a stable, memoized track function. No-op until the SDK initializes.

```tsx
const track = useLayersTrack();
track('signup_click', { source: 'hero' });
```

### useLayersScreen

```typescript
function useLayersScreen(): (screenName: string, properties?: EventProperties) => void;
```

Returns a stable, memoized screen tracking function. No-op until the SDK initializes.

```tsx
const screen = useLayersScreen();
useEffect(() => {
  screen('Profile');
}, []);
```

## Imperative API

All exports from `@layers/react-native` are re-exported from `@layers/expo`. You can use the SDK without the provider:

```typescript
import { LayersReactNative } from '@layers/expo';

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

layers.track('event_name', { key: 'value' });
layers.screen('ScreenName');
layers.setAppUserId('user_123');
await layers.setUserProperties({ plan: 'premium' });
await layers.setConsent({ analytics: true, advertising: false });
await layers.flush();
layers.shutdown();
```

See the [@layers/react-native README](https://www.npmjs.com/package/@layers/react-native) for the full imperative API documentation.

## ATT (App Tracking Transparency) -- iOS

### Via Provider

Set `requestTracking={true}` on `<LayersProvider>` to automatically request ATT permission on mount.

### Via Expo Functions

```typescript
import { getExpoTrackingStatus, requestExpoTrackingPermission } from '@layers/expo';

// Request permission (auto-updates device info and consent on the Layers instance)
const status = await requestExpoTrackingPermission(layers);
// Returns: 'authorized' | 'denied' | 'restricted' | 'not_determined'

// Check current status without prompting
const currentStatus = await getExpoTrackingStatus();
```

When `layers` is passed, `requestExpoTrackingPermission` automatically:

- Collects IDFA if authorized
- Updates device info with IDFA and ATT status
- Sets advertising consent based on the result

### Via SDK Instance

```typescript
const status = await layers.requestTrackingPermission();
```

This also auto-detects `expo-tracking-transparency` and prefers it when available.

## Deep Links

### Via Provider

Set `enableDeepLinks={true}` (default) and provide an `onDeepLink` callback:

```tsx
<LayersProvider
  config={config}
  enableDeepLinks={true}
  onDeepLink={(data) => {
    console.log('Deep link:', data.url);
    navigation.navigate(data.path);
  }}
>
```

### Via Expo Functions

```typescript
import { parseDeepLink, setupExpoDeepLinkListener } from '@layers/expo';

const cleanup = await setupExpoDeepLinkListener((data) => {
  console.log('Deep link:', data.url);
  console.log('Source:', data.queryParams.utm_source);
});

// Later: cleanup()
```

### Auto-Tracking

The SDK automatically tracks `deep_link_opened` events (configurable via `autoTrackDeepLinks` in the config). This runs in addition to your `onDeepLink` callback.

## Clipboard Attribution -- iOS

```typescript
import { readExpoClipboardAttribution } from '@layers/expo';

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

Uses `expo-clipboard` under the hood. The SDK's `init()` also reads clipboard attribution automatically when enabled by remote config.

## Expo Router Integration

For automatic screen tracking with Expo Router:

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

function RootLayout() {
  const layers = useRequiredLayers();
  useLayersExpoRouterTracking(layers, usePathname, useGlobalSearchParams);

  return <Stack />;
}
```

## SKAdNetwork (SKAN) -- iOS

SKAN is auto-configured from the server's remote config. No additional setup is required beyond including SKAdNetwork IDs in your config plugin (which is done by default).

Access the auto-configured SKAN manager:

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

For manual SKAN configuration, see the `@layers/react-native` documentation.

## Full Export List

### From `@layers/react-native`

```typescript
// Core
(LayersReactNative, LayersError);

// Types
(LayersRNConfig,
  ConsentState,
  DeviceContext,
  Environment,
  EventProperties,
  UserProperties,
  DeepLinkData,
  ClipboardAttribution,
  SKANConversionRule,
  SKANPresetConfig,
  SKANMetrics,
  ATTStatus);

// SKAN
SKANManager;

// ATT
(getATTStatus, requestTrackingAuthorization, isATTAvailable, getAdvertisingId, getVendorId);

// Deep Links
(parseDeepLink, setupDeepLinkListener);

// Utilities
(getOrSetInstallId, readClipboardAttribution, useLayersExpoRouterTracking);
```

### Expo-Specific

```typescript
// Config plugin
withLayers (default export from '@layers/expo/plugin')
LayersExpoPluginProps

// ATT
requestExpoTrackingPermission, getExpoTrackingStatus

// Deep Links
setupExpoDeepLinkListener

// Clipboard
readExpoClipboardAttribution

// React
LayersProvider, LayersProviderProps,
useLayers, useRequiredLayers, useLayersTrack, useLayersScreen
```