
Security News
Feross on the 10 Minutes or Less Podcast: Nobody Reads the Code
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.
@layers/expo
Advanced tools
@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 instead.
npx expo install @layers/expo
For full functionality, install these Expo packages:
npx expo install expo-tracking-transparency expo-linking expo-clipboard
In your app.config.js or app.json:
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:
npx expo prebuild
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>
);
}
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 />;
}
The Layers Expo config plugin automates native project configuration via npx expo prebuild.
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'
}>;
};
}
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 specifyCFBundleURLTypes -- Custom URL schemes for deep linkingiOS (Entitlements):
com.apple.developer.associated-domains -- Associated domains for Universal Links (auto-prefixed with applinks:)Android (AndroidManifest.xml):
.MainActivity for deep link and App Link handlingandroid:autoVerify="true" is set automatically for HTTPS intent filtersThe plugin includes 16 SKAdNetwork IDs by default from these networks:
Set includeDefaultSKAdNetworkIds: false to disable defaults.
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:
LayersReactNative instanceinit() with AsyncStorage persistenceexpo-tracking-transparencyexpo-linkingonErrorfunction useLayers(): { isReady: boolean; layers: LayersReactNative | null };
Access the SDK context. Returns isReady: false until initialization completes.
function MyComponent() {
const { isReady, layers } = useLayers();
if (!isReady) return <Text>Loading...</Text>;
return <Button title="Track" onPress={() => layers?.track('button_press')} />;
}
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.
function CheckoutButton() {
const layers = useRequiredLayers();
return (
<Button
title="Checkout"
onPress={() => layers.track('checkout_started', { cart_value: 49.99 })}
/>
);
}
function useLayersTrack(): (eventName: string, properties?: EventProperties) => void;
Returns a stable, memoized track function. No-op until the SDK initializes.
const track = useLayersTrack();
track('signup_click', { source: 'hero' });
function useLayersScreen(): (screenName: string, properties?: EventProperties) => void;
Returns a stable, memoized screen tracking function. No-op until the SDK initializes.
const screen = useLayersScreen();
useEffect(() => {
screen('Profile');
}, []);
All exports from @layers/react-native are re-exported from @layers/expo. You can use the SDK without the provider:
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 for the full imperative API documentation.
Set requestTracking={true} on <LayersProvider> to automatically request ATT permission on mount.
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:
const status = await layers.requestTrackingPermission();
This also auto-detects expo-tracking-transparency and prefers it when available.
Set enableDeepLinks={true} (default) and provide an onDeepLink callback:
<LayersProvider
config={config}
enableDeepLinks={true}
onDeepLink={(data) => {
console.log('Deep link:', data.url);
navigation.navigate(data.path);
}}
>
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()
The SDK automatically tracks deep_link_opened events (configurable via autoTrackDeepLinks in the config). This runs in addition to your onDeepLink callback.
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.
For automatic screen tracking with Expo Router:
import { useLayersExpoRouterTracking, useRequiredLayers } from '@layers/expo';
import { usePathname, useGlobalSearchParams } from 'expo-router';
function RootLayout() {
const layers = useRequiredLayers();
useLayersExpoRouterTracking(layers, usePathname, useGlobalSearchParams);
return <Stack />;
}
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:
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.
@layers/react-native// 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);
// 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
FAQs
Layers Analytics Expo SDK — convenience wrapper with config plugin
We found that @layers/expo demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.

Research
/Security News
Campaign of 108 extensions harvests identities, steals sessions, and adds backdoors to browsers, all tied to the same C2 infrastructure.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.