Get started

React Native and Expo

@unolink/react-native is a thin bridge over the native iOS and Android SDKs, which ship inside the package. It needs no config plugin, no permissions beyond internet and no Info.plist entries. Pro plan and up.

Requirements

  • React Native 0.79+ (new architecture).
  • Expo: a development build, npx expo prebuild or EAS Build. It doesn't run in Expo Go, because it has native code.
  • An SDK key: in the dashboard, open your app → Settings → SDK keys → New key. It's shown once, so copy it then.

Install

Expo:

npx expo install @unolink/react-native
npx expo prebuild        # or: eas build

Bare React Native:

npm install @unolink/react-native
cd ios && pod install

Then rebuild the app. A JavaScript reload isn't enough after adding native code.

If pod install fails with an encoding error (Ruby 4, or a locale that isn't UTF-8), run it in UTF-8:

cd ios && LANG=en_US.UTF-8 pod install

Don't install with npm install --legacy-peer-deps: it rewrites package-lock.json and drops optional peer dependencies.

Configure

Call configure once, at the top level, before your first screen renders: in app/_layout.tsx with Expo Router, or in index.js.

import { Unolink } from "@unolink/react-native"

Unolink.configure("ulk_…", { debug: __DEV__ })

With 0.3.1 or later:

  • Calls made before configure wait, and are sent right after it, so restoring a session first is fine.
  • A missing key (for example a missing .env entry) is reported in the console instead of failing silently. Unolink.isConfigured() says whether it's running.
  • debug defaults to __DEV__.

Options

OptionDefaultWhat it does
debug__DEV__ (0.3.1+; false before)Marks this install as a test device. Dashboards hide test devices unless Include test devices is on.
logLevel"warn"How much goes to the Xcode console or Logcat (tag Unolink): "none", "error", "warn", "info", "debug".
baseUrlhttps://api.unolink.devThe SDK API. Leave it as it is.

The key is public: it ships inside your app and can only report data for that one app. See SDK keys.

Funnel events

Add one line at each moment that matters. These become the steps in Funnel and Journeys.

Unolink.onboardingCompleted()            // the last onboarding screen is done
Unolink.paywallViewed("onboarding")      // a paywall appeared; placement says which one
Unolink.paywallSkipped("onboarding")     // closed without buying
Unolink.signUp("apple")                  // a new account was created
Unolink.signIn("apple")                  // signed in to an account they already had
Unolink.signUpSkipped()                  // went on as a guest (0.3.1+)

Onboarding screens, one call per screen as it appears:

import { useOnboardingStep } from "@unolink/react-native"

useOnboardingStep("goal", { index: 0 })   // in each onboarding screen component
Unolink.onboardingSkipped("goal")         // on Skip

Screen IDs use letters, digits, _ and . (how_heard, not how-heard). In development, React's StrictMode mounts screens twice, so you may see a screen sent twice there; release builds send it once. Development builds warn in the console about IDs or properties the server would reject (0.3.1+).

Without RevenueCat, report trials and purchases yourself. Pass the amount as a string so it never rounds:

Unolink.trialStarted("pro_yearly")
Unolink.purchase("49.99", "USD", "pro_yearly")
Unolink.purchaseCancelled("pro_yearly")               // closed the purchase sheet
Unolink.purchaseFailed("pro_yearly", "payment_failed")

Your own events take a name and up to 20 properties (strings, numbers or booleans):

Unolink.track("level_up", { level: 3 })

The full list, with the names that reach the dashboard, is in Events.

User ID and RevenueCat

If you use RevenueCat, connect it under your app → Settings → RevenueCat. Then don't call purchase or trialStarted: RevenueCat sends trials, purchases, renewals, cancellations and refunds itself, and Unolink ignores app purchase calls so nothing counts twice.

To join those purchases to the install, report RevenueCat's own app user ID. With SDK 0.3.2 or later, one line keeps it current. Call it once at launch, before or after configure:

import Purchases from "react-native-purchases"

Unolink.connectRevenueCat(Purchases)

Unolink reads the ID now, on every customer info update (purchases, logIn, logOut) and whenever the app comes back to the foreground, and sends it only when it changed. If RevenueCat isn't configured yet, the read fails quietly and is tried again later. It returns a function that disconnects. So Unolink.revenueCatUserChanged() is normally not needed on React Native.

Without it (or before 0.3.2), call setUserId with RevenueCat's ID after Purchases.configure, after every Purchases.logIn and Purchases.logOut, and on every launch. Use one or the other, not both:

// after Purchases.configure, logIn and logOut, and on every launch
Unolink.setUserId(await Purchases.getAppUserID())

For a guest, that's RevenueCat's anonymous ID, so a purchase made before signing up joins the install right away. After logIn, it's the account's ID. Every ID an install has reported counts, so the guest's purchase stays joined. See Guests and accounts.

Unolink never stores the ID itself, only a one-way hash that's different for each of your apps.

On Android, when someone installs from a Unolink link that has a deep link, the link arrives once, on first open. On iPhone there's none: Apple has no install referrer, and Unolink never fingerprints.

link.deepLink is what you set on the link: a path such as /promo/3for2, or a full URL.

First opens usually start in onboarding. If the link's screen sits behind onboarding or sign-in, open it once that's done, then consume it (0.3.1+):

// when onboarding (or sign-in) is done, and on every launch after that until it's consumed:
const link = Unolink.getPendingDeferredDeepLink()
if (link?.deepLink) {
  router.push(link.deepLink)               // a path; open a full URL with Linking.openURL
  Unolink.consumeDeferredDeepLink()
}

From 0.3.3 the link waits until you consume it, even if the app is closed in between (up to 30 days after the install). A new user who quits halfway through onboarding still gets it on the next launch, so you don't need to store it yourself (no AsyncStorage holder). Call consumeDeferredDeepLink() once you've opened it: until then, getPendingDeferredDeepLink() returns it on every launch. It's null before configure and until Google Play has answered. With 0.3.1 and 0.3.2 it's kept for the current launch only.

If the screen is reachable straight away, a listener is enough. Listeners are called once per install. With 0.3.1 a link that arrived before you subscribed goes to the first listener you add; with 0.3.0, subscribe right after configure.

import { router } from "expo-router"

const subscription = Unolink.addDeferredDeepLinkListener((link) => {
  // link.deepLink: "/promo/3for2", link.params: { offer: "3for2" }
  // link.linkSlug: "tiktok" or null, link.clickId: "clk_01JA…"
  if (link.deepLink) router.push(link.deepLink)
  Unolink.consumeDeferredDeepLink()
})

// when the component unmounts
subscription.remove()

Set the link's Deep link and Parameters in the dashboard (Links → ⋯ → Edit). How it works is in Android deferred deep links.

Other calls

CallWhat it does
Unolink.getInstallId()The random ID of this install, or null before configure. It identifies no person or device.
Unolink.flush()Sends queued events now. Normally not needed.
Unolink.setEnabled(false)Turns tracking off and deletes queued events, for your opt-out setting. Remembered across launches.

Calls never throw and never block: events are queued on the device and sent in the background by the native SDK.

Jest

From 0.3.1 the package has its own mock, which records every call:

jest.mock("@unolink/react-native", () => require("@unolink/react-native/jest"))
// expect(Unolink.signUp).toHaveBeenCalledWith("apple")

Test devices

Development builds are test devices (pass debug: __DEV__ before 0.3.1). Google Play testing tracks can't be recognised on the phone: pass debug: true in those builds, for example from an EXPO_PUBLIC_ variable. Their data is real but hidden until you turn on Include test devices in Funnel and Journeys. More in Testing.

Privacy

  • No IDFA, no Google Advertising ID, no App Tracking Transparency prompt, no fingerprinting.
  • A random install ID, app version, OS version, SDK version, phone or tablet, language. Country comes from the network edge. IP addresses are never stored.
  • On iOS the native SDK's privacy manifest comes with the package: analytics, not linked to identity.
  • Never put names, emails or other personal data in event properties.

Troubleshooting

  • "Native module not found" or similar? You're in Expo Go, or haven't rebuilt since installing. Make a development build. From 0.3.1 the app doesn't crash without the native module: calls do nothing and a warning says why.
  • Nothing in the dashboard? Turn on Include test devices: __DEV__ builds are hidden by default.
  • Still nothing? Configure with { debug: __DEV__, logLevel: "debug" } and look for Unolink in the Xcode console or Logcat. A wrong or revoked key is logged as an error.

More in Troubleshooting.