React Native SDK Setup Guide
The Linkzly React Native SDK is a TypeScript wrapper over the native iOS and Android SDKs. It tracks installs, opens, and custom events; handles Universal Links
React Native SDK Setup Guide
The Linkzly React Native SDK is a TypeScript wrapper over the native iOS and Android SDKs. It tracks installs, opens, and custom events; handles Universal Links (iOS) and App Links (Android), including deferred deep linking; captures affiliate attribution for S2S postbacks; supports SKAdNetwork and ATT on iOS; integrates Firebase Cloud Messaging for push; and exposes a gaming event pipeline.
The package is @linkzly/react-native-sdk, has zero JavaScript dependencies, ships with full TypeScript types, and works with both the bare workflow and Expo (managed/dev-client). It is compatible with both Old and New Architecture.
Prerequisites
Before integrating, set up your app in the Linkzly Console:
- Go to Dashboard > Apps and click "Register App".
- Enter your iOS Bundle ID and/or Android Package Name (plus SHA-256 fingerprints for Android).
- Choose a verification method (Hosted is recommended for a quick start).
- Copy your SDK Key from the post-creation wizard, or from Manage App > Overview > SDK Configuration.
| Requirement | Minimum | Recommended |
|---|---|---|
| React Native | 0.71.2+ | 0.73+ |
| Node.js | 16+ | 18+ |
| iOS | 12.0+ | 15.0+ |
| Android SDK | API 21+ | API 33+ |
Your SDK key starts with slk_ and uniquely identifies one app — do not share keys between apps.
Installation
npm install @linkzly/react-native-sdk
# <span id="or"></span>or
yarn add @linkzly/react-native-sdk
iOS
Pin the native iOS SDK in ios/Podfile (before use_native_modules!):
pod 'LinkzlySDK', :git => 'https://github.com/Linkzly/linkzly-ios-sdk.git', :tag => '1.0.3'
Then:
cd ios && pod install && cd ..
Android
Add JitPack to the root android/build.gradle:
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
Platform Setup
iOS — AppDelegate forwarding (required for warm-start deep links)
For deep links to work when the app is in the background, you must forward incoming URLs to both RCTLinkingManager and LinkzlySDK. The setup below works on both Old and New Architecture.
Bridging header (ios/YourApp-Bridging-Header.h):
#import <React/RCTLinkingManager.h>
Swift AppDelegate (ios/YourApp/AppDelegate.swift):
import Linkzly
override func application(_ app: UIApplication, open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let handledByRN = RCTLinkingManager.application(app, open: url, options: options)
_ = LinkzlySDK.handleUniversalLink(url)
return handledByRN
}
override func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
let handledByRN = RCTLinkingManager.application(application,
continue: userActivity,
restorationHandler: restorationHandler)
_ = LinkzlySDK.handleUniversalLink(userActivity)
return handledByRN
}
Do not write import React_RCTLinkingManager — the module name varies between RN versions. Use the bridging header.
For Objective-C AppDelegates, import <React/RCTLinkingManager.h> and <Linkzly/Linkzly-Swift.h> and implement the equivalent openURL: and continueUserActivity: methods.
You also need an Associated Domains entitlement (applinks:{prefix}.linkz.ly) and an NSUserTrackingUsageDescription key in Info.plist. See the iOS SDK guide for details.
Android — MainActivity onNewIntent (required for warm-start deep links)
React Native's Linking.addEventListener is unreliable on Android for warm starts. Override onNewIntent to route through the native module instead:
import android.content.Intent
import com.linkzly.reactnative.LinkzlyReactNativeModule
class MainActivity : ReactActivity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
LinkzlyReactNativeModule.getLatestInstance()?.handleIntent(intent)
}
}
Add the intent filters (custom URL scheme + App Links with android:autoVerify="true") and android:launchMode="singleTask" to MainActivity in AndroidManifest.xml. See the Android SDK guide for the exact manifest snippet.
Initialization
import LinkzlySDK, { Environment } from '@linkzly/react-native-sdk';
await LinkzlySDK.configure('slk_your_key_from_console', Environment.PRODUCTION);
Environments are Environment.PRODUCTION, Environment.STAGING, and Environment.DEVELOPMENT (verbose logging).
A typical app-startup pattern:
import React, { useEffect } from 'react';
import { Linking } from 'react-native';
import LinkzlySDK, { Environment } from '@linkzly/react-native-sdk';
function App() {
useEffect(() => {
// 1. Subscribe before configuring so cold-start events are not missed
const unsubscribe = LinkzlySDK.addDeepLinkListener((data) => {
// data.path, data.parameters, data.smartLinkId, data.clickId
});
// 2. Handle cold-start URLs (app launched from a link)
Linking.getInitialURL().then((url) => {
if (url) { /* parse and route */ }
});
// 3. Configure the SDK
LinkzlySDK.configure('slk_your_key_from_console', Environment.PRODUCTION);
return () => unsubscribe();
}, []);
return <YourApp />;
}
Linking.getInitialURL() only works on cold starts. Warm-start URLs come through addDeepLinkListener (or React Native's Linking.addEventListener('url') if you prefer).
Tracking Events
// Custom event
await LinkzlySDK.trackEvent('purchase_completed', {
product_id: '12345', amount: 29.99, currency: 'USD'
});
// Purchase
await LinkzlySDK.trackPurchase({ amount: 9.99, currency: 'USD', sku: 'premium_monthly' });
// Batch
await LinkzlySDK.trackEventBatch([
{ eventName: 'screen_view', parameters: { screen: 'home' } },
{ eventName: 'button_click', parameters: { button: 'signup' } }
]);
// Install / open (auto on lifecycle; explicit form when you need the deferred-deep-link return)
const installData = await LinkzlySDK.trackInstall();
const openData = await LinkzlySDK.trackOpen();
// Session control (sessions are automatic by default)
await LinkzlySDK.startSession();
await LinkzlySDK.endSession();
// Event queue
const success = await LinkzlySDK.flushEvents();
const count = await LinkzlySDK.getPendingEventCount();
User Identity
await LinkzlySDK.setUserID('user_12345');
const userId = await LinkzlySDK.getUserID();
const visitorId = await LinkzlySDK.getVisitorID();
await LinkzlySDK.resetVisitorID();
Deep Linking
const unsubscribe = LinkzlySDK.addDeepLinkListener((data) => {
// data.path, data.parameters, data.smartLinkId, data.clickId
});
// Universal/App link raw capture (fires before server enrichment)
LinkzlySDK.addUniversalLinkListener((event) => {
// event.url, event.attributionData
});
// Manual handling (when autoHandleDeepLinks: false)
await LinkzlySDK.handleUniversalLink(url);
LinkzlySDK.removeAllListeners();
DeepLinkData shape:
interface DeepLinkData {
url?: string;
path?: string;
parameters: Record<string, any>;
smartLinkId?: string;
clickId?: string;
}
Affiliate Attribution
Affiliate clicks are not captured automatically — call captureAffiliateAttribution(url) from your deep link handler:
LinkzlySDK.addUniversalLinkListener(async (data) => {
await LinkzlySDK.captureAffiliateAttribution(data.url);
});
At conversion time, retrieve the click ID for an S2S postback:
const clickId = await LinkzlySDK.getAffiliateClickId();
if (clickId) {
await fetch('https://your-backend.com/api/conversions', {
method: 'POST',
body: JSON.stringify({ click_id: clickId, order_value: 99.99, currency: 'USD' })
});
}
const attribution = await LinkzlySDK.getAffiliateAttribution();
const hasAttribution = await LinkzlySDK.hasAffiliateAttribution();
await LinkzlySDK.clearAffiliateAttribution();
Storage delegates to native — Keychain on iOS, EncryptedSharedPreferences (AES256-GCM) on Android. Data expires after 30 days.
SKAdNetwork and ATT (iOS)
import { Platform } from 'react-native';
if (Platform.OS === 'ios') {
const status = await LinkzlySDK.requestTrackingPermission();
// 'authorized' | 'denied' | 'restricted' | 'notDetermined'
const idfa = await LinkzlySDK.getIDFA();
const attStatus = await LinkzlySDK.getATTStatus();
await LinkzlySDK.updateConversionValue(42); // 0-63
}
Set NSUserTrackingUsageDescription in Info.plist. IDFA is only collected after the user grants ATT.
Push Notifications
await LinkzlySDK.configure('slk_your_key_from_console');
const success = await LinkzlySDK.initializePush(); // returns false if Firebase Messaging is unavailable
await LinkzlySDK.disablePush();
initializePush() subscribes the device to a Linkzly FCM broadcast topic via runtime reflection — no Firebase dependency in the SDK. If you use OneSignal, Braze, or another non-FCM provider, skip these methods; the Linkzly backend supports multiple push providers and will deliver through whichever is configured in the console.
Privacy Controls
await LinkzlySDK.setTrackingEnabled(false);
const isEnabled = await LinkzlySDK.isTrackingEnabled();
await LinkzlySDK.setAdvertisingTrackingEnabled(false);
const isAdEnabled = await LinkzlySDK.isAdvertisingTrackingEnabled();
Gaming Intelligence
A separate event pipeline for game telemetry with automatic batching, retry logic, optional HMAC signing, and offline support.
await LinkzlySDK.configureGamingTracking(
'your_gaming_api_key',
'your_org_id',
'your_game_id',
Environment.PRODUCTION,
{ gameVersion: '1.2.0', maxBatchSize: 50, flushIntervalMs: 10_000, debug: true }
);
await LinkzlySDK.identifyGamingPlayer('player_12345', { level: 42, vip_tier: 'gold' });
await LinkzlySDK.trackGamingEvent('level_complete', {
level: 5, score: 12500, stars: 3
});
// `immediate: true` bypasses batching
await LinkzlySDK.trackGamingEvent(
'purchase',
{ item_id: 'sword_of_fire', price: 4.99, currency: 'USD' },
true
);
await LinkzlySDK.setGamingAttribution(
'click_abc123',
'https://yourgame.com/promo',
{ campaign: 'summer_sale' }
);
await LinkzlySDK.startGamingSession();
await LinkzlySDK.endGamingSession();
await LinkzlySDK.flushGamingEvents();
const status = await LinkzlySDK.getGamingStatus();
// status.pendingEventCount, status.hasInflightBatch
await LinkzlySDK.resetGamingTracking();
HMAC request signing
When HMAC signing is enabled in Game Settings → Security Settings, pass signingSecret in the options object. The SDK computes the HMAC-SHA256 signature and attaches X-Signature-256, X-Timestamp, and X-Nonce to every batch. The replay window is fixed at 300 seconds — device clocks must be within 5 minutes of UTC. Your signing secret is in Game Settings → SDK Configuration, separate from the SDK Key.
Options reference
| Option | Default |
|---|---|
gameVersion |
"" |
maxBatchSize |
100 |
maxBatchBytes |
524288 (512 KB) |
flushIntervalMs |
5000 |
maxRetries |
3 |
retryDelayMs |
1000 |
maxQueueSize |
10000 |
sessionTimeoutMs |
1800000 (30 min) |
autoSessionTracking |
true |
signingSecret |
null |
debug |
false |
API Reference Summary
| Area | Methods |
|---|---|
| Configuration | configure, setAutoHandleDeepLinks, isAutoHandleDeepLinksEnabled |
| Tracking | trackInstall, trackOpen, trackEvent, trackPurchase, trackEventBatch |
| Deep links | addDeepLinkListener, addUniversalLinkListener, handleUniversalLink, removeAllListeners |
| Event queue | flushEvents, getPendingEventCount |
| Users / sessions | setUserID, getUserID, getVisitorID, resetVisitorID, startSession, endSession |
| Privacy | setTrackingEnabled, isTrackingEnabled, setAdvertisingTrackingEnabled, isAdvertisingTrackingEnabled |
| iOS | requestTrackingPermission, getIDFA, getATTStatus, updateConversionValue |
| Affiliate | captureAffiliateAttribution, getAffiliateAttribution, getAffiliateClickId, hasAffiliateAttribution, clearAffiliateAttribution |
| Push | initializePush, disablePush |
| Gaming | configureGamingTracking, identifyGamingPlayer, trackGamingEvent, startGamingSession, endGamingSession, flushGamingEvents, setGamingAttribution, clearGamingAttribution, resetGamingTracking, getGamingStatus |
Troubleshooting
Deep links don't fire on warm start (iOS). Make sure both RCTLinkingManager forwards are in your AppDelegate (Swift bridging header or Objective-C imports), then rebuild from Xcode after pod install.
Deep links don't fire on warm start (Android). Override onNewIntent in MainActivity to call LinkzlyReactNativeModule.getLatestInstance()?.handleIntent(intent), set android:launchMode="singleTask", and rebuild the Android app.
Listener never fires. Subscribe with addDeepLinkListener before calling configure, and remember to call the returned unsubscribe in your cleanup.
initializePush() returns false. Firebase Messaging is not integrated, or it was not initialized before the call.
Gaming 401 with HMAC enabled. The signingSecret must match Game Settings exactly, and the device clock must be within 5 minutes of UTC.
Was this helpful?
Help us improve our documentation