{"id":899,"date":"2026-04-24T17:00:00","date_gmt":"2026-04-24T22:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=899"},"modified":"2026-03-07T03:48:33","modified_gmt":"2026-03-07T08:48:33","slug":"react-native-universal-links","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/react-native-universal-links\/","title":{"rendered":"Universal Links in React Native: Complete Guide"},"content":{"rendered":"\n<p>Universal Links are Apple&#39;s mechanism for opening your app when a user taps an HTTPS link to your domain. Unlike custom URL schemes, Universal Links are secure (only you can claim your domain), reliable (they fall back to the web if the app isn&#39;t installed), and don&#39;t show a disambiguation dialog.<\/p>\n\n\n\n<p>This guide focuses on the iOS-specific setup for Universal Links in React Native. For the full cross-platform setup (including Android App Links), see <a href=\"https:\/\/tolinku.com\/blog\/react-native-deep-linking-setup\/\">React Native Deep Linking: Complete Setup Tutorial<\/a>. For Universal Links fundamentals, see <a href=\"https:\/\/tolinku.com\/blog\/universal-links-everything-you-need-to-know\/\">Universal Links: Everything You Need to Know<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How Universal Links Work<\/h2>\n\n\n\n<p>The flow when a user taps a Universal Link:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>User taps an HTTPS link<\/strong> (in Safari, Mail, Messages, or another app)<\/li>\n<li><strong>iOS checks its local cache<\/strong> to see if any installed app has claimed this domain<\/li>\n<li><strong>If the app is installed and claims the domain<\/strong>, iOS opens the app and passes the URL<\/li>\n<li><strong>If the app is not installed<\/strong>, iOS opens the URL in Safari (normal web behavior)<\/li>\n<\/ol>\n\n\n\n<p>The &quot;claim&quot; happens through two pieces:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Your app declares which domains it handles (Associated Domains entitlement)<\/li>\n<li>Your domain declares which apps can handle it (AASA file)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Step 1: Apple App Site Association (AASA) File<\/h2>\n\n\n\n<p>Your web server (or deep linking platform) must serve a JSON file at:<\/p>\n\n\n\n<pre><code>https:\/\/go.yourapp.com\/.well-known\/apple-app-site-association\n<\/code><\/pre>\n\n\n\n<p>The file content:<\/p>\n\n\n\n<pre><code class=\"language-json\">{\n  &quot;applinks&quot;: {\n    &quot;apps&quot;: [],\n    &quot;details&quot;: [\n      {\n        &quot;appIDs&quot;: [&quot;TEAMID.com.yourapp.bundleid&quot;],\n        &quot;components&quot;: [\n          { &quot;\/&quot;: &quot;\/product\/*&quot; },\n          { &quot;\/&quot;: &quot;\/invite\/*&quot; },\n          { &quot;\/&quot;: &quot;\/promo\/*&quot; }\n        ]\n      }\n    ]\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>If using Tolinku, the AASA file is served automatically when you configure your Appspace with your Team ID and Bundle ID.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AASA Requirements<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Must be served over HTTPS (no HTTP, no redirects)<\/li>\n<li>Must have <code>Content-Type: application\/json<\/code><\/li>\n<li>Must be accessible without authentication<\/li>\n<li>Must not exceed 128 KB<\/li>\n<li>Must not be behind a redirect (Apple&#39;s crawler won&#39;t follow redirects)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Verify the AASA<\/h3>\n\n\n\n<pre><code class=\"language-bash\"># Check accessibility\ncurl -I https:\/\/go.yourapp.com\/.well-known\/apple-app-site-association\n\n# Check content\ncurl https:\/\/go.yourapp.com\/.well-known\/apple-app-site-association | python3 -m json.tool\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Apple&#39;s CDN Cache<\/h3>\n\n\n\n<p>Apple caches AASA files through their CDN. When you first deploy or update your AASA, it can take up to 24-48 hours for Apple to re-fetch it. Check what Apple currently has cached:<\/p>\n\n\n\n<pre><code class=\"language-bash\">curl &quot;https:\/\/app-site-association.cdn-apple.com\/a\/v1\/go.yourapp.com&quot;\n<\/code><\/pre>\n\n\n\n<p>To request a re-crawl, use Apple&#39;s <a href=\"https:\/\/search.developer.apple.com\/appsearch-validation-tool\/\" rel=\"nofollow noopener\" target=\"_blank\">Search Validation tool<\/a> and enter your domain.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 2: Associated Domains Entitlement<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">In Xcode<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open <code>ios\/YourApp.xcworkspace<\/code> in Xcode<\/li>\n<li>Select your app target<\/li>\n<li>Go to &quot;Signing &amp; Capabilities&quot;<\/li>\n<li>Click &quot;+ Capability&quot;<\/li>\n<li>Add &quot;Associated Domains&quot;<\/li>\n<li>Add: <code>applinks:go.yourapp.com<\/code><\/li>\n<\/ol>\n\n\n\n<p>This generates (or updates) the <code>YourApp.entitlements<\/code> file:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;\n&lt;!DOCTYPE plist PUBLIC &quot;-\/\/Apple\/\/DTD PLIST 1.0\/\/EN&quot; &quot;http:\/\/www.apple.com\/DTDs\/PropertyList-1.0.dtd&quot;&gt;\n&lt;plist version=&quot;1.0&quot;&gt;\n&lt;dict&gt;\n  &lt;key&gt;com.apple.developer.associated-domains&lt;\/key&gt;\n  &lt;array&gt;\n    &lt;string&gt;applinks:go.yourapp.com&lt;\/string&gt;\n  &lt;\/array&gt;\n&lt;\/dict&gt;\n&lt;\/plist&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Multiple Domains<\/h3>\n\n\n\n<p>If your links come from multiple domains:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;array&gt;\n  &lt;string&gt;applinks:go.yourapp.com&lt;\/string&gt;\n  &lt;string&gt;applinks:links.yourapp.com&lt;\/string&gt;\n&lt;\/array&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Developer Mode (iOS 14+)<\/h3>\n\n\n\n<p>For development and testing, you can use the <code>?mode=developer<\/code> suffix:<\/p>\n\n\n\n<pre><code class=\"language-xml\">&lt;string&gt;applinks:go.yourapp.com?mode=developer&lt;\/string&gt;\n<\/code><\/pre>\n\n\n\n<p>This tells iOS to fetch the AASA file directly from your server instead of Apple&#39;s CDN, which is useful during development when you&#39;re frequently updating the file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step 3: AppDelegate Configuration<\/h2>\n\n\n\n<p>React Native&#39;s <code>RCTLinkingManager<\/code> bridges the native Universal Link callback to JavaScript.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">For Objective-C AppDelegate (AppDelegate.mm)<\/h3>\n\n\n\n<pre><code class=\"language-objective-c\">#import &lt;React\/RCTLinkingManager.h&gt;\n\n@implementation AppDelegate\n\n\/\/ Handle Universal Links\n- (BOOL)application:(UIApplication *)application\n   continueUserActivity:(NSUserActivity *)userActivity\n     restorationHandler:(void (^)(NSArray&lt;id&lt;UIUserActivityRestoring&gt;&gt; * _Nullable))restorationHandler\n{\n  return [RCTLinkingManager application:application\n                   continueUserActivity:userActivity\n                     restorationHandler:restorationHandler];\n}\n\n\/\/ Handle custom URL schemes (optional, but recommended as fallback)\n- (BOOL)application:(UIApplication *)application\n   openURL:(NSURL *)url\n   options:(NSDictionary&lt;UIApplicationOpenURLOptionsKey,id&gt; *)options\n{\n  return [RCTLinkingManager application:application openURL:url options:options];\n}\n\n@end\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">For Swift AppDelegate (AppDelegate.swift)<\/h3>\n\n\n\n<pre><code class=\"language-swift\">import React\n\n@main\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n  func application(\n    _ application: UIApplication,\n    continue userActivity: NSUserActivity,\n    restorationHandler: @escaping ([UIUserActivityRestoring]?) -&gt; Void\n  ) -&gt; Bool {\n    return RCTLinkingManager.application(\n      application,\n      continue: userActivity,\n      restorationHandler: restorationHandler\n    )\n  }\n\n  func application(\n    _ app: UIApplication,\n    open url: URL,\n    options: [UIApplication.OpenURLOptionsKey: Any] = [:]\n  ) -&gt; Bool {\n    return RCTLinkingManager.application(app, open: url, options: options)\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">SceneDelegate (iOS 13+)<\/h3>\n\n\n\n<p>If your app uses <code>UISceneDelegate<\/code> (the default for newer React Native projects):<\/p>\n\n\n\n<pre><code class=\"language-objective-c\">\/\/ SceneDelegate.m\n#import &lt;React\/RCTLinkingManager.h&gt;\n\n- (void)scene:(UIScene *)scene\n   continueUserActivity:(NSUserActivity *)userActivity {\n  [RCTLinkingManager application:[UIApplication sharedApplication]\n            continueUserActivity:userActivity\n              restorationHandler:^(NSArray&lt;id&lt;UIUserActivityRestoring&gt;&gt; * _Nullable restorableObjects) {\n  }];\n}\n\n- (void)scene:(UIScene *)scene\n   openURLContexts:(NSSet&lt;UIOpenURLContext *&gt; *)URLContexts {\n  for (UIOpenURLContext *context in URLContexts) {\n    [RCTLinkingManager application:[UIApplication sharedApplication]\n                           openURL:context.URL\n                           options:@{}];\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Step 4: JavaScript Link Handling<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Receiving Universal Links<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">import { Linking } from &#39;react-native&#39;;\n\n\/\/ Cold start: app was launched by the link\nuseEffect(() =&gt; {\n  Linking.getInitialURL().then((url) =&gt; {\n    if (url) {\n      handleUniversalLink(url);\n    }\n  });\n}, []);\n\n\/\/ Warm start: app was already running\nuseEffect(() =&gt; {\n  const subscription = Linking.addEventListener(&#39;url&#39;, ({ url }) =&gt; {\n    handleUniversalLink(url);\n  });\n  return () =&gt; subscription.remove();\n}, []);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Parsing Universal Link URLs<\/h3>\n\n\n\n<p>Universal Links arrive as full HTTPS URLs:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function handleUniversalLink(url) {\n  const parsed = new URL(url);\n\n  \/\/ Verify it&#39;s your domain\n  if (parsed.hostname !== &#39;go.yourapp.com&#39;) {\n    return;\n  }\n\n  const path = parsed.pathname;\n  const params = Object.fromEntries(parsed.searchParams);\n\n  \/\/ Route to the correct screen\n  if (path.startsWith(&#39;\/product\/&#39;)) {\n    const id = path.split(&#39;\/&#39;)[2];\n    navigation.navigate(&#39;Product&#39;, { id });\n  } else if (path.startsWith(&#39;\/invite\/&#39;)) {\n    const code = path.split(&#39;\/&#39;)[2];\n    navigation.navigate(&#39;Invite&#39;, { code });\n  } else {\n    \/\/ Unknown path: navigate to home or show not-found\n    navigation.navigate(&#39;Home&#39;);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Universal Links Don&#39;t Open the App<\/h3>\n\n\n\n<p><strong>Check the AASA file:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Is it accessible at <code>https:\/\/yourdomain\/.well-known\/apple-app-site-association<\/code>?<\/li>\n<li>Does it return <code>Content-Type: application\/json<\/code>?<\/li>\n<li>Does the <code>appID<\/code> match your Team ID + Bundle ID exactly?<\/li>\n<li>Is the path pattern in <code>components<\/code> (or <code>paths<\/code> for the older format) correct?<\/li>\n<\/ul>\n\n\n\n<p><strong>Check the entitlement:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Does the Associated Domains entry exactly match your domain? (no <code>https:\/\/<\/code>, no trailing slash)<\/li>\n<li>Is the entitlement present in the correct build target (not just the test target)?<\/li>\n<\/ul>\n\n\n\n<p><strong>Check the device:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Universal Links don&#39;t work in the iOS Simulator for all scenarios. Test on a physical device.<\/li>\n<li>Delete and reinstall the app. iOS fetches the AASA on install and caches it.<\/li>\n<li>Check if another app on the device is claiming the same domain.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Universal Links Open Safari Instead<\/h3>\n\n\n\n<p>This happens when:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The user <strong>typed<\/strong> the URL into Safari&#39;s address bar (Universal Links don&#39;t trigger from the address bar, by design)<\/li>\n<li>The user previously tapped the breadcrumb in Safari to &quot;Open in Safari&quot; for your domain, which disables Universal Links for that domain until they long-press a link and choose &quot;Open in [App]&quot;<\/li>\n<li>The AASA is cached with an old or invalid configuration<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Universal Links Work in Some Apps but Not Others<\/h3>\n\n\n\n<p>Different apps handle Universal Links differently:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Messages, Notes, Mail<\/strong>: Fully support Universal Links (tapping a link opens the app)<\/li>\n<li><strong>Safari<\/strong>: Only works when navigating from another page, not from the address bar<\/li>\n<li><strong>Some third-party apps<\/strong>: May use <code>SFSafariViewController<\/code> or <code>WKWebView<\/code>, which have different Universal Link behavior<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Cold Start Links Not Received<\/h3>\n\n\n\n<p>If links work when the app is backgrounded but not when launching the app fresh:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Verify that <code>continueUserActivity<\/code> (or the SceneDelegate equivalent) is called before <code>RCTBridge<\/code> finishes initialization<\/li>\n<li>Verify <code>Linking.getInitialURL()<\/code> is called early in your app&#39;s JavaScript entry point<\/li>\n<li>Check that the method is implemented in the correct delegate (AppDelegate vs SceneDelegate)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Testing Checklist<\/h2>\n\n\n\n<ul class=\"checklist wp-block-list\"><li><input type=\"checkbox\" disabled> AASA file accessible at <code>\/.well-known\/apple-app-site-association<\/code><\/li><li><input type=\"checkbox\" disabled> AASA returns <code>Content-Type: application\/json<\/code><\/li><li><input type=\"checkbox\" disabled> <code>appIDs<\/code> matches Team ID + Bundle ID<\/li><li><input type=\"checkbox\" disabled> Associated Domains entitlement includes <code>applinks:yourdomain<\/code><\/li><li><input type=\"checkbox\" disabled> <code>continueUserActivity<\/code> implemented in AppDelegate\/SceneDelegate<\/li><li><input type=\"checkbox\" disabled> Cold start links received via <code>Linking.getInitialURL()<\/code><\/li><li><input type=\"checkbox\" disabled> Warm start links received via <code>Linking.addEventListener<\/code><\/li><li><input type=\"checkbox\" disabled> Tested from Notes, Messages, and Mail on physical device<\/li><li><input type=\"checkbox\" disabled> App deleted and reinstalled after AASA changes<\/li><\/ul>\n\n\n\n<p>For Android App Links (the Android equivalent), see the Android section in <a href=\"https:\/\/tolinku.com\/blog\/react-native-deep-linking-setup\/\">React Native Deep Linking<\/a>. For the Tolinku SDK, see the <a href=\"https:\/\/tolinku.com\/docs\/developer\/sdks\/react-native\/\">React Native SDK docs<\/a>. For deep linking features, see <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku deep linking<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Configure Universal Links for React Native apps. Handle iOS-specific setup, AppDelegate configuration, and link handling in JavaScript.<\/p>\n","protected":false},"author":2,"featured_media":898,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Universal Links in React Native: Complete Guide","rank_math_description":"Configure Universal Links for React Native apps. Handle iOS-specific setup, AppDelegate configuration, and link handling in JavaScript.","rank_math_focus_keyword":"React Native universal links","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-react-native-universal-links.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-react-native-universal-links.png","footnotes":""},"categories":[15],"tags":[76,122,20,24,71,69,56,22],"class_list":["post-899","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-aasa","tag-apple","tag-deep-linking","tag-ios","tag-javascript","tag-mobile-development","tag-react-native","tag-universal-links"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/899","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=899"}],"version-history":[{"count":1,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/899\/revisions"}],"predecessor-version":[{"id":900,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/899\/revisions\/900"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/898"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=899"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=899"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=899"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}