Deferred Deep Links in React Native: Complete Integration Guide
The two cases, quickly
- Direct deep link - app is already installed. The OS routes the tap straight to your app via a universal link (iOS) or app link (Android). Standard Linking API territory.
- Deferred deep link - app is not installed yet. User taps, lands in the browser/store, installs, opens the app for the first time. There is no OS mechanism for this - something has to persist the link's intent server-side and hand it back on first open.
Both need to be handled for a link campaign to actually work end to end. Below is the full setup for both.
1. Install the SDK
npm install linktrail-react-native
# or yarn add linktrail-react-native
cd ios && pod install
2. Native configuration
iOS - Associated Domains
In Xcode, enable the Associated Domains capability and add your link domain:
applinks:links.yourapp.com
Your server needs a valid apple-app-site-association file at https://links.yourapp.com/.well-known/apple-app-site-association, served with Content-Type: application/json, no redirects:
{
"applinks": {
"details": [
{
"appIDs": ["TEAMID.com.yourcompany.yourapp"],
"components": [{ "/": "/*" }]
}
]
}
}
Android - App Links
In AndroidManifest.xml, add an intent filter to your launcher activity:
<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="links.yourapp.com" />
</intent-filter>
And serve a matching assetlinks.json at https://links.yourapp.com/.well-known/assetlinks.json:
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.yourcompany.yourapp",
"sha256_cert_fingerprints": ["YOUR:APP:SIGNING:CERT:FINGERPRINT"]
}
}]
Both files are exactly what a link validator checks first - get these right before writing any app code, or nothing downstream will work no matter how correct your JS is.
3. Initialize the SDK
Do this once, as early as possible - before your navigation container mounts, so a deferred link isn't dropped by a race with the first render.
// App.tsx
import { useEffect, useState } from 'react';
import { LinkTrail } from 'linktrail-react-native';
import { NavigationContainer } from '@react-navigation/native';
LinkTrail.configure({
apiKey: 'YOUR_LINKTRAIL_API_KEY',
});
export default function App() {
const [initialRoute, setInitialRoute] = useState(null);
useEffect(() => {
// Direct link: app already installed, tapped while running or cold-started via the OS
const subscription = LinkTrail.addLinkListener((link) => {
routeFromLink(link);
});
// Deferred link: first open after install, resolved from the server-side match
LinkTrail.getFirstOpenLink().then((link) => {
if (link) routeFromLink(link);
});
return () => subscription.remove();
}, []);
function routeFromLink(link) {
// link.path, link.params, link.campaign are all populated
setInitialRoute({ path: link.path, params: link.params });
}
return <NavigationContainer>{ /* ... */ }</NavigationContainer>;
}
The split matters: addLinkListener only fires for a link tapped by a user who already has the app - it's the same event Linking.addEventListener('url', ...) gives you. getFirstOpenLink is the deferred case - it resolves once, on first launch after install, from a server-side match rather than anything the OS passed in, because on a cold install there's no OS event carrying that data at all.
4. Route on the resolved link
Whatever router you use, treat both cases the same way once you have a link object - the app shouldn't care whether it came from the OS or from the deferred resolver:
function routeFromLink(link) {
switch (link.path) {
case '/product':
navigation.navigate('ProductDetail', { id: link.params.id });
break;
case '/invite':
navigation.navigate('Onboarding', { referrer: link.params.ref });
break;
default:
navigation.navigate('Home');
}
}
5. Test both paths before you trust either
This is the step people skip, and it's the one that actually matters:
- Direct case - install the app, background it, tap a link. Confirm
addLinkListenerfires with the right path. - Deferred case - uninstall completely, tap the same link, get sent to the store, install, open cold. Confirm
getFirstOpenLinkresolves with the same path - not just that the app opens. - Run your static files through a validator, not just a browser tab - you want to know if
assetlinks.jsonis missing the right fingerprint or the AASA file is being blocked by bot protection, not just that a manual check looked fine once.
If step 2 doesn't resolve, check first whether the native config from step 2 above is actually correct on your domain - that's the failure mode that looks like an SDK bug but almost always isn't.
What's next
This covers getting context across the install gap. It doesn't cover attribution - which campaign, which creative, which referrer gets credit for the install - which is really a separate concern layered on top of the same link. That's worth its own post.
If you want to sanity-check your own assetlinks.json / apple-app-site-association setup before wiring any of this up, LinkTrail has a free validator that checks both files against the same criteria described above, rather than a plain pass/fail.
Comments
No comments yet. Start the discussion.