Learn how to implement Universal Links in React Native with comprehensive Xcode configuration, AASA file setup, and best practices for iOS deep linking. Master associated domains, entitlements, and real device testing.
Universal Links are HTTP or HTTPS URLs that seamlessly open your iOS app if it's installed, or open the website if it's not. They're Apple's standard way of handling deep links on iOS, providing a smooth user experience without the app chooser dialog.
Custom Schemes
myapp://product/123Only if app installed
Universal Links
https://myapp.com/product/123App or website
User taps an HTTPS link from Safari, email, or another app
iOS fetches apple-app-site-association from your domain
iOS verifies domain is associated with your app via team ID and bundle ID
If verified, app opens and receives the URL. Otherwise, Safari opens the URL
Key Point: iOS caches the AASA file, so changes may take time to propagate on devices. For new domains, it can take up to 2-3 hours to fully cache.
applinks:myapp.com applinks:www.myapp.com applinks:api.myapp.com # For subdomains applinks:*.myapp.com
Note: Include all domain variants you might use. iOS is strict about domain matching.
Xcode automatically generates an Entitlements.plist file. Verify it contains:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp.com</string>
<string>applinks:www.myapp.com</string>
</array>
</dict>
</plist>The apple-app-site-association (AASA) file is a JSON file hosted on your domain that tells iOS which paths your app should handle. This is the critical piece that enables Universal Links.
{
"applinks": {
"apps": [],
"details": [
{
"appID": "ABC123DEF456.com.myapp.ios",
"paths": [
"/product/*",
"/user/:userId",
"/search/*",
"NOT /admin/*"
]
},
{
"appID": "ABC123DEF456.com.myapp.beta",
"paths": [
"/beta/*"
]
}
]
}
}Find Your App ID: Your appID is Team ID (from Apple Developer) + Bundle Identifier
Example: ABC123DEF456.com.myapp.ioshttps://myapp.com/.well-known/apple-app-site-association https://www.myapp.com/.well-known/apple-app-site-association
The file must be served from all domain variants you add to Associated Domains.
# Nginx example
location /.well-known/apple-app-site-association {
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
# Apache example
<Files "apple-app-site-association">
Header set Content-Type "application/json"
Header set Access-Control-Allow-Origin "*"
</Files>Use Our AASA Validator Tool
Check your AASA configurationSet up React Navigation to handle incoming universal links from iOS.
import { NavigationContainer, useNavigation } from '@react-navigation/native';
import { Linking } from 'react-native';
import { useEffect } from 'react';
const linking = {
prefixes: ['https://myapp.com', 'https://www.myapp.com'],
config: {
screens: {
Home: '',
Product: 'product/:id',
UserProfile: 'user/:userId',
NotFound: '*',
},
},
async getInitialURL() {
// Handle notification-opened while app was killed
const url = await Linking.getInitialURL();
if (url != null) {
return url;
}
},
};
export function RootNavigator() {
const navigationRef = useRef(null);
useEffect(() => {
const listening = Linking.addEventListener('url', ({ url }) => {
navigationRef.current?.resetRoot({
index: 0,
routes: [
{
name: 'Home',
state: {
routes: [
{
name: 'Product',
params: { id: extractProductId(url) },
},
],
},
},
],
});
});
return () => listening.remove();
}, []);
return (
<NavigationContainer ref={navigationRef} linking={linking}>
{/* Your screens */}
</NavigationContainer>
);
}
function extractProductId(url: string): string {
return url.split('/product/')[1];
}In some cases, you may need to manually handle deep links in AppDelegate, especially if using custom RCTLinkingManager handling.
import UIKit
import React
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let rootViewController = RCTRootViewController()
rootViewController.bundle = Bundle(url: jsCodeLocation)
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = rootViewController
self.window?.makeKeyAndVisible()
return true
}
// Handle universal links
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
// React Navigation will handle the routing
RCTLinkingManager.application(application, open: url, sourceApplication: nil, annotation: [:])
return true
}
return false
}
}https://myapp.com/product/123Wait 2-3 hours: iOS caches AASA files. New domains may not work immediately.
# On your Mac, check the device logs log stream --predicate 'eventMessage contains[c] "universal link" or eventMessage contains[c] "applinks"' --level debug # Or check via Xcode Console while running # Build & Run your app, then view Console output for linking events
Possible Causes:
Solutions:
Possible Causes:
Solutions:
Solutions:
Combine with Deferred Deep Links
Universal Links are great for installed apps, but for users who don't have your app yet, deferred deep links ensure they get the intended experience after installation.
Learn About Deferred Deep LinksSetting up Universal Links takes effort, but it significantly improves the user experience by providing seamless navigation from web to app. Combined with proper deferred deep linking, you have a complete solution for all user scenarios.