React Native and Expo

@x3growth/react-native is a thin TypeScript facade over the native iOS and Android SDKs, built as an Expo Module (new architecture, React Native 0.82 or later, Expo SDK 58 tested). Consent, queue, sessions, hashing, attribution and recordings run natively; the wrapper adds navigation hooks, React components for consent and masking, and an Expo config plugin for the native project files.

Install

Expo (managed or with prebuild):

npx expo install @x3growth/react-native

Bare React Native: install the Expo Modules runtime once, then the package and the pods:

npx install-expo-modules@latest
npm install @x3growth/react-native
npx pod-install

The native SDKs ship inside the npm package: X3Kit (iOS, pod X3Kit with Core and Replay) and io.x3growth:sdk / io.x3growth:sdk-replay (Android, Gradle projects) are linked from node_modules/@x3growth/react-native/vendor by Expo autolinking. No CocoaPods trunk, Maven Central or extra repository is needed; do not add X3Kit or io.x3growth:sdk to the app yourself.

Config plugin

Add the plugin to app.json or app.config.ts; it writes the native settings on every npx expo prebuild and EAS build. All props are optional and validated with clear errors.

app.json
{
  "expo": {
    "plugins": [
      ["@x3growth/react-native", {
        "linkDomains": ["nordlicht.app"],
        "attributionEndpoint": "https://nordlicht.app",
        "attUsageDescription": "We use this permission to measure which ad led to your sign-up."
      }]
    ]
  }
}
PropEffect
linkDomains: string[]iOS: adds applinks:<host> to the Associated Domains entitlement. Android: adds an App Links intent filter (autoVerify, https, host, pathPrefix = linkPathPrefix) to the main activity
linkPathPrefix: string (default /n3/go)path prefix of your X3 links for the Android intent filter
attributionEndpoint: string (https origin)iOS: NSAdvertisingAttributionReportEndpoint (SKAdNetwork) and AdAttributionKit › AttributionCopyEndpoint (postback copies)
reengagementPostbackCopies: boolean (default true, only with attributionEndpoint)iOS: AdAttributionKit › OptInForReengagementPostbackCopies, so Apple also sends copies of re-engagement postbacks
attUsageDescription: stringiOS: NSUserTrackingUsageDescription (only when you call requestTrackingAuthorization)
removeAdIdPermission: boolean (default true)Android: removes com.google.android.gms.permission.AD_ID from the merged manifest
privacyManifest: boolean (default true)iOS: adds X3's required-reason API entry (NSPrivacyAccessedAPICategorySystemBootTime, reason 35F9.1) to the app's privacy manifest, merged without duplicates (privacy manifest)

You still serve apple-app-site-association and assetlinks.json on your domain (iOS, Android).

Configure

src/x3.ts
import { X3 } from "@x3growth/react-native";

X3.configure("pk_3x_YOUR_APP_KEY", {
  consent: { mode: "opt_in", purposes: { recordings: true } },
  host: "https://nordlicht.app/n3",
  recording: { mode: "wireframe" },
});

Import this file once at the top of your entry point. configure is the only call that throws: a missing or invalid consent.mode throws a TypeError, because a forgotten mode would otherwise silently track nothing. An invalid publishable key (not pk_3x_ plus 30 letters or digits) logs a warning and leaves the SDK off: every call is ignored and consentState() stays pending. Every other call returns immediately and never throws (lead rejects on errors). The wrapper always runs the native SDK with autoScreens: false (native screens would report MainActivity or RNSScreen); screens come from the navigation hooks.

Screens

React Navigation:

import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import { useX3NavigationTracking } from "@x3growth/react-native";

export function App() {
  const navigationRef = useNavigationContainerRef();
  useX3NavigationTracking(navigationRef);   // current route name → X3.screen on ready and on every state change
  return <NavigationContainer ref={navigationRef}>{/* … */}</NavigationContainer>;
}

Expo Router:

import { useX3ExpoRouterTracking } from "@x3growth/react-native";

export default function RootLayout() {
  useX3ExpoRouterTracking();   // usePathname() → X3.screen(pathname), e.g. "/(tabs)/home" stays a path
  return <Stack />;
}

React Navigation and Expo Router are optional peer dependencies. Without either, call X3.screen("Paywall") yourself.

Events, identify, leads, purchases

X3.track("plan_selected", { plan: "year", trial: true });
X3.identify(user.id, { email: user.email });
X3.reset();
await X3.lead({ form: "consultation", email, firstName, message, marketingConsent: true });
X3.purchase({ orderId: transactionId, value: 59.99, currency: "EUR", productId: "pro_yearly" });
const token = await X3.purchaseAccountToken();   // appAccountToken (iOS) / obfuscatedAccountId (Android)

lead rejects with an Error whose code is one of notConfigured, invalid, disabled, rateLimited, network, server. With RevenueCat, log users in with the same id you pass to identify (Purchases.logIn(user.id)), so RevenueCat's app_user_id maps to the person (RevenueCat).

Components

import { X3ConsentSheet, X3PrivacySettings, X3Mask, X3Unmask, useX3Consent } from "@x3growth/react-native";

<X3ConsentSheet visible={ask} onDone={(state) => setAsk(false)} privacyPolicyUrl="https://nordlicht.app/privacy" appName="Nordlicht" />
<X3PrivacySettings privacyPolicyUrl="https://nordlicht.app/privacy" />

const { state, consent, consentAll, setOptOut } = useX3Consent();   // live consent state for your own UI

The components use Modal, Switch and Pressable, follow the device's language (German for de*, else English) and color scheme, and carry accessible roles and labels. texts overrides any default text (default texts).

With trackDeepLinks (default true) the wrapper passes Linking.getInitialURL() and every Linking url event to handleOpenURL, so X3 links and UTM links are attributed without code. Call X3.handleOpenURL(url) yourself only for URLs that reach your app another way (for example a push notification payload) and X3.applyLinkToken(token) for magic link tokens.

Masking

<X3Mask>
  <CreditCardPreview />      {/* always masked in recordings */}
</X3Mask>
<X3Unmask>
  <Text>{plan.price}</Text>   {/* text visible; inputs stay masked */}
</X3Unmask>
<Pressable testID="buy_button" onPress={buy} />   {/* tap target name in recordings */}

X3Mask and X3Unmask are native views. testID becomes the anchor (iOS maps it to accessibilityIdentifier, Android reads it through the module's anchor resolver). Recordings need the purpose recordings and recording: { mode: "wireframe" } (session recordings in apps).