React Native SDK Setup Guide
---
sidebar_label: React Native sidebar_category: Gaming SDK Guides
React Native SDK Setup Guide
This guide covers integrating Gaming Intelligence into a React Native app using TypeScript. There is no npm package to install — integration uses React Native's built-in fetch API to call the Gaming Intelligence REST API directly. This works for both iOS and Android from a single codebase, and is compatible with both bare React Native and Expo projects.
Prerequisites
Before you begin, have the following ready:
| Requirement | Where to Find It |
|---|---|
| SDK Key | Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → SDK Key |
| Organization ID | Linkzly Console → Organization Settings |
| Game ID | Linkzly Console → Gaming → [Your Game] → Settings → General Settings |
| React Native Version | 0.71 or later |
| Expo | Compatible with bare workflow and managed workflow (Expo SDK 49+) |
Your SDK key is masked by default in the console. Use the eye icon to reveal it or the copy button to copy it directly.
Installation
No npm packages are required for basic integration — fetch and AppState are built into React Native. However, if your app targets React Native versions below 0.71 and needs UUID generation, install one of these:
# <span id="option-a-uuid-package-with-a-polyfill-most-common"></span>Option A: uuid package with a polyfill (most common)
npm install uuid react-native-get-random-values
# <span id="then-import-react-native-get-random-values-at-the-top-of-your-indexjs-entry-file"></span>Then import 'react-native-get-random-values' at the top of your index.js entry file
# <span id="option-b-expo-provides-cryptorandomuuid-natively-from-expo-sdk-49"></span>Option B: Expo (provides crypto.randomUUID natively from Expo SDK 49+)
npx expo install expo-crypto
For HMAC signing (required when HMAC Signing Required is enabled in Game Settings), crypto.subtle is available natively in React Native 0.74+ and Expo SDK 51+. For older versions, see the HMAC Signing section.
Setup
Create src/lib/linkzlyGaming.ts. This file is the complete integration — the client class, TypeScript types, and the HMAC signing helper:
// src/lib/linkzlyGaming.ts
import { Platform } from 'react-native';
// ── Types ──────────────────────────────────────────────────────────────────────
export interface EventData {
[key: string]: string | number | boolean | null | EventData;
}
export interface GameEvent {
event_id: string;
event_type: string;
timestamp: string; // ISO 8601
platform: string; // 'ios' | 'android'
player_id: string;
session_id: string;
sdk_version?: string;
game_version?: string;
country?: string;
data?: EventData;
}
export interface BatchResponse {
success: boolean;
batch_id: string;
events_received: number;
events_valid: number;
events_dropped: number;
trace_id: string;
server_timestamp: string;
}
export interface LinkzlyGamingOptions {
sdkKey: string;
organizationId: string;
gameId: string;
gameVersion?: string;
baseUrl?: string;
signingSecret?: string; // Required when HMAC Signing is enabled
}
// ── Client ─────────────────────────────────────────────────────────────────────
export class LinkzlyGamingClient {
private sdkKey: string;
private organizationId: string;
private gameId: string;
private gameVersion: string;
private baseUrl: string;
private signingSecret?: string;
private readonly platform: 'ios' | 'android';
constructor(options: LinkzlyGamingOptions) {
this.sdkKey = options.sdkKey;
this.organizationId = options.organizationId;
this.gameId = options.gameId;
this.gameVersion = options.gameVersion ?? '';
this.baseUrl = options.baseUrl ?? 'https://gaming.linkzly.com';
this.signingSecret = options.signingSecret;
// Platform.OS is 'ios' or 'android' in React Native
this.platform = Platform.OS === 'ios' ? 'ios' : 'android';
}
/**
* Build a fully-populated GameEvent object.
* Call this to construct events before passing them to sendEvents().
*/
buildEvent(
eventType: string,
playerId: string,
sessionId: string,
data?: EventData
): GameEvent {
return {
event_id: randomUUID(),
event_type: eventType,
timestamp: new Date().toISOString(),
platform: this.platform,
player_id: playerId,
session_id: sessionId,
game_version: this.gameVersion || undefined,
data,
};
}
/**
* Send an array of events to the Linkzly ingestion endpoint.
* Returns a 202 BatchResponse on success, or throws on network/HTTP error.
*/
async sendEvents(events: GameEvent[]): Promise<BatchResponse> {
const payload = {
batch_id: randomUUID(),
batch_timestamp: new Date().toISOString(),
events,
};
const bodyString = JSON.stringify(payload);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.sdkKey}`,
'X-Organization-ID': this.organizationId,
'X-Game-ID': this.gameId,
};
if (this.signingSecret) {
const timestamp = String(Date.now()); // Unix milliseconds
const nonce = randomUUID();
const signature = await computeHmacSignature(
this.signingSecret, timestamp, nonce, bodyString
);
headers['X-Signature-256'] = signature;
headers['X-Timestamp'] = timestamp;
headers['X-Nonce'] = nonce;
}
const response = await fetch(`${this.baseUrl}/api/v1/gaming/events`, {
method: 'POST',
headers,
body: bodyString,
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Linkzly ingestion error ${response.status}: ${text}`);
}
return response.json() as Promise<BatchResponse>;
}
}
// ── UUID helper ────────────────────────────────────────────────────────────────
/**
* Generate a UUID v4.
* Uses crypto.randomUUID() on React Native 0.71+ and Expo SDK 49+.
* Falls back to a Math.random()-based implementation for older versions.
*/
function randomUUID(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
// Fallback for React Native < 0.71 without react-native-get-random-values
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
}
// ── HMAC signing ───────────────────────────────────────────────────────────────
/**
* Compute HMAC-SHA256 and return a hex string.
* Uses crypto.subtle (available in React Native 0.74+ / Expo SDK 51+).
* For older versions, see the HMAC Signing section of the SDK guide.
*/
export async function computeHmacSignature(
signingSecret: string,
timestamp: string,
nonce: string,
bodyString: string
): Promise<string> {
const signingString = `${timestamp}.${nonce}.${bodyString}`;
if (typeof crypto !== 'undefined' && crypto.subtle) {
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(signingSecret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
key,
new TextEncoder().encode(signingString)
);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
throw new Error(
'crypto.subtle is not available. Upgrade to React Native 0.74+ / Expo SDK 51+, ' +
'or install react-native-quick-crypto. See the HMAC Signing section of the SDK guide.'
);
}
Instantiation
Create a shared singleton module that your entire app imports. This prevents multiple client instances from being created:
// src/lib/gaming.ts
import { LinkzlyGamingClient } from './linkzlyGaming';
export const gaming = new LinkzlyGamingClient({
sdkKey: process.env.LINKZLY_SDK_KEY ?? '',
organizationId: process.env.LINKZLY_ORG_ID ?? '',
gameId: process.env.LINKZLY_GAME_ID ?? '',
gameVersion: process.env.LINKZLY_GAME_VERSION ?? '1.0.0',
signingSecret: process.env.LINKZLY_SIGNING_SECRET, // optional
});
Use .env files with react-native-dotenv (bare workflow) or Expo's EXPO_PUBLIC_ prefix convention (managed workflow) to supply credentials without hardcoding them.
Expo managed workflow example:
// src/lib/gaming.ts (Expo)
import Constants from 'expo-constants';
import { LinkzlyGamingClient } from './linkzlyGaming';
const extra = Constants.expoConfig?.extra ?? {};
export const gaming = new LinkzlyGamingClient({
sdkKey: extra.linkzlySdkKey ?? '',
organizationId: extra.linkzlyOrgId ?? '',
gameId: extra.linkzlyGameId ?? '',
gameVersion: extra.linkzlyGameVersion ?? '1.0.0',
signingSecret: extra.linkzlySigningSecret,
});
// app.config.ts
export default {
extra: {
linkzlySdkKey: process.env.LINKZLY_SDK_KEY,
linkzlyOrgId: process.env.LINKZLY_ORG_ID,
linkzlyGameId: process.env.LINKZLY_GAME_ID,
linkzlyGameVersion: process.env.LINKZLY_GAME_VERSION,
linkzlySigningSecret: process.env.LINKZLY_SIGNING_SECRET,
},
};
Tracking Events
Session Start
Send a session_start event when the player begins a play session. Generate a new sessionId UUID for each session:
import { gaming } from '../lib/gaming';
const sessionId = crypto.randomUUID();
const playerId = 'player-001'; // Your authenticated player ID
await gaming.sendEvents([
gaming.buildEvent('session_start', playerId, sessionId),
]);
Tracking Game Events
// Level completed
await gaming.sendEvents([
gaming.buildEvent('level_complete', playerId, sessionId, {
level_id: 'level-4',
score: 9800,
duration_seconds: 112,
stars: 3,
}),
]);
// In-app purchase
await gaming.sendEvents([
gaming.buildEvent('purchase', playerId, sessionId, {
item_id: 'starter-pack',
amount: 2.99,
currency: 'USD',
store: Platform.OS === 'ios' ? 'app_store' : 'google_play',
}),
]);
// Achievement unlocked
await gaming.sendEvents([
gaming.buildEvent('achievement_unlocked', playerId, sessionId, {
achievement_id: 'first-win',
achievement_name: 'First Victory',
points: 50,
}),
]);
// Custom event
await gaming.sendEvents([
gaming.buildEvent('custom', playerId, sessionId, {
custom_event_name: 'tutorial_skipped',
step: 3,
}),
]);
For a complete list of supported event types, see Section 17.1 of the SDKs documentation.
Batching Multiple Events
Send multiple events in a single request to reduce network overhead. The ingestion endpoint accepts up to 1,000 events per batch:
await gaming.sendEvents([
gaming.buildEvent('currency_earned', playerId, sessionId, {
amount: 500,
currency_type: 'coins',
source: 'daily_reward',
}),
gaming.buildEvent('level_start', playerId, sessionId, {
level_id: 'level-5',
}),
gaming.buildEvent('achievement_unlocked', playerId, sessionId, {
achievement_id: 'streak-7',
achievement_name: '7-Day Streak',
}),
]);
Session Management
Use React Native's AppState module to send session lifecycle events when the app moves between foreground and background. The recommended pattern is a custom hook that you mount in your root navigator component:
// src/hooks/useGamingSession.ts
import { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import { gaming } from '../lib/gaming';
export function useGamingSession(playerId: string | null): React.MutableRefObject<string> {
const sessionIdRef = useRef<string>(crypto.randomUUID());
useEffect(() => {
if (!playerId) return;
// Send session_start on initial mount
gaming
.sendEvents([gaming.buildEvent('session_start', playerId, sessionIdRef.current)])
.catch(console.error);
const subscription = AppState.addEventListener(
'change',
(nextState: AppStateStatus) => {
if (nextState === 'active') {
// App returned to foreground — generate a new session ID
sessionIdRef.current = crypto.randomUUID();
gaming
.sendEvents([gaming.buildEvent('session_start', playerId, sessionIdRef.current)])
.catch(console.error);
}
if (nextState === 'background' || nextState === 'inactive') {
// App moved to background — end the current session
gaming
.sendEvents([gaming.buildEvent('session_end', playerId, sessionIdRef.current)])
.catch(console.error);
}
}
);
return () => {
// Component unmount — end the session and remove the listener
gaming
.sendEvents([gaming.buildEvent('session_end', playerId, sessionIdRef.current)])
.catch(console.error);
subscription.remove();
};
}, [playerId]);
return sessionIdRef;
}
Mount the hook in your root App component:
// App.tsx
import { useGamingSession } from './src/hooks/useGamingSession';
export default function App() {
const playerId = useAuthStore(state => state.playerId); // null before login
const sessionIdRef = useGamingSession(playerId);
return <RootNavigator />;
}
Pass sessionIdRef.current to child components that need to track events within the same session.
Platform Detection
The client automatically sets the platform field to "ios" or "android" based on Platform.OS. You do not need to set it manually. The buildEvent() helper handles this for you.
If your app also targets web via React Native Web (Platform.OS === 'web'), the client maps it to "android" by default. Override this at construction time if needed:
// Override for a React Native Web build
export const gaming = new LinkzlyGamingClient({
sdkKey: 'YOUR_SDK_KEY',
organizationId: 'YOUR_ORG_ID',
gameId: 'YOUR_GAME_ID',
// For web targets, construct with a modified baseUrl or handle separately
});
For store-specific data, use Platform.OS directly in your event payloads:
import { Platform } from 'react-native';
await gaming.sendEvents([
gaming.buildEvent('purchase', playerId, sessionId, {
item_id: 'gem-pack-100',
amount: 0.99,
currency: 'USD',
store: Platform.OS === 'ios' ? 'app_store' : 'google_play',
}),
]);
HMAC Request Signing
HMAC signing is enabled by default for all new games. When enabled, every request must include a valid HMAC-SHA256 signature.
Pass signingSecret in the client options — the client computes the signature and attaches X-Signature-256, X-Timestamp, and X-Nonce to every outgoing request automatically:
export const gaming = new LinkzlyGamingClient({
sdkKey: 'YOUR_SDK_KEY',
organizationId: 'YOUR_ORG_ID',
gameId: 'YOUR_GAME_ID',
signingSecret: 'YOUR_SIGNING_SECRET',
});
How the signature is computed:
- Serialize the request body to a JSON string.
- Build the signing string:
"{timestamp}.{nonce}.{bodyString}" - Compute
HMAC-SHA256of the signing string using your signing secret as the key. - Hex-encode the resulting bytes.
- Send the result in
X-Signature-256, along withX-Timestamp(Unix milliseconds) andX-Nonce(UUID v4).
The replay window is fixed at 300 seconds. Requests timestamped more than 5 minutes from the server clock are rejected with 401 Unauthorized.
Crypto availability by platform:
| Environment | crypto.subtle available |
Notes |
|---|---|---|
| React Native 0.74+ | Yes | Built into Hermes |
| Expo SDK 51+ | Yes | Built into the Hermes runtime |
| React Native 0.71–0.73 | No | Use react-native-quick-crypto |
| Expo SDK 49–50 | No | Use expo-crypto |
Using react-native-quick-crypto for older React Native versions:
npm install react-native-quick-crypto
npx pod-install # iOS only
// src/lib/hmac-legacy.ts
import QuickCrypto from 'react-native-quick-crypto';
export async function computeHmacSignatureLegacy(
signingSecret: string,
timestamp: string,
nonce: string,
bodyString: string
): Promise<string> {
const signingString = `${timestamp}.${nonce}.${bodyString}`;
const hmac = QuickCrypto.createHmac('sha256', signingSecret);
hmac.update(signingString);
return hmac.digest('hex');
}
Using expo-crypto for older Expo versions:
npx expo install expo-crypto
// src/lib/hmac-expo.ts
import * as Crypto from 'expo-crypto';
export async function computeHmacSignatureExpo(
signingSecret: string,
timestamp: string,
nonce: string,
bodyString: string
): Promise<string> {
const signingString = `${timestamp}.${nonce}.${bodyString}`;
// expo-crypto does not expose raw HMAC; use react-native-quick-crypto for full HMAC support
// Alternatively, disable HMAC for development and enable it server-side via a backend proxy
throw new Error('expo-crypto does not support HMAC-SHA256 directly. Use react-native-quick-crypto.');
}
Security note: Do not hardcode your signing secret in JavaScript source files that are bundled into the app binary. App binaries can be reverse-engineered. For production mobile apps, the recommended pattern is to proxy event batches through your backend server, which holds the signing secret and computes the signature server-side before forwarding to the Linkzly ingestion endpoint. This removes the need to distribute the signing secret to client devices.
If you choose to embed the signing secret in the app (for example, in a first-party game where reverse engineering is an acceptable risk), use Expo's
EXPO_PUBLIC_environment variables or Gradle/Xcode build settings to inject it at build time rather than committing it to source control.
To disable HMAC for development, toggle off HMAC Signing Required in Game Settings → Security Settings and omit signingSecret from the client options. Re-enable it before releasing to production.
Error Handling
Wrap sendEvents() calls in try/catch to handle both network failures and HTTP error status codes:
async function safeTrack(
eventType: string,
playerId: string,
sessionId: string,
data?: EventData
): Promise<void> {
try {
const result = await gaming.sendEvents([
gaming.buildEvent(eventType, playerId, sessionId, data),
]);
if (result.events_dropped > 0) {
console.warn(
`[Linkzly] ${result.events_dropped} event(s) dropped. ` +
`Trace ID: ${result.trace_id}`
);
}
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('429')) {
// Rate limited — back off and retry after the Retry-After interval
console.warn('[Linkzly] Rate limited. Retry after backoff.');
} else if (message.includes('401')) {
console.error('[Linkzly] Authentication failed — check SDK key and signing secret.');
} else if (message.includes('400')) {
console.error('[Linkzly] Malformed event payload — check required fields.');
} else if (
message.includes('Network request failed') ||
message.includes('Failed to fetch')
) {
// No connectivity — store locally and retry when back online
console.warn('[Linkzly] No network. Event queued for retry.');
} else {
console.error('[Linkzly] Send error:', message);
}
}
}
Offline support: React Native does not include a built-in event queue. For games with intermittent connectivity (common on mobile), store unsent events in AsyncStorage or a SQLite database and flush them using @react-native-community/netinfo when connectivity is restored.
Android Network Security
If you are testing against a local development server (http://) instead of the production HTTPS endpoint, Android's network security policy blocks cleartext traffic by default.
Create android/app/src/main/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>
Reference it in android/app/src/main/AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
The development endpoint at https://linkzly-gaming-tracking-development.mec-fahid.workers.dev uses HTTPS and does not require any network security configuration changes.
Troubleshooting
Network request failed error
- Confirm the device has internet connectivity. Test with a simple
fetch('https://gaming.linkzly.com')call. - On Android, check that the network security config allows HTTPS traffic to
gaming.linkzly.com. - In Expo Go during development, ensure your development machine and device are on the same network if routing through a local proxy.
401 Unauthorized with HMAC enabled
- Confirm
signingSecretin the client options matches the value in Game Settings → SDK Configuration exactly. - Verify the device clock is synchronized. The replay window is 300 seconds (5 minutes).
- Ensure the body string passed to
computeHmacSignatureis identical to the string sent in the request body — do not JSON-serialize the payload twice.
Events dropped (events_dropped > 0)
Check that each event object includes all required fields: event_id, event_type, timestamp (ISO 8601), platform, player_id, and session_id. Events with missing required fields are dropped silently. The trace_id in the response can be provided to Linkzly support for diagnosis.
crypto.randomUUID is not a function
Upgrade to React Native 0.71 or later, or install react-native-get-random-values and import it at the top of your entry file (index.js). The randomUUID() helper in the client class includes a Math.random()-based fallback for environments where the native implementation is unavailable, but that fallback is not cryptographically secure — use it only for testing.
Platform detected incorrectly
Platform.OS returns 'ios', 'android', or 'web'. The client maps 'ios' and anything else to their respective platform values. If you are building a React Native Web target and need the platform set to 'web', construct a separate client instance with an explicit override for the web build target.
HMAC signing throws "crypto.subtle is not available"
Install react-native-quick-crypto (bare workflow) or use the expo-crypto-based approach described in the HMAC Signing section. For Expo SDK 51+ and React Native 0.74+, crypto.subtle is available natively and no additional packages are required.
Was this helpful?
Help us improve our documentation