Linkzly

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

8 min read

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:

  1. Go to Dashboard > Apps and click "Register App".
  2. Enter your iOS Bundle ID and/or Android Package Name (plus SHA-256 fingerprints for Android).
  3. Choose a verification method (Hosted is recommended for a quick start).
  4. 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