@layers/react-native
React Native SDK for Layers Analytics with ATT, SKAN, deep link support, and offline queueing.
Install
npm install @layers/react-native
pnpm add @layers/react-native
Peer Dependencies
react | Yes | -- |
react-native | Yes | -- |
@react-native-async-storage/async-storage | Optional | Events are only persisted in memory; lost on app kill |
@react-native-community/netinfo | Optional | SDK assumes always-online; no offline queueing |
npm install @react-native-async-storage/async-storage @react-native-community/netinfo
Quick Start
import { LayersReactNative } from '@layers/react-native';
const layers = new LayersReactNative({
apiKey: 'your-api-key',
appId: 'your-app-id',
environment: 'production',
enableDebug: __DEV__
});
await layers.init();
await layers.track('app_open', { source: 'push_notification' });
await layers.screen('home');
layers.setAppUserId('user-123');
Configuration
All fields for LayersRNConfig:
apiKey | string | required | API key from the Layers dashboard |
appId | string | required | Application identifier |
environment | 'development' | 'staging' | 'production' | required | Deployment environment |
appUserId | string | undefined | Pre-set user ID at init time |
enableDebug | boolean | false | Verbose console logging |
baseUrl | string | https://in.layers.com | Override the ingest endpoint |
flushIntervalMs | number | 30000 | Periodic flush interval (ms) |
flushThreshold | number | 20 | Queue depth that triggers auto-flush |
maxQueueSize | number | 10000 | Max events in queue before dropping |
User Identity
The SDK uses set-user-once semantics. Once setAppUserId() is called, subsequent calls are ignored until clearAppUserId() is called:
layers.setAppUserId('user-123');
layers.setAppUserId('user-456');
layers.clearAppUserId();
layers.setAppUserId('user-456');
ATT (App Tracking Transparency)
iOS 14.5+ requires asking for tracking permission before accessing IDFA.
import {
getATTStatus,
getAdvertisingId,
getVendorId,
isATTAvailable,
requestTrackingAuthorization
} from '@layers/react-native';
const available = await isATTAvailable();
const status = await requestTrackingAuthorization();
const currentStatus = await getATTStatus();
const idfa = await getAdvertisingId();
const idfv = await getVendorId();
if (status === 'authorized') {
await layers.setConsent({ analytics: true, advertising: true });
} else {
await layers.setConsent({ analytics: true, advertising: false });
}
ATT functions require the native LayersATT module. On Android or when the native module is not linked, they return safe defaults ('not_determined', null, false).
SKAN (SKAdNetwork)
Manage iOS SKAdNetwork conversion values with a rule-based engine:
import { SKANManager } from '@layers/react-native';
const skan = new SKANManager((update) => {
console.log(`Conversion: ${update.previousValue} -> ${update.newValue} (${update.event})`);
});
await skan.initialize();
skan.setPreset('subscriptions');
skan.setCustomRules([
{ eventName: 'app_open', conversionValue: 1, priority: 1 },
{ eventName: 'trial_start', conversionValue: 20, priority: 5 },
{ eventName: 'subscription_start', conversionValue: 50, priority: 10 }
]);
await skan.processEvent('trial_start', { plan: 'premium' });
console.log(skan.getCurrentValue());
console.log(skan.getMetrics());
SKAN requires the native LayersSKAN module. On Android or without the module, isSupported() returns false and all native operations are no-ops.
Deep Links
React Native Linking API
import { parseDeepLink, setupDeepLinkListener } from '@layers/react-native';
const unsubscribe = setupDeepLinkListener((data) => {
console.log(data.url);
console.log(data.scheme);
console.log(data.host);
console.log(data.path);
console.log(data.queryParams);
layers.track('deep_link', {
url: data.url,
source: data.queryParams.ref
});
});
unsubscribe();
Expo Router
Automatic screen tracking with Expo Router:
import { useLayersExpoRouterTracking } from '@layers/react-native';
import { usePathname, useGlobalSearchParams } from 'expo-router';
function RootLayout() {
useLayersExpoRouterTracking(layers, usePathname, useGlobalSearchParams);
return <Stack />;
}
This fires a screen() call on every route change with the pathname and search params as properties.
Consent Management
await layers.setConsent({ analytics: true, advertising: false });
const consent = layers.getConsentState();
When analytics is false, track() and screen() calls are silently dropped.
Error Handling
Register error listeners to catch errors that would otherwise be silently dropped:
layers.on('error', (err) => {
console.error('Layers SDK error:', err.message);
});
layers.off('error', myListener);
When enableDebug is true and no error listeners are registered, errors are logged to console.warn.
Debug Mode
Enable enableDebug: true (or __DEV__) for detailed logging:
[Layers] track("app_open", 1 properties)
[Layers] screen("home", 0 properties)
[Layers] setAppUserId("user-123")
Testing
Mock the SDK in tests by replacing the instance with a simple spy object:
const mockLayers = {
init: jest.fn().mockResolvedValue(undefined),
track: jest.fn().mockResolvedValue(undefined),
screen: jest.fn().mockResolvedValue(undefined),
setAppUserId: jest.fn(),
clearAppUserId: jest.fn(),
getAppUserId: jest.fn(),
setConsent: jest.fn().mockResolvedValue(undefined),
getConsentState: jest.fn().mockReturnValue({}),
setUserProperties: jest.fn().mockResolvedValue(undefined),
setDeviceInfo: jest.fn(),
flush: jest.fn().mockResolvedValue(undefined),
shutdown: jest.fn(),
getSessionId: jest.fn().mockReturnValue('mock-session-id'),
on: jest.fn().mockReturnThis(),
off: jest.fn().mockReturnThis()
};
License
MIT