React Native SDK

Install and integrate the LinkTrail React Native SDK — linktrail-react-native, a New Architecture TurboModule for iOS and Android.

A New Architecture TurboModule that wraps the native iOS and Android SDKs behind one JS API (LinkTrail) · package linktrail-react-native, 0.1.0 on npm · React Native 0.76+, iOS 15.1+, Android minSdk 26.

Prerequisites

Before this works, set up three things in the dashboard. Skip one and links fail silently — they open the browser instead of your app. Full flow: Quickstart.

  1. 1Register both app targets — Apps → Register app. iOS needs your Apple Team ID and bundle ID; Android needs its package name and SHA-256 fingerprints.
  2. 2Register the identifier your build really has — the SDK sends the iOS bundle ID and the Android applicationId, and it must equal the identifier you registered, in the same workspace as the API key. A debug build with a suffixed ID (com.acme.shop.debug) needs its own registered app, or every install answers 404 (onError reports it as a server error).
  3. 3Add a link domain — Domains. Or use your free *.linktrail.io subdomain.
  4. 4Create an API key — Settings → API Keys. That is the lt_live_… value below.
  1. Install

    Shell
    npm install linktrail-react-native

    The native SDKs resolve automatically — LinkTrailSDK from CocoaPods (a fresh pod install resolves it to 0.1.0, with the hold-until-decision consent model described below) and io.linktrail:sdk:0.1.0 from Maven Central — so there's nothing else to add. On iOS run cd ios && pod install; on Android just make sure minSdkVersion is 26 or higher.

    Note

    Expo needs a development build (not Expo Go — this is a native TurboModule). The config below does three things: expo-build-properties raises Android's minSdkVersion to 26, which the SDK requires; associatedDomains adds the iOS entitlement; intentFilters adds the verified Android App Links filter.

    Shell
    npx expo install expo-build-properties
    app.jsonJSON
    {
      "expo": {
        "plugins": [["expo-build-properties", { "android": { "minSdkVersion": 26 } }]],
        "ios": { "associatedDomains": ["applinks:yourapp.linktrail.io"] },
        "android": {
          "intentFilters": [{
            "autoVerify": true,
            "action": "VIEW",
            "data": [{ "scheme": "https", "host": "yourapp.linktrail.io" }],
            "category": ["BROWSABLE", "DEFAULT"]
          }]
        }
      }
    }

    Then npx expo prebuild generates the ios/ and android/ projects from this config (and npx expo run:ios / run:android builds the development build). Because prebuild owns those folders, change native settings in app.json or a config plugin rather than by hand: npx expo prebuild --clean regenerates them and drops manual edits. The iOS AppDelegate link forwarding in the next step is already in the AppDelegate Expo's prebuild templates generate (SDK 52–54: the RCTLinkingManager open-URL and continue-userActivity methods) — check yours has both rather than adding them again.

  2. Integrate

    App.tsxTypeScript
    import LinkTrail from 'linktrail-react-native';
    
    // At app launch. The API key is required. The install is sent automatically —
    // once your app records a consent decision (see Consent below).
    await LinkTrail.configure('lt_live_...');
    
    // One listener handles first-launch (deferred) AND re-engagement links:
    LinkTrail.onLink((link, source) => {
      router.navigate(link.path, link.customData);
    });
    
    LinkTrail.onError((error) => console.warn('[LinkTrail] ' + error.code + ': ' + error.message));

    customData is a map of strings to strings. A nested object or array set on the link arrives as a compact JSON string — decode it yourself.

    Incoming links (Universal Links / App Links / custom schemes) are picked up from React Native's Linking API automatically. One iOS requirement: your AppDelegate must forward URLs to RCTLinkingManager — and the continue userActivity override below is NOT in the stock React Native template, so Universal Links silently never arrive without it:

    ios/YourApp/AppDelegate.swiftSwift
    import React
    
    // Custom-scheme URLs (yourapp://…):
    func application(_ app: UIApplication, open url: URL,
                     options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
      RCTLinkingManager.application(app, open: url, options: options)
    }
    
    // Universal Links (https://yourapp.linktrail.io/…):
    func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
      RCTLinkingManager.application(application, continue: userActivity,
                                    restorationHandler: restorationHandler)
    }

    Listeners return a subscription — call .remove() when your component unmounts. onLink also fires immediately if a deferred link already resolved this session, so registration order vs. the auto-tracked install doesn't matter.

  3. Consent gating is on by default (requireConsent: true) and it holds the install: nothing is sent and the deferred link does not route until your app records a decision. setConsent(true) sends the install counted and attributed and routes the deferred link; setConsent(false) sends one uncounted install that still resolves it, so the user lands in the right place. Re-engagement links (app already installed) route regardless. The SDK has no consent getter — persist the choice yourself and call setConsent on every launch after configure. Full model, CMP integration and store questionnaires: Consent & privacy.

    Important

    An app that never calls setConsent gets no deferred link and no attribution. Wire both answers of your consent prompt to setConsent, or pass requireConsent: false if consent is collected elsewhere.

    TypeScript
    // Call once your consent UI resolves — and again on every launch, after configure:
    LinkTrail.setConsent(true);   // grant → install sent + attributed, deferred link routes, queue flushed
    LinkTrail.setConsent(false);  // deny  → one uncounted install, deferred link still routes, queue cleared
    
    // Collecting consent elsewhere (a CMP)? Opt out of the gate — tracking runs from configure,
    // and an explicit setConsent(false) still wins:
    await LinkTrail.configure('lt_live_...', { requireConsent: false });
  4. Standard app deep-link config — the wrapper handles the rest. Declare your LinkTrail host as a Universal Link (iOS Associated Domains: applinks:yourapp.linktrail.io) and an App Links intent-filter (Android). LinkTrail hosts the apple-app-site-association / assetlinks.json files for your link domains.

    Warning

    handleDeepLink claims only https links on linktrail.io (or a subdomain) and on the hosts listed in linkDomains (or their subdomains). Custom domains must be listed: links on go.yourbrand.com need { linkDomains: ['go.yourbrand.com'] }, or the app opens and never navigates. Any other URL (mailto:, an OAuth callback, myapp://settings) resolves null from handleDeepLink with no request and no onError, so route it yourself. Custom-scheme LinkTrail links (myapp://slug) are no longer handled; use the https link. The full rules: Which links the SDK claims. Upgrading from an earlier version? See Upgrading to 0.1.0.

  5. Verify it works

    Every failure on this page is silent unless you look in the right place. Check these in order:

    1. 1SDK logs. Pass { logEnabled: true, logLevel: 'debug' } to configure and read the native logs — the Xcode console (lines prefixed [LinkTrail]) or adb logcat -s LinkTrail. On the first launch expect Install held — awaiting consent decision; after setConsent(true) expect Consent granted, Sending install event… (consent: true), then Install attributed — Install attributed (deterministic click token) when the click token was recovered — or Organic install (no attribution match). Both platforms print [LinkTrail] [LEVEL] message, so these lines read [LinkTrail] [INFO] … on iOS and Android (Android logs them under the LinkTrail tag). No lines at all means configure never ran.
    2. 2API key Last used. Dashboard → Settings → API Keys: the key's Last used changes from Never after the first call made with a valid key.
    3. 3Overview and Links. Dashboard → Overview: the installs count goes up. Links → your link shows a click and an install.
    4. 4onError. A wrong or revoked key does not fail configure. The backend answers 401 with {"code":401,"message":"Invalid or missing API key"} and the SDK reports it through onError as { code: 'invalid_api_key' } — but only once the install is sent: after setConsent, or at configure with requireConsent: false. Nothing is sent before that.
    5. 5Re-test deferred without reinstalling. Call LinkTrail.resetForTesting() — it clears the install flag, cached attribution, device ID and the consent decision, so the next launch behaves like a genuine first launch and asks for consent again. Testing only; never ship it.
    [LinkTrail] [INFO] Install held — awaiting consent decision
    [LinkTrail] [INFO] Consent granted
    [LinkTrail] [INFO] Sending install event… (consent: true)
    [LinkTrail] [INFO] Install attributed (deterministic click token)
    [LinkTrail] [INFO] Install attributed
    [LinkTrail] [INFO] Organic install (no attribution match)

    Still nothing? Troubleshooting lists the usual causes by symptom.

More

TypeScript
// Custom + revenue events:
await LinkTrail.trackEvent('purchase', { value: 59.99, currency: 'USD' });

// Cached results:
const attribution = await LinkTrail.getLastAttribution();
const lastLink = await LinkTrail.getLastDeepLink();

// Manual install (rare): autoTrackInstall: false, then call trackInstall() yourself.
// Still subject to the consent gate above — call it after setConsent.
await LinkTrail.configure('lt_live_...', { autoTrackInstall: false });
await LinkTrail.trackInstall();

// iOS ATT / SKAdNetwork (no-ops on Android):
await LinkTrail.requestTrackingAuthorization();
LinkTrail.registerForSKAdAttribution();
LinkTrail.updateConversionValue(42, 'medium');

configure also accepts { logEnabled, logLevel, requestTimeoutMillis (default 15000), retryPolicy ({ maxAttempts: 3, baseDelayMillis: 500, maxDelayMillis: 8000 }), linkDomains, autoTrackInstall, requireConsent, clickTokenSource, autoHandleLinks }. Set autoHandleLinks: false to forward URLs yourself via LinkTrail.handleDeepLink(url) — it resolves null for URLs that aren't LinkTrail links.

onError receives { code, message }. Codes: missing_api_key, invalid_api_key, transport, server, decoding, empty_response, invalid_url, not_a_linktrail_url, not_configured, unknown. An invalid key surfaces here asynchronously (the backend rejects with 401) — configure itself only rejects on a blank key.

Deferred attribution & the paste button (iOS)

On iOS, deferred attribution recovers a click token the tapped link leaves on the clipboard. clickTokenSource, passed to configure, picks how it is read. Android uses the Play Install Referrer instead, so this setting is ignored there and the button renders nothing.

clickTokenSourceBehaviourTrade-off
'pasteButton' (default)Read only when the user taps <LinkTrailPasteButton/>.No system alert, but it needs UI and autoTrackInstall: false.
'automatic'The SDK reads the clipboard itself at install.No UI, but iOS shows the “Allow Paste” alert on first launch.
'none'The clipboard is never touched.No alert and no UI; deferred matching falls back to probabilistic only.
Warning

With the default 'pasteButton' you must render the button and set autoTrackInstall: false — the install fires when the token is pasted. Skip either and deferred installs on iOS stay unattributed.

TypeScript
import { LinkTrail, LinkTrailPasteButton } from 'linktrail-react-native';

await LinkTrail.configure('lt_live_...', {
  clickTokenSource: 'pasteButton',
  autoTrackInstall: false, // required: the install fires on tap
});

// On your first-launch screen:
<LinkTrailPasteButton
  style={{ height: 46, alignSelf: 'stretch' }}
  cornerStyle="fixed"
  fillColor="#111"
  foregroundColor="#FFFFFF"
  // Omitted (default): the tap grants consent only while it is still undecided.
  // true: always grants (an explicit opt-in UI). false: never touches consent (a CMP).
  // grantsConsent={false}
  onTokenPasted={(token) => {
    // The SDK already sent the install with this token; onLink routes the
    // recovered destination. Use this only for UI side effects.
  }}
/>

Since the package (0.1.0) the tap goes through the native paste path: it records consent per grantsConsent and sends the install with the token, so don't call setConsent or trackInstallWithClickToken from onTokenPasted. A token pasted after a denial is kept for routing; the install stays uncounted. Using a CMP that answers later? Pass grantsConsent={false} — the token is held with the install until your own setConsent.

API surface

APIPurpose
configure(apiKey, options?)Initialize; the install is held until a setConsent decision
setConsent(granted)Record the decision; releases the held install either way (see Consent above)
onLink((link, source) => …)Deep link delivered (deferred + re-engagement)
onAttribution((attribution) => …)Install attribution result
onError((error) => …)Failures (invalid key, network…)
handleDeepLink(url)Manual forward (with autoHandleLinks: false)
trackEvent(name, { value, currency })Custom + revenue events
trackInstall(force?)Manual install track (with autoTrackInstall: false)
trackInstallWithClickToken(clickToken, force?)Manual install carrying a click token you read yourself (iOS; same as trackInstall on Android)
getLastAttribution() / getLastDeepLink()Cached last results
requestTrackingAuthorization()ATT prompt (iOS; no-op on Android)
registerForSKAdAttribution() / updateConversionValue(value, coarse)SKAdNetwork (iOS; no-op on Android)
resetForTesting()Clears the install flag, attribution, device ID and consent — testing only
Note

Example app: KickFlip — a storefront in the react-native repo's example folder that demonstrates deferred deep linking end to end. Put your key in example/src/attribution.ts, then cd example && npm install && npm run android (for iOS, from example/ run cd ios && bundle install && bundle exec pod install && cd .., then npm run ios). Source · example app · issue tracker · changelog.