Back to Blog
React Native
13 min read
March 5, 2024

Universal Links in React Native: Complete iOS Setup Guide

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.

What Are Universal Links?

Definition

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.

Universal Links Benefits

  • ✓ No app chooser dialog
  • ✓ Works with HTTPS URLs
  • ✓ Fallback to website
  • ✓ Seamless user experience
  • ✓ Shareable standard URLs
  • ✓ Great for marketing

URL Schemes Comparison

Custom Schemes

myapp://product/123

Only if app installed

Universal Links

https://myapp.com/product/123

App or website

How Universal Links Work in React Native

The Universal Link Flow

1

User Clicks Link

User taps an HTTPS link from Safari, email, or another app

2

iOS Checks AASA File

iOS fetches apple-app-site-association from your domain

3

Domain Verification

iOS verifies domain is associated with your app via team ID and bundle ID

4

App Opens Directly

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.

Xcode Configuration & Entitlements

Step 1: Add Associated Domains Entitlement

In Xcode:

  1. 1. Open your project in Xcode
  2. 2. Select your app target
  3. 3. Go to "Signing & Capabilities" tab
  4. 4. Click "+ Capability"
  5. 5. Search for and add "Associated Domains"
  6. 6. Under "Domains", add your domain with "applinks:" prefix
text
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.

Step 2: Verify Entitlements.plist

Xcode automatically generates an Entitlements.plist file. Verify it contains:

xml
<?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>

Step 3: Update Build Settings

Required Settings:

  • Code Signing Identity: Apple Development certificate
  • Team ID: Must match your Apple Developer account
  • Bundle Identifier: Must be registered in Apple Developer
  • Provisioning Profile: Includes "Associated Domains" capability

Creating the AASA File

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.

AASA File Structure

json
{
  "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.ios

File Hosting Requirements

Location: /.well-known/

text
https://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.

HTTP Headers Required:

bash
# 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>

Validate Your AASA File

Use Our AASA Validator Tool

Check your AASA configuration

React Native Code Implementation

Set up React Navigation to handle incoming universal links from iOS.

typescript
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];
}

AppDelegate Configuration

In some cases, you may need to manually handle deep links in AppDelegate, especially if using custom RCTLinkingManager handling.

swift
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
  }
}

Testing on Real Devices

Method 1: Testing via Notes App

  1. 1. Build and deploy your app to a real device
  2. 2. Open Notes app on the device
  3. 3. Create a new note and paste your universal link
  4. 4. Long-press the link (don't tap yet)
  5. 5. Select "Open" or tap the link
  6. 6. App should open directly to the intended screen
https://myapp.com/product/123

Method 2: Testing via Safari

  1. 1. Open Safari on your test device
  2. 2. Navigate to your universal link URL
  3. 3. iOS should intercept and open your app
  4. 4. If it opens the website instead, check AASA file
  5. 5. Try force-closing the app and retesting

Wait 2-3 hours: iOS caches AASA files. New domains may not work immediately.

Method 3: Using Apple's Diagnostic Tool

bash
# 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

Common Issues & Fixes

Issue: Link Opens in Safari Instead of App

Possible Causes:

  • AASA file not properly hosted
  • Domain not in Associated Domains list
  • Incorrect App ID in AASA file
  • AASA file not cached yet

Solutions:

  • Verify AASA file with our validator tool
  • Check Content-Type header is application/json
  • Force remove app from device and reinstall
  • Wait 2-3 hours for iOS cache to clear
  • Add domain to Settings → Developer on device

Issue: App Crashes When Opening Deep Link

Possible Causes:

  • URL parameters not properly extracted
  • Navigation route doesn't exist
  • Missing error handling in navigation

Solutions:

  • Add error boundaries in navigation
  • Log incoming URLs: console.log(url)
  • Validate parameters before navigation
  • Use try-catch in getInitialURL handler

Issue: Associated Domains Capability Missing

Solutions:

  • In Xcode, go to Signing & Capabilities
  • Click + Capability and search "Associated"
  • Add your domain with applinks: prefix
  • Ensure provisioning profile includes capability
  • Clean build folder and rebuild

Need Help?

Use our AASA Validator to check your configuration:

Validate Your AASA File

Deferred Deep Links Integration

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 Links

Universal Links are Worth It!

Setting 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.

Related Articles