React Native SDK
Deep linking, deferred attribution, user identity, custom events, and campaign link generation for Android and iOS — on both the new and legacy React Native architectures.
Jump to
Deep link resolution, the install referrer, attribution, queues, retries, device signals and networking all live in the native SDKs, shared with every other Deeplinkly integration. Native method names match those SDKs' own entry points one-for-one, and validation is enforced natively rather than in JavaScript — so a native-only integration and this package give the same answer for the same input.
- React Native: developed against 0.87 with React 19. Both the new and legacy architectures are supported from one import path.
- Android:
minSdk24,compileSdk35, JVM target 17. Wrapscom.deeplinkly:deeplinkly-android:1.3.0. - iOS: deployment target 13.0, Swift 5. Wraps pod
Deeplinkly1.2.1. The paste control is iOS 16+ and ATT is iOS 14+, both weak-linked at runtime rather than raising the floor. - API key: set natively, per platform — never hard-coded in JavaScript. You can create a key in App Settings.
Native key names
| Purpose | Android | iOS |
|---|---|---|
| API key | com.deeplinkly.sdk.api_key | DeeplinklyApiKey |
| Link domain allowlist | com.deeplinkly.sdk.link_domains | DeeplinklyLinkDomains |
| Attribution level | com.deeplinkly.sdk.attribution_level | DeeplinklyAttributionLevel |
| Deferred mechanism | Play Install Referrer | Pasteboard / DeeplinklyPasteButton |
| Pasteboard opt-out | n/a — Android has no pasteboard path | DeeplinklyCheckPasteboardOnInstall |
npm install react-native-deeplinkly
cd ios && pod installRebuild afterwards
A JS reload does not pick up native code. If the module is missing at runtime the SDK says so on first use rather than at import — “the native module is not linked” — so the bundle still boots and isAvailable() is still callable.
Android autolinks and needs no manifest entries of its own: the install referrer receiver and the INTERNET permission arrive transitively from the native SDK.
Android build requirement — Kotlin 2.2.0
The native Android SDK is compiled with Kotlin 2.2.0, and a 2.0.x compiler cannot read its metadata. Gradle loads one Kotlin plugin for the whole build, so this is set by your app, not by the library:
// android/build.gradle
buildscript {
ext {
kotlinVersion = "2.2.0"
}
dependencies {
// Version it explicitly. Left bare — as the React Native template has
// it — the version arrives transitively from react-native-gradle-plugin
// and ext.kotlinVersion is silently ignored.
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
}
}React Native 0.87's template already sets kotlinVersion = "2.2.0" but still leaves the classpath entry unversioned, so the version arrives transitively from react-native-gradle-plugin and ext.kotlinVersion is silently ignored. The library checks the resolved version at configuration time and fails with this instruction rather than letting the compiler emit an internal error that names nothing.
There is no init() to call. Both native modules initialise themselves and read their API key from the manifest / Info.plist.
Subscribing is what tells native you are ready. Links that resolved before then — a cold start from a tap, or a deferred link recovered from the pasteboard — are buffered natively and delivered on subscribe, so nothing races your bundle.
import { useEffect } from 'react';
import Deeplinkly from 'react-native-deeplinkly';
export function DeeplinklyBootstrap() {
useEffect(() => {
// Subscribing is what signals readiness to native. Links that resolved
// before now — a cold start from a tap, or a deferred link recovered from
// the pasteboard — are buffered natively and delivered here, so nothing
// races your bundle.
const sub = Deeplinkly.addListener(({ click_id, params }) => {
navigate(params.screen as string);
});
return () => sub.remove();
}, []);
return null;
}Payload envelope — identical on Android and iOS
{
click_id: 'ab12…', // null if the backend did not recognise the click
params: { screen: 'home' }, // the link's own parameters
}One read path, resolved or not
click_id is always present; only its value may be null, when the backend did not recognise the click. params carries the link's parameters whether they came back from the backend or — when it could not be reached — from the URL itself, so a single read path covers both.
Do not subscribe on a screen that unmounts
Delivery is at-least-once against an attached listener. The SDK holds a link while nothing is listening, so an unmounted listener does not lose it — but a listener that unsubscribes mid-delivery may see the link redelivered later. Own the subscription once, at app root.
Use HTTPS App Links for production domains and a custom scheme for dev or fallback.
<activity android:name=".MainActivity" android:launchMode="singleTask">
<!-- App Links. This is the one that matters: it lets a tap on
https://links.yourapp.com/abc123 open the app directly. Without it
every link detours through the browser, and in-app browsers that
block intent:// URLs (Instagram, Facebook, TikTok) never reach your
app at all — even when it is installed.
autoVerify only does anything on http/https. -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="links.yourapp.com" />
</intent-filter>
<!-- Custom scheme. The browser fallback path uses this, so keep it —
but it is a fallback, not a substitute for the filter above. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourapp" />
</intent-filter>
</activity>
<application ...>
<meta-data
android:name="com.deeplinkly.sdk.api_key"
android:value="your_api_key_here" />
<!-- Optional, but set it if the app App Links any host besides its
Deeplinkly link domain. Comma separated. -->
<meta-data
android:name="com.deeplinkly.sdk.link_domains"
android:value="links.yourapp.com" />
</application>Replace links.yourapp.com with your Deeplinkly link domain, and yourapp with the URI scheme you set in the dashboard.
React Native's template already sets launchMode="singleTask", which delivers a warm deep link through onNewIntent — that is what the SDK needs. Do not change it to standard, which would start a second activity instance per link instead.
Which links the SDK claims
The rule is the same on both platforms. A link that came through the redirect carries a click_id, and the SDK acts on that whatever the scheme. The ambiguous case is the App Link / Universal Link bypass, where the OS routes https://links.yourapp.com/<code> straight to the app and the first path segment is the only thing there is to resolve on. So:
- Custom-scheme URLs without a
click_idare ignored. Your own routes (yourapp://settings/notifications) are yours; the SDK will not resolve them. - http(s) URLs are resolved by code. If the link-domains list is set, only those hosts are; without it every https link the app handles is, which is fine for an app whose only App Link filter is its link domain.
Set link domains if you App Link anything else
A marketing site, say — otherwise https://www.yourapp.com/pricing is resolved as the link code pricing.
Verifying App Links
Android checks https://<your-link-domain>/.well-known/assetlinks.json on install. Deeplinkly serves that file for you, but only once the dashboard has both your package name and your SHA-256 signing certificate fingerprint — with either missing the endpoint returns 404 and verification silently fails.
# The SHA-256 fingerprint the dashboard needs.
keytool -list -v -keystore <your-keystore> -alias <your-alias> | grep SHA256
# Confirm the file is live and verification passed.
curl https://links.yourapp.com/.well-known/assetlinks.json
adb shell pm get-app-links <your.package.name>If you use Play App Signing, take the fingerprint from Play Console → Test and release → Setup → App signing, not from your upload keystore — Google re-signs the APK, so the upload fingerprint will not match what ships. pm get-app-links should report verified for your domain; none or legacy_failure means the fingerprint does not match or the file is not reachable.
Universal Links need Associated Domains and a hosted apple-app-site-association file on your link domain.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>
<key>DeeplinklyApiKey</key>
<string>your_api_key_here</string>
<!-- Your Deeplinkly link domains. Subdomains count. Two jobs: the hosts a
deferred link may be read from, and the hosts whose first path segment
may be read as a link code. Set it if the app Universal Links any host
besides its link domain. -->
<key>DeeplinklyLinkDomains</key>
<array>
<string>yourbrand.deeplinkly.com</string>
</array>Then add an Associated Domains capability with applinks:yourbrand.deeplinkly.com for each link domain.
Without this, no deep link reaches the SDK
A React Native native module never receives app-delegate callbacks, so the SDK cannot register itself for the UIApplicationDelegate and UIScene link callbacks the way a native integration does. React Native's own template also ships an AppDelegate with no linking support at all, so there is nothing to piggyback on.
This is the opposite of the Flutter plugin, which registers for those callbacks itself and delivers links twice if you hand-wire them.
import react_native_deeplinkly
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// ... existing React Native setup ...
// Cold launch. A Universal Link that *starts* the app is in launchOptions,
// not in continue(userActivity:) — missing this loses exactly the case
// deferred deep linking exists for.
RNDeeplinklyLinking.handleLaunchOptions(launchOptions)
return true
}
// Universal Links while running.
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
RNDeeplinklyLinking.handleUserActivity(userActivity)
return RCTLinkingManager.application(
application, continue: userActivity, restorationHandler: restorationHandler)
}
// Custom-scheme links.
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
RNDeeplinklyLinking.handleURL(url)
return RCTLinkingManager.application(app, open: url, options: options)
}
}Calling RCTLinkingManager as well keeps JS Linking working — the Deeplinkly call is non-exclusive.
Forward eagerly; do not order these carefully
handleLink buffers until the SDK initialises, buffers again until a JS listener attaches, and suppresses a duplicate for the same link — because the resolve is idempotent and attribution is written once.
If your app uses a SceneDelegate
When the host adopts UISceneDelegate, the UIApplicationDelegate callbacks above never fire. Use these three instead:
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
// Cold launch on the scene path.
RNDeeplinklyLinking.handleSceneConnectionOptions(connectionOptions)
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
RNDeeplinklyLinking.handleUserActivity(userActivity)
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
RNDeeplinklyLinking.handleOpenURLContexts(URLContexts)
}From an Objective-C AppDelegate
Import the generated Swift header instead:
#if __has_include(<react_native_deeplinkly/react_native_deeplinkly-Swift.h>)
#import <react_native_deeplinkly/react_native_deeplinkly-Swift.h>
#else
#import "react_native_deeplinkly-Swift.h"
#endif
[RNDeeplinklyLinking handleURL:url];The two forms cover framework and static-library linkage respectively.
iOS has no install-referrer API, so the link survives an App Store install via the pasteboard.
The Deeplinkly interstitial copies the link when the visitor taps through to the store, and the SDK reads it back on first launch. There are two ways to read it back — pick one.
Option A — <DeeplinklyPasteButton> (recommended, no banner)
A system paste button rendered in your component tree. Because the user taps it themselves, iOS treats the tap as the grant and shows no “Pasted from…” banner at all.
import { DeeplinklyPasteButton } from 'react-native-deeplinkly';
<DeeplinklyPasteButton
onPasted={(handled) => setShowPasteButton(!handled)}
fallback={null}
/>;Put it on a first-run screen next to something like “Tapped a link to get here? Restore where you left off.” The recovered link arrives on your normal listener exactly like any other; onPasted only tells you whether the pasted content was one of your links, so you can hide the button or explain that it was not.
Requires iOS 16+. Renders fallback on Android and older iOS, so it is safe to place unconditionally — isPasteButtonSupported is exported for hosts that want to change surrounding copy rather than just the button. The control has an intrinsic size React Native's layout does not read, so it defaults to 140×40; override with style. Other props are displayMode (iconOnly | labelOnly | iconAndLabel), cornerStyle (small | medium | large | capsule), backgroundColor and foregroundColor.
Leave the colors unset unless you have a reason
Styling a paste button to look like something else is what gets it rejected as misleading.
Option B — automatic read (on by default, shows the banner)
On by default; you do not need to enable it. To turn it off:
<key>DeeplinklyCheckPasteboardOnInstall</key>
<false/>Do that in Info.plist, not from JavaScript — the read happens during native module construction, before your JS runs, so setCheckPasteboardOnInstall(false) arrives too late to prevent the first one. Turning it on from JS at runtime reads immediately rather than waiting for a next launch the pasteboard may not survive to.
To explain the prompt before it appears, turn the automatic read off in Info.plist and drive it yourself:
if (await Deeplinkly.willShowPasteboardBanner()) {
await showMyPrimingDialog(); // "we can restore where you left off"
await Deeplinkly.checkPasteboardNow();
}willShowPasteboardBanner reads no content and shows no banner itself.
Either way
- The visitor must tap through the interstitial — there is no auto-redirect on iOS, because Safari will not allow a clipboard write without a user gesture.
- The automatic read happens once per install, guarded by a persisted flag, and probes the pasteboard's types first with
hasURLs, which is banner-free. On iOS 16+ a second banner-free probe,detectPatterns(.probableWebURL), catches links that arrived as plain text. - The banner is not limited to your own links. Any URL on the clipboard triggers the read; the SDK then discards anything whose host is not in
DeeplinklyLinkDomains— but the banner has already shown. A user with no URL copied sees nothing. - The automatic read clears your own link from the pasteboard once the resolve is durably queued; the paste button leaves the pasteboard alone, since the user pasted deliberately.
- The resolved click is stamped
attribution_source = "clipboard", notinstall_referrer— that API does not exist on iOS. - If the first launch is offline the pending resolve is persisted and retried next launch, so an offline install is not lost.
- Both paths are skipped entirely when tracking is disabled via
setTrackingEnabled(false).
Deeplinkly’s stable install id and your own user id, alongside campaign context.
import Deeplinkly from 'react-native-deeplinkly';
const attribution = await Deeplinkly.getInstallAttribution();
const deeplinklyId = await Deeplinkly.getDeeplinklyId();
// custom_user_id, for enrichment and backend user linking.
Deeplinkly.setUserId('user_123');
Deeplinkly.setUserId(null); // clear on logoutgetDeeplinklyId is the stable per-install id — the same value the API sees as deeplinkly_device_id / X-Deeplinkly-User-Id. setUserId sets custom_user_id for enrichment and backend user linking.
The fields a conversion is matched on once it reaches Meta’s Conversions API or Google’s enhanced conversions.
import Deeplinkly from 'react-native-deeplinkly';
// Returns false if any field was malformed, in which case nothing was stored.
const ok = await Deeplinkly.setUserData({
userId: 'user_123',
email: 'ada@example.com',
phoneNumber: '+441234567890',
firstName: 'Ada',
lastName: 'Lovelace',
city: 'London',
country: 'GB',
});Every field is optional and each call merges, so you can supply an email at sign-up and an address at checkout. A malformed field rejects the whole call — nothing is stored — so a rejected call never leaves you guessing which of the values took.
Validation is native, so a native-only integration gets the same answer this one does. These fields survive a reduced downgrade, because attribution levels gate what the SDK observes about a device and an email someone typed into your app is not an observation. At none nothing is sent.
dateOfBirth:YYYY-MM-DD.gender:'m'or'f'— the only two values Meta'sgeaccepts. Anything else is refused rather than coerced.country: ISO-3166-1 alpha-2, for example'US'.- Every field has a maximum length, enforced before anything is stored.
Your own identifiers
customData carries identifiers Deeplinkly does not name — typically product-analytics ids such as a Mixpanel distinct id or a CleverTap id. Attach a new identifier anytime; no app release required.
await Deeplinkly.setUserData({
userId: 'user_123',
customData: {
mixpanel_distinct_id: 'd-8837',
clevertap_id: 'ct-4412',
},
});Up to 10 entries, keys up to 64 characters and values up to 256, enforced natively. Anything larger rejects the whole call, as one bad typed field does.
Erasing it
Deeplinkly.clearUserData(); // erases everything setUserData and setUserId recorded
Deeplinkly.setUserId(null); // clears only the idThis is not merely “stop sending”: the next enrichment carries each previously-set field as an empty value, which the service reads as null this column rather than not reported. The erasure is re-sent until it is delivered, so calling it on a device that is offline still takes effect once it is not.
Validation is enforced in the native layer, so the same rules apply to every host — not just React Native callers.
import Deeplinkly, { DeeplinklyEvent } from 'react-native-deeplinkly';
const ok = await Deeplinkly.logEvent(DeeplinklyEvent.purchase, {
order_id: 'ord_42',
amount: 49.99,
currency: 'INR',
});DeeplinklyEvent holds the well-known names the backend reports on without extra configuration (purchase, add_to_cart, signup, and so on); any string is accepted for your own funnels.
Validation
- Event name: non-empty after trimming, at most 64 characters.
- At most 25 custom parameters. The SDK's own
_dl_*keys do not count toward this, and passing a key with that prefix is rejected. - Parameter keys: max 64 characters.
- Parameter string values: max 256 characters.
- Array and object values are stored as compact JSON, and the 256 limit applies to that encoded form.
nullvalues are rejected, not dropped. So is any type other than string, number, boolean, array or object.
Numbers and booleans keep their JSON types end to end — 49.99 is stored as a number, not "49.99". A rejected event resolves false and sends nothing.
await Deeplinkly.logPurchase({
value: 49.99,
currency: 'USD',
orderId: 'ord_42',
quantity: 1,
productId: 'sku_9',
});Not a separate pipeline: it sends the event named purchase with value and currency set, so everything true of logEvent — the retry queue, the parameter limits, the device block — is true of this too.
value and currency are what Meta's Conversions API and Google's enhanced conversions both key off, so this one call feeds both without you having to match their spelling.
Rejected, sending nothing, if the value is negative or not finite (a refund is a different event, not a negative purchase), the currency is not three letters, the quantity is negative, or parameters contains any of value, currency, order_id, quantity or product_id — pass those as fields instead.
Pass orderId
It is what Google deduplicates conversions on, and how you reconcile a forwarded conversion against your own records.
Build structured Deeplinkly links from the app for referrals, UTM-tagged shares, and programmatic growth loops.
const result = await Deeplinkly.generateLink(
{
canonicalIdentifier: 'product/sku_42',
title: 'Pro Plan',
metadata: { plan: 'pro' },
},
{
channel: 'email',
feature: 'upgrade_campaign',
tags: ['spring', 'sale'],
}
);
if (result.success) {
Share.share({ message: result.url! });
} else {
// camelCase, not the snake_case that crosses the bridge.
console.warn(result.errorCode, result.errorMessage);
}The result is { success, url?, errorCode?, errorMessage? }. It resolves rather than rejecting on failure. Observed errorCode values: SDK_DISABLED, INVALID, NO_URL, LINK_ERROR, HTTP_<status>, NULL_NATIVE_RESPONSE, NATIVE_EXCEPTION, or a backend code passed through.
camelCase in JS, snake_case on the wire
The bridge speaks the same snake_case maps as the native SDKs, but src/index.tsx maps the result before it reaches you. Read result.errorCode, not result.error_code — the latter is always undefined. The deep link envelope is the exception: its keys (click_id, params) are forwarded unchanged.
tags is a list, not an object
The API only accepts a list (or a comma-separated string) and silently discards anything else — so tags sent as an object never reach us.
await Deeplinkly.setTrackingEnabled(false); // the consent-flow off switch
await Deeplinkly.setAttributionLevel('reduced'); // a middle ground
await Deeplinkly.resetPrivacyData(); // forget this devicesetTrackingEnabled(false) sends no enrichment, no events and no error reports, skips the iOS pasteboard read, and deletes pending reporting retries. Deep links still resolve and are still delivered — the link a user tapped keeps working — but functional requests omit the stable Deeplinkly ID and custom user ID. It persists across launches.
resetPrivacyData() removes the stable Deeplinkly ID, custom user ID, attribution, cached device profile, session and event state, pasteboard state and pending queues. Tracking stays disabled afterwards; call setTrackingEnabled(true) only once the user opts back in.
Hashing identifiers on the device
Off by default. With it on, the email, phone number and names given to setUserData are SHA-256 hashed on the device, so the plaintext never leaves it.
await Deeplinkly.setPIIHashingEnabled(true); // SHA-256 on device before sending
await Deeplinkly.isPIIHashingEnabled(); // off unless you turned it onOnly those four are hashed. Gender, country and date of birth are not: their value ranges are small enough that a digest is reversed by enumerating them, so hashing them would be protection in appearance only.
It costs attribution quality
A digest is computed once, under one normalisation, and advertising destinations disagree about phone formatting — so a conversion forwarded to a destination whose rules differ will not match. Turn it on when a compliance requirement says plaintext must not leave the device, not by default.
| Level | What is sent |
|---|---|
| full | Everything. The default. |
| reduced | Drops every high-entropy hardware signal: screen geometry, model, CPU, local IP, WebView user agent, and the advertising ID / Android ID / IDFA / IDFV. Keeps the coarse context campaign reporting reads — locale, timezone, OS and app version. |
| minimal | Only the install id, app build, and the link being reported on. Nothing describing the device. |
| none | No enrichment at all. Links still resolve and still deliver. |
Each level is a strict subset of the one above. Deep link delivery works at every level, including none — this restricts reporting, not functionality. Resolving a link never sends anything describing the device, at any level. setTrackingEnabled(false) wins over the level: while disabled, getAttributionLevel() reports none whatever was set.
To start restricted before any JavaScript runs — enrichment can be sent during native module construction, before a JS call could arrive — set it natively:
<key>DeeplinklyAttributionLevel</key>
<string>reduced</string><meta-data
android:name="com.deeplinkly.sdk.attribution_level"
android:value="reduced" />The package's docs/SIGNALS.md is the field-by-field reference: every signal the SDK can send, its level, and which platforms report it. It is generated from the same catalogue the SDKs compile against, so it cannot drift from what is actually sent.
Every method answers with its documented failure value rather than throwing.
isAvailable() returns false and the rest degrade in place — logEvent resolves false, getAttributionLevel resolves none, generateLink resolves { success: false, errorCode: 'SDK_DISABLED' }. getDeeplinklyId and resetPrivacyData keep working, since they are local operations that need no key.
Each method carries its own correctly-typed default rather than sharing one SDK_DISABLED envelope: a typed native module cannot resolve a map where it declared a boolean, so an envelope would break the contract for every method that does not return an object. isAvailable() is what tells you the key is missing — assert it once on a debug build, since a missing key is a configuration bug rather than a runtime condition.
if (__DEV__ && !(await Deeplinkly.isAvailable())) {
console.warn('Deeplinkly: no API key in AndroidManifest.xml / Info.plist');
}Deeplinkly.setDebugMode(__DEV__);| Method | Returns | Description |
|---|---|---|
| addListener(handler) | Subscription | Subscribes to resolved deep links, and signals readiness to native — links buffered before the first listener are flushed on subscribe. |
| isAvailable() | Promise<boolean> | False when the API key is missing from AndroidManifest.xml / Info.plist. This is how you tell a misconfigured build from a call that simply failed. |
| getDeeplinklyId() | Promise<string> | Stable per-install id — the same value the API sees as deeplinkly_device_id. Works even with no API key. Empty string on failure. |
| setUserId(id | null) | void | Sets custom_user_id for enrichment and backend user linking. Fire-and-forget; pass null to clear on logout. |
| setUserData(data) | Promise<boolean> | Merges the conversion-matching fields, plus your own customData ids. All-or-nothing: one malformed field stores none of them. |
| clearUserData() | void | Erases everything setUserData and setUserId recorded, here and on Deeplinkly's servers. Re-sent until delivered. |
| getInstallAttribution() | Promise<Record<string, string>> | Install and campaign attribution for this device. Empty object on failure. |
| generateLink(content, options) | Promise<DeeplinklyResult> | Builds a campaign link. Resolves on failure rather than rejecting — inspect success, url, errorCode, errorMessage. |
| logEvent(name, params?) | Promise<boolean> | Validated custom event. False when the native layer rejects it; nothing is sent on rejection. |
| logPurchase(purchase) | Promise<boolean> | Typed wrapper over logEvent. Sends the `purchase` event under the one spelling Meta and Google are both built from. |
| setTrackingEnabled(bool) | Promise<boolean> | The consent-flow off switch. Persists across launches. Wins over setAttributionLevel. |
| resetPrivacyData() | Promise<boolean> | Deletes locally stored privacy data. Leaves tracking disabled afterwards. |
| setAttributionLevel(level) | Promise<boolean> | Restricts what enrichment is sent. False on an unknown level. |
| getAttributionLevel() | Promise<AttributionLevel> | The level currently in force. Reports none while tracking is disabled, whatever was set. |
| setPIIHashingEnabled(bool) | Promise<boolean> | SHA-256 the email, phone and names on the device before they are sent. Off by default. |
| isPIIHashingEnabled() | Promise<boolean> | Whether on-device hashing is on. |
| setCheckPasteboardOnInstall(on, checkNow?) | Promise<boolean> | iOS only; false on Android. To disable the automatic read, use Info.plist instead — this call arrives too late. |
| willShowPasteboardBanner() | Promise<boolean> | Whether a read would surface the system banner. Reads no content and shows no banner itself. Always false on Android. |
| checkPasteboardNow() | Promise<boolean> | Starts the pasteboard read on demand — it reports that the read began, not what it found. The link arrives on your listener. |
| setDebugMode(bool) | void | Verbose native logging for integration debugging. |
Generate a link with generateLink, then open it. A URL with no click_id and no Deeplinkly short code is skipped by design, so yourapp://open?screen=home will never deliver anything — use a generated link.
# iOS Simulator
xcrun simctl openurl booted "https://<your-link-domain>/<code>"
# Android device or emulator
adb shell "am start -W -a android.intent.action.VIEW \
-d 'https://<your-link-domain>/<code>' -p com.yourapp"Verify cold start (process killed) and warm start (app backgrounded) separately. Two constraints worth planning around:
- Android: the Install Referrer is unavailable on sideloaded builds — test deferred flows through Play internal testing.
- iOS: deferred deep linking cannot be tested on the Simulator (no App Store). The automatic read is once per install and clears the pasteboard, so reinstall to retest — relaunching will not repeat it.
“The native module is not linked”
The bundle resolved neither the TurboModule nor NativeModules entry. Rebuild the app after installing — a JS reload will not pick up native code — and run pod install first on iOS.
No link ever reaches the listener on iOS
Almost always the missing AppDelegate forwarding: a React Native module cannot receive those callbacks itself. If the app adopts UISceneDelegate, the UIApplicationDelegate callbacks never fire — wire the three scene methods instead.
Android build fails on Kotlin metadata
Set kotlinVersion = "2.2.0" in android/build.gradle and version the kotlin-gradle-plugin classpath entry explicitly. Left bare, the version arrives transitively and ext.kotlinVersion is ignored.
Cold-start links are lost
Subscribe with addListener at app root rather than on a screen. Subscribing is what flushes the native queue, and a listener that unsubscribes mid-delivery may see the link redelivered later.
Attribution always empty
Read after splash; on first open the native store may not be ready. Check the attribution level — none sends no enrichment, so it looks empty server-side even when the link resolved correctly.
Deferred does nothing on iOS
Confirm the link host is listed in DeeplinklyLinkDomains; an unlisted custom domain is ignored by design. Then confirm you are testing on a real device with a genuinely fresh install.
logEvent returns false
Check name length, param count, key length, and string value length. Keys starting with _dl_ are reserved and rejected, and null values are rejected rather than dropped.
generateLink error is undefined
Read result.errorCode / result.errorMessage. The wire shape is snake_case but the JS surface is camelCase, so result.error_code is always undefined.
Repository & more docs
The extended README, the full markdown guide, and the signal-by-signal reference live in the open-source package: README.md, docs/REACT_NATIVE_SDK.md, docs/SIGNALS.md. The example/ app is a React Native 0.87 host wired to the library, with a button for each API and a running log of received links.