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.
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.
- 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.
- 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 (onErrorreports it as a server error). - 3Add a link domain — Domains. Or use your free
*.linktrail.iosubdomain. - 4Create an API key — Settings → API Keys. That is the
lt_live_…value below.
Install
Shellnpm install linktrail-react-nativeThe native SDKs resolve automatically —
LinkTrailSDKfrom CocoaPods (a freshpod installresolves it to 0.1.0, with the hold-until-decision consent model described below) andio.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.NoteExpo needs a development build (not Expo Go — this is a native TurboModule). The config below does three things:
expo-build-propertiesraises Android's minSdkVersion to 26, which the SDK requires;associatedDomainsadds the iOS entitlement;intentFiltersadds the verified Android App Links filter.Shellnpx expo install expo-build-propertiesapp.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 prebuildgenerates theios/andandroid/projects from this config (andnpx expo run:ios/run:androidbuilds the development build). Because prebuild owns those folders, change native settings inapp.jsonor a config plugin rather than by hand:npx expo prebuild --cleanregenerates them and drops manual edits. The iOSAppDelegatelink forwarding in the next step is already in the AppDelegate Expo's prebuild templates generate (SDK 52–54: theRCTLinkingManageropen-URL and continue-userActivity methods) — check yours has both rather than adding them again.Integrate
App.tsxTypeScriptimport 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));customDatais 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
AppDelegatemust forward URLs toRCTLinkingManager— 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.swiftSwiftimport 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.
onLinkalso fires immediately if a deferred link already resolved this session, so registration order vs. the auto-tracked install doesn't matter.Consent
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 callsetConsenton every launch afterconfigure. Full model, CMP integration and store questionnaires: Consent & privacy.ImportantAn app that never calls
setConsentgets no deferred link and no attribution. Wire both answers of your consent prompt tosetConsent, or passrequireConsent: falseif 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 });Deep-link setup
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 theapple-app-site-association/assetlinks.jsonfiles for your link domains.WarninghandleDeepLinkclaims onlyhttpslinks onlinktrail.io(or a subdomain) and on the hosts listed inlinkDomains(or their subdomains). Custom domains must be listed: links ongo.yourbrand.comneed{ linkDomains: ['go.yourbrand.com'] }, or the app opens and never navigates. Any other URL (mailto:, an OAuth callback,myapp://settings) resolvesnullfromhandleDeepLinkwith no request and noonError, so route it yourself. Custom-scheme LinkTrail links (myapp://slug) are no longer handled; use thehttpslink. The full rules: Which links the SDK claims. Upgrading from an earlier version? See Upgrading to 0.1.0.Verify it works
Every failure on this page is silent unless you look in the right place. Check these in order:
- 1SDK logs. Pass
{ logEnabled: true, logLevel: 'debug' }toconfigureand read the native logs — the Xcode console (lines prefixed[LinkTrail]) oradb logcat -s LinkTrail. On the first launch expectInstall held — awaiting consent decision; aftersetConsent(true)expectConsent granted,Sending install event… (consent: true), thenInstall attributed—Install attributed (deterministic click token)when the click token was recovered — orOrganic 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 meansconfigurenever ran. - 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.
- 3Overview and Links. Dashboard → Overview: the installs count goes up. Links → your link shows a click and an install.
- 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 throughonErroras{ code: 'invalid_api_key' }— but only once the install is sent: aftersetConsent, or atconfigurewithrequireConsent: false. Nothing is sent before that. - 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.
- 1SDK logs. Pass
More
// 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.
| clickTokenSource | Behaviour | Trade-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. |
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.
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
| API | Purpose |
|---|---|
| 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 |
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.