@urlshortener/react-native-deep-linking
v0.1.0
Published
React Native / Expo SDK for deep linking with deferred installation support
Maintainers
Readme
@urlshortener/react-native-deep-linking
A pure JavaScript/TypeScript React Native SDK for deep linking with deferred installation support.
Features
- Deferred Deep Linking — Match deep link payloads to fresh app installs using device fingerprinting
- Incoming Deep Links — Resolve Universal Links (iOS) and App Links (Android) via API
- Zero Native Modules — Pure JavaScript/TypeScript, works in both Expo managed and bare React Native apps
- Expo Config Plugin — Automatic iOS Associated Domains + Android App Links setup for Expo projects
- One-Time Call Guarantee — Deferred linking fires exactly once on first launch; subsequent calls are no-ops
- Graceful Error Handling — Network failures and API errors return
{ matched: false }rather than throwing
Installation
npm / yarn
npm install @urlshortener/react-native-deep-linking
# or
yarn add @urlshortener/react-native-deep-linkingPeer Dependencies
npm install react react-native @react-native-async-storage/async-storageAsyncStorage is required for the SDK to persist the one-time deferred linking flag. Install it:
npm install @react-native-async-storage/async-storage
# For Expo projects:
expo install @react-native-async-storage/async-storageQuick Start
1. Initialize the SDK
Call configure() early in your app (e.g., in a root component or app entry point):
import { configure } from '@urlshortener/react-native-deep-linking';
configure({
apiKey: 'YOUR_API_KEY_FROM_BACKEND',
baseUrl: 'https://api.yourdomain.com', // optional, defaults to https://snip.sa
});2. Handle Deferred Deep Links (First Launch)
Add the useDeferredDeepLink hook to your root component:
import React from 'react';
import { useDeferredDeepLink } from '@urlshortener/react-native-deep-linking';
export default function App() {
const { result, loading } = useDeferredDeepLink();
if (loading) {
return <SplashScreen />;
}
// If deferred linking matched, result.screen and result.params contain the intent
if (result?.matched) {
// Navigate to the appropriate screen
// e.g., navigate(result.screen, result.params)
}
return <MainApp />;
}3. Handle Incoming Deep Links (Universal Links / App Links)
Add the useIncomingDeepLink hook in the same component (or wrap it separately):
import { useIncomingDeepLink } from '@urlshortener/react-native-deep-linking';
export function DeepLinkListener() {
const { result } = useIncomingDeepLink();
React.useEffect(() => {
if (result?.matched) {
// Handle the incoming deep link
// e.g., navigate(result.screen, result.params)
}
}, [result]);
return null;
}Expo Config Plugin Setup
If you're using Expo, the config plugin automates iOS and Android deep linking configuration.
Install the Plugin
The plugin is bundled in the SDK package:
npm install @expo/config-pluginsConfigure in app.json
Add the plugin to your Expo config:
{
"expo": {
"plugins": [
[
"@urlshortener/react-native-deep-linking",
{
"domain": "short.yourdomain.com"
}
]
]
}
}The plugin will:
- iOS: Add
applinks:short.yourdomain.comto the Associated Domains entitlement - Android: Inject an intent-filter for HTTPS App Links with
android:autoVerify="true"
Bare React Native Setup
If you're not using Expo, you must manually configure deep linking:
iOS
- Open
ios/YourApp.xcodeprojin Xcode - Select your app target → Signing & Capabilities
- Add "Associated Domains" capability
- Add
applinks:short.yourdomain.com
Android
Edit android/app/src/main/AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<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="short.yourdomain.com" />
</intent-filter>
</activity>API Reference
configure(options: { apiKey: string; baseUrl?: string }): void
Initialize the SDK. Must be called before using any other functions.
Parameters:
apiKey(required) — Your app's API key from the backendbaseUrl(optional) — Base URL of the API (defaults tohttps://snip.sa)
Example:
configure({
apiKey: 'sk_live_abc123...',
baseUrl: 'https://api.yourdomain.com',
});useDeferredDeepLink(): { result: DeepLinkResult | null; loading: boolean }
React hook for handling deferred deep links on first app launch.
Returns:
result— The matched deep link data (ornullwhile loading)loading— Whether the request is in flight
When to use: Add to your root component to handle deep links from install campaigns.
Example:
const { result, loading } = useDeferredDeepLink();
if (loading) return <Splash />;
if (result?.matched) {
// Navigate to result.screen with result.params
}handleDeferredDeepLink(): Promise<DeepLinkResult>
Imperative version of useDeferredDeepLink(). Useful if you need to handle deferred linking outside a React component.
Returns: Promise<DeepLinkResult>
Example:
const result = await handleDeferredDeepLink();
if (result.matched) {
navigate(result.screen, result.params);
}useIncomingDeepLink(): { result: DeepLinkResult | null }
React hook for listening to Universal Links (iOS) and App Links (Android).
Returns:
result— The resolved deep link (ornullif no link received yet)
When to use: Wrap this in a component to listen for deep links while the app is running.
Example:
const { result } = useIncomingDeepLink();
useEffect(() => {
if (result?.matched) {
navigate(result.screen, result.params);
}
}, [result]);resolveDeepLink(url: string): Promise<DeepLinkResult>
Imperative API to resolve a deep link URL (e.g., from a clicked link).
Parameters:
url— The incoming URL (e.g.,https://short.yourdomain.com/dl/abc123)
Returns: Promise<DeepLinkResult>
Example:
const deepLink = await resolveDeepLink('https://short.yourdomain.com/dl/abc123');
if (deepLink.matched) {
navigate(deepLink.screen, deepLink.params);
}Types
DeepLinkResult
interface DeepLinkResult {
matched: boolean; // Whether a deep link was found
screen?: string; // The screen name to navigate to
params?: Record<string, any>; // Navigation parameters
confidence?: number; // Confidence score (0-1) for deferred linking
}Error Handling
The SDK is designed to fail gracefully:
- Network errors — Returns
{ matched: false }instead of throwing - Invalid API key — Returns
{ matched: false }(logs to console) - 404 / Deep link not found — Returns
{ matched: false } - Invalid shortCode — Returns
{ matched: false }(no network call made)
Always check result.matched before attempting to navigate.
One-Time Deferred Link Guarantee
The handleDeferredDeepLink() function uses AsyncStorage to ensure it fires exactly once, even if:
- The function is called multiple times
- The app crashes and restarts
- The network request fails
After the first attempt (success or failure), the flag is set and all subsequent calls return { matched: false } immediately without making a network request.
To reset this behavior (for testing), clear the AsyncStorage key:
import AsyncStorage from '@react-native-async-storage/async-storage';
await AsyncStorage.removeItem('urlshortener_sdk_deferred_checked');Testing
The SDK exports clean separation between hooks and imperative APIs, making it easy to test:
import { handleDeferredDeepLink, resolveDeepLink } from '@urlshortener/react-native-deep-linking';
// Test deferred linking
test('should handle matched deep link', async () => {
const result = await handleDeferredDeepLink();
expect(result.matched).toBe(true);
expect(result.screen).toBe('product');
});
// Test URL resolution
test('should resolve deep link', async () => {
const result = await resolveDeepLink('https://short.yourdomain.com/dl/abc123');
expect(result.matched).toBe(true);
});Mock @react-native-async-storage/async-storage and fetch in your test setup for full control.
Platform-Specific Notes
iOS
- Requires
applinks:associated domain entry to be configured - Works with both Universal Links (https) and custom URL schemes
- SDK uses React Native's
Linking.getInitialURL()to detect cold starts
Android
- Requires
android:autoVerify="true"in the App Links intent-filter - Must use
https://(not custom schemes likemyapp://for auto-verification) - SDK uses React Native's
Linking.getInitialURL()andLinking.addEventListener()for detection
Troubleshooting
Deferred link not matching
Cause: The device fingerprint (IP, OS version, screen dimensions, install time) didn't match any stored payloads.
Solution: Ensure the backend captured a click with the same IP and similar device fingerprint before the install.
Deep links not opening the app
Cause: Associated Domains or App Links not properly configured.
Solution:
- iOS: Verify
applinks:entry is present in Xcode's Signing & Capabilities - Android: Rebuild APK/AAB after adding intent-filter, and verify on a real device (emulator may not support App Links auto-verification)
"Invalid API key" errors
Cause: The API key is missing, malformed, or inactive on the backend.
Solution: Verify the API key matches the one created in the backend app registration.
Contributing
Bug reports and PRs welcome. Run tests with:
npm test
npm run test:coverageLicense
MIT
