@humanlabs-kr/link-expo-sdk
v1.7.0
Published
Expo SDK for HumanlabsLink - open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links. Deferred deep linking, attribution, and smart routing.
Maintainers
Readme
HumanlabsLink Expo SDK
Expo SDK for HumanlabsLink — the open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links. Add deferred deep linking, mobile attribution, and smart link routing to your Expo app. Self-hosted, privacy-first, no per-click pricing. Pure Expo modules — no native linking required.
Features
- Deferred deep linking (install attribution) — deterministic on Android (Play Install Referrer) and iOS (clipboard hand-off token, no fingerprinting), with a probabilistic device-fingerprint fallback on Android/web
- Direct deep linking with server-side URL resolution
- Event tracking with offline queue (persists across app restarts)
- Revenue tracking
- Programmatic link creation
- Pure Expo modules - no native linking required (
expo-device,expo-application,expo-localization,expo-linking;expo-clipboardoptional, enables iOS deferred attribution)
Installation
npx expo install @humanlabs-kr/link-expo-sdk expo-device expo-application expo-localization expo-linking @react-native-async-storage/async-storageexpo-clipboard is an optional peer dependency that enables iOS deferred
attribution (the clipboard hand-off token). Add it if you ship on iOS:
npx expo install expo-clipboardWithout it, iOS deferred deep links fall back to organic (a one-time warning is
logged); Android is unaffected (it uses the Play Install Referrer). The SDK
imports expo-clipboard lazily, so its absence never breaks the build.
Quick Start
import HumanlabsLink from '@humanlabs-kr/link-expo-sdk';
// Initialize (call once at app startup)
const response = await HumanlabsLink.initialize({
baseUrl: 'https://go.yourdomain.com',
apiKey: 'your-api-key', // Optional, required for link creation
appToken: 'at_a1b2c3d4...', // Recommended for Cloud — enables organic-install attribution
debug: true, // Optional, enables verbose logging
attributionWindowHours: 168, // Optional, default 7 days
});
// Check attribution
if (response.attributed) {
console.log('Install attributed!', response.deepLinkData);
}
// Listen for deferred deep links (new installs)
HumanlabsLink.onDeferredDeepLink((data) => {
if (data) {
console.log('Deferred deep link:', data.shortCode);
// Navigate to content
}
});
// Listen for direct deep links (existing users)
HumanlabsLink.onDeepLink((url, data) => {
console.log('Deep link received:', url);
if (data?.customParameters?.route) {
// Navigate to route
}
});Expo Router: add app/+native-intent.ts (required)
HumanlabsLink links open your app on the path /<app-slug>/<code>, which is not a screen in your app — so Expo Router renders its built-in "Unmatched Route" screen on every link open unless you intercept it. The SDK already resolves the URL and navigates via your onDeepLink / onDeferredDeepLink callback; +native-intent.ts just drops the raw short-link path so Expo Router doesn't claim it first. Don't resolve the short code with your own fetch — the SDK owns resolution.
// app/+native-intent.ts
const APP_SLUG = 'myapp'; // must equal your HumanlabsLink registry slug
export function redirectSystemPath({ path }: { path: string; initial: boolean }) {
try {
const firstSegment = path.replace(/^\/+/, '').split(/[/?#]/)[0];
if (firstSegment === APP_SLUG) return null; // HumanlabsLink short link → SDK routes it
} catch {}
return path;
}Rebuild the app after adding this file — it's build-time native config, not a hot-reloadable JS change.
API Reference
Initialization
await HumanlabsLink.initialize(config); // Returns InstallAttributionResponse
HumanlabsLink.isInitialized; // boolean getterDeep Linking
HumanlabsLink.onDeferredDeepLink(callback); // Deferred (install attribution)
HumanlabsLink.onDeepLink(callback); // Direct (multiple callbacks supported)
HumanlabsLink.handleDeepLink(url); // Manual URL pass-throughEvent Tracking
await HumanlabsLink.trackEvent(name, properties?);
await HumanlabsLink.trackRevenue(amount, currency, properties?);
await HumanlabsLink.flushEvents(); // Flush offline queue
await HumanlabsLink.clearEventQueue(); // Clear without sending
HumanlabsLink.queuedEventCount; // Queue size getterEvery tracked event (including auto screen views) is automatically attributed to the deep link that drove the visit using a last-click model — so re-engagement campaigns are credited to the link the user tapped, not their original install link.
Automatic Screen Tracking
Auto-emit a screen_view event on each navigation — no manual per-screen calls — so your dashboard can show an attributed screen-flow funnel per campaign. Opt-in, via React Navigation (an optional peer dependency):
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef();
await HumanlabsLink.initialize({
baseUrl: 'https://go.yourdomain.com',
autoTrackNavigation: true, // screen names only (privacy-safe default)
navigationRef, // also pass this to <NavigationContainer ref={navigationRef}>
});Route params are OFF by default (they can contain PII). To capture specific non-PII params, opt in with an explicit allow-list:
autoTrackNavigation: { captureParams: ['productId', 'category'] }Apps that don't use React Navigation simply omit navigationRef — nothing changes and there's no required dependency. Rapid transitions are debounced and same-screen re-renders deduped.
Link Creation
Requires apiKey in config.
const result = await HumanlabsLink.createLink({
deepLinkParameters: { route: 'PRODUCT', id: '123' },
title: 'Check out this product',
utmParameters: { source: 'app', medium: 'share' },
});
console.log(result.url); // https://go.yourdomain.com/tmpl/abc123Attribution Data
await HumanlabsLink.getInstallId(); // Cached install UUID
await HumanlabsLink.getInstallData(); // Cached DeepLinkData
await HumanlabsLink.isFirstLaunch(); // First launch statusData Management
await HumanlabsLink.clearData(); // Wipe all stored SDK data
HumanlabsLink.reset(); // Return to uninitialized stateNamed Exports
import { HumanlabsLinkSDK, HumanlabsLinkError, HumanlabsLinkErrorCode } from '@humanlabs-kr/link-expo-sdk';
import type {
HumanlabsLinkConfig,
DeepLinkData,
InstallAttributionResponse,
UTMParameters,
DeviceFingerprint,
CreateLinkOptions,
CreateLinkResult,
EventRequest,
AutoTrackNavigationOptions,
DeferredDeepLinkCallback,
DeepLinkCallback,
} from '@humanlabs-kr/link-expo-sdk';Error Handling
All SDK errors are instances of HumanlabsLinkError with a .code property:
| Code | When |
|-------------------------|----------------------------------------------------------------|
| NOT_INITIALIZED | Method called before initialize() |
| ALREADY_INITIALIZED | initialize() called twice |
| INVALID_CONFIGURATION | Bad config (HTTP on non-localhost, invalid attribution window) |
| NETWORK_ERROR | Network request failed after retries |
| INVALID_RESPONSE | Server returned non-2xx response |
| DECODING_ERROR | Failed to parse server response |
| INVALID_EVENT_DATA | Empty event name or negative revenue |
| MISSING_API_KEY | createLink() called without API key |
Offline Resilience
Events that fail to send are automatically queued in AsyncStorage (max 100 events, persists across app restarts). Successfully tracked events trigger a queue flush attempt. You can also manually flush or clear the queue.
Configuration
| Field | Type | Required | Default | Description |
|--------------------------|-----------|------------|-----------|-----------------------------------|
| baseUrl | string | Yes | - | HumanlabsLink server URL |
| apiKey | string | No | - | API key for link creation |
| debug | boolean | No | false | Enable verbose logging |
| attributionWindowHours | number | No | 168 | Attribution window (1–2160 hours) |
| autoTrackNavigation | boolean \| { captureParams?: string[]; debounceMs?: number } | No | false | Auto-emit screen_view from React Navigation (screen names only unless params are allow-listed) |
| navigationRef | NavigationContainerRefLike | No | - | React Navigation ref; required when autoTrackNavigation is enabled |
Other SDKs
| Platform | Package |
|----------|---------|
| React Native | @humanlabs-kr/link-expo-sdk |
| iOS (Swift) | HumanlabsLinkSDK |
| Android (Kotlin) | HumanlabsLinkSDK |
Contributing
Contributions are welcome! Please open an issue or pull request.
License
MIT License - see LICENSE for details.
Support
- Documentation: docs.humanlabs.world
- Issues: GitHub Issues
- Core Engine: HumanlabsLink Core — self-hosted open-source backend
- Cloud Platform: HumanlabsLink Cloud — hosted SaaS with dashboard, teams, and billing
