<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Marko Boras</title>
    <description>The latest articles on Forem by Marko Boras (@marko_boras_64fe51f7833a6).</description>
    <link>https://forem.com/marko_boras_64fe51f7833a6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1602552%2Fc1530472-1cde-463a-8713-e8c844ce25f9.jpg</url>
      <title>Forem: Marko Boras</title>
      <link>https://forem.com/marko_boras_64fe51f7833a6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/marko_boras_64fe51f7833a6"/>
    <language>en</language>
    <item>
      <title>Universal &amp; Deep Links: 2026 Complete Guide</title>
      <dc:creator>Marko Boras</dc:creator>
      <pubDate>Fri, 05 Dec 2025 16:53:21 +0000</pubDate>
      <link>https://forem.com/marko_boras_64fe51f7833a6/universal-deep-links-2026-complete-guide-36c4</link>
      <guid>https://forem.com/marko_boras_64fe51f7833a6/universal-deep-links-2026-complete-guide-36c4</guid>
      <description>&lt;p&gt;Deep linking sounds simple on paper. A user taps a link, the app opens, and they land on the exact screen they need. In reality, anyone who has implemented &lt;strong&gt;Universal Links (iOS)&lt;/strong&gt; or &lt;strong&gt;App Links (Android)&lt;/strong&gt; knows it rarely works that smoothly.&lt;/p&gt;

&lt;p&gt;Sometimes the app opens. Sometimes the browser opens. Sometimes nothing happens at all. Email providers break links with tracking wrappers, hosting platforms silently rewrite files, and mobile OSes often decide a domain isn’t “trusted” without telling you why.&lt;/p&gt;

&lt;p&gt;If you’ve ever spent hours trying to understand why a link works on Android but not on iOS, or why Gmail refuses to trigger your app, you’re in the right place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here's the plan
&lt;/h2&gt;

&lt;p&gt;Universal Links and App Links only work when four layers line up correctly. In this guide, I’ll walk you through each one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your domain&lt;/strong&gt;&lt;br&gt;
How to set up AASA and assetlinks.json so iOS and Android trust your URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your mobile apps&lt;/strong&gt;&lt;br&gt;
What needs to be in your iOS entitlements, Android manifest, and native URL handlers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your hosting or framework&lt;/strong&gt;&lt;br&gt;
How platforms like Next.js, Firebase, or Vercel can break Universal Links if they rewrite or redirect files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your client-side logic&lt;/strong&gt;&lt;br&gt;
How to map URL paths and query params inside React Native so the right screens open.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you’ll understand how these pieces fit together, why Universal Links often fail silently, and how to debug them quickly when they do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep linking basics
&lt;/h2&gt;

&lt;p&gt;Before we get into implementation, it’s important to understand the two types of deep links you’ll deal with: custom URL schemes and Universal/App Links. Both open specific screens inside your app, but they behave very differently and serve different purposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom URL schemes
&lt;/h2&gt;

&lt;p&gt;Custom schemes look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;myapp://reset-password?token=123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They’re simple and useful for internal navigation, but they come with serious limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They only work if the app is installed&lt;/li&gt;
&lt;li&gt;There’s no fallback (the link does nothing otherwise)&lt;/li&gt;
&lt;li&gt;Any other app can register the same scheme&lt;/li&gt;
&lt;li&gt;Some apps and email clients block them completely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Custom schemes are still good for certain in-app flows, but they’re not reliable for anything being sent from outside the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20edk6iq0iee35z6uu86.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F20edk6iq0iee35z6uu86.webp" alt=" " width="800" height="820"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Universal Links (iOS) and App Links (Android)
&lt;/h2&gt;

&lt;p&gt;This is the modern approach. Both platforms moved to secure deep links based on &lt;strong&gt;HTTPS + domain verification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Universal/App Link looks like a regular link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/reset-password?token=123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the app is installed, it opens the correct screen.&lt;br&gt;
If it’s not installed, the link falls back to the website.&lt;/p&gt;

&lt;p&gt;Because the system verifies that your app “owns” this domain, these links are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More secure&lt;/li&gt;
&lt;li&gt;More predictable&lt;/li&gt;
&lt;li&gt;Supported across email, SMS, browsers, and most apps&lt;/li&gt;
&lt;li&gt;Designed to avoid hijacking or the wrong app opening your link&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;p&gt;Here's the simplest way to think about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Universal/App Links&lt;/strong&gt; for anything coming from outside your app(email verification, invitations, password reset, shared links, notifications)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use custom URL schemes&lt;/strong&gt; for deep navigation within your app (especially when moving between webviews or hybrid screens)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How universal &amp;amp; app links work
&lt;/h2&gt;

&lt;p&gt;To make Universal Links (iOS) and App Links (Android) behave reliably, it helps to understand what actually happens when a user taps one. The OS doesn’t simply “open your app.” It runs a sequence of checks before deciding whether your link is trustworthy.&lt;/p&gt;

&lt;p&gt;This is the part most developers never see, and the reason Universal Links often fail without explanation.&lt;/p&gt;
&lt;h2&gt;
  
  
  The OS decision flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpa1jrc4m38mo6uhj373b.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpa1jrc4m38mo6uhj373b.webp" alt=" " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Domain verification
&lt;/h2&gt;

&lt;p&gt;Universal/App Links rely entirely on domain verification.&lt;/p&gt;

&lt;p&gt;If the OS can’t fetch your verification files, or if they’re served incorrectly, Universal Links will never open your app, no matter how perfect your mobile code is.&lt;/p&gt;

&lt;p&gt;Common examples of what breaks verification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verification files behind &lt;strong&gt;redirects&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Wrong &lt;strong&gt;Content-Type&lt;/strong&gt; header&lt;/li&gt;
&lt;li&gt;File served as text/html &lt;strong&gt;instead of JSON&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hosting platform rewriting routes to index.html&lt;/li&gt;
&lt;li&gt;Wrong domain (e.g., &lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt; instead of example.com)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the OS fails verification, it often caches that failure. This is why uninstalling the app is sometimes required during testing.&lt;/p&gt;
&lt;h2&gt;
  
  
  App installed vs not installed
&lt;/h2&gt;

&lt;p&gt;After verification, the OS chooses between two outcomes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feejsr31yj5ioxtpxyjt8.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feejsr31yj5ioxtpxyjt8.webp" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one of the biggest advantages over custom URL schemes: the user always lands somewhere meaningful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real-world scenarios
&lt;/h2&gt;

&lt;p&gt;Universal/App Links are most commonly used for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email verification&lt;/li&gt;
&lt;li&gt;Password reset&lt;/li&gt;
&lt;li&gt;Invite flows&lt;/li&gt;
&lt;li&gt;Shared content&lt;/li&gt;
&lt;li&gt;Notifications that deep link into the app&lt;/li&gt;
&lt;li&gt;Webview → app handoff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These flows all rely on the same OS decision logic, which is why a single misconfigured domain can break all of them at once.&lt;/p&gt;
&lt;h2&gt;
  
  
  Web setup
&lt;/h2&gt;

&lt;p&gt;Universal Links and App Links only work if your domain is configured correctly. This is the most sensitive part of the entire setup, and also the part that breaks the most often.&lt;/p&gt;

&lt;p&gt;If iOS or Android can’t read your verification files with the exact format and headers they expect, the app will never open, even if everything else is perfect.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verification files
&lt;/h2&gt;

&lt;p&gt;Both platforms expect a file at a very specific path:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.well-known/apple-app-site-association
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Must be &lt;strong&gt;raw JSON&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Must &lt;strong&gt;not&lt;/strong&gt; have a .json extension&lt;/li&gt;
&lt;li&gt;Must use &lt;strong&gt;Content-Type: application/json&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Android&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.well-known/assetlinks.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Must be a &lt;strong&gt;valid JSON array&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Must contain the &lt;strong&gt;package name&lt;/strong&gt; and the &lt;strong&gt;SHA-256 certificate fingerprint&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Must be served as &lt;strong&gt;application/json&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These files tell the OS: &lt;em&gt;“This domain belongs to this app. It’s safe to open links directly.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the OS can’t load or parse these files, Universal/App Links will silently fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  File hosting requirements
&lt;/h2&gt;

&lt;p&gt;This is where most teams run into trouble. The OS expects all of the following to be true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HTTPS only&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No redirects&lt;/strong&gt; (even a single 301 or 302 breaks verification)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No authentication or cookies&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact path:&lt;/strong&gt; /.well-known/ must be literal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Correct Content-Type header&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status 200&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No index.html fallback&lt;/strong&gt; (common issue with frameworks like Next.js)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your hosting platform rewrites URLs or forces HTML responses, the OS won’t accept your domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server constraints &amp;amp; checklist
&lt;/h2&gt;

&lt;p&gt;Here’s a quick checklist to confirm your domain setup is correct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both files live in &lt;strong&gt;/.well-known/&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Served as &lt;strong&gt;application/json&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No redirects&lt;/li&gt;
&lt;li&gt;URL returns &lt;strong&gt;HTTP 200&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessible publicly&lt;/strong&gt; (no auth, no tokens)&lt;/li&gt;
&lt;li&gt;Content is valid &lt;strong&gt;JSON&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Filename for AASA has &lt;strong&gt;no extension&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hosting platform &lt;strong&gt;doesn’t override&lt;/strong&gt; routes or headers&lt;/li&gt;
&lt;li&gt;Uses &lt;strong&gt;HTTPS&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If even one of these is wrong, Universal Links won’t work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hosting examples
&lt;/h2&gt;

&lt;p&gt;Different hosting platforms behave differently. Here’s how they fit into the setup:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Place both files in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/public/.well-known/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Next.js may default to serving HTML, you usually need custom headers in &lt;strong&gt;next.config.js&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  source: '/.well-known/apple-app-site-association',
  headers: [{ key: 'Content-Type', value: 'application/json' }]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same for &lt;strong&gt;assetlinks.json.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firebase hosting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Firebase serves static files correctly, but only if configured explicitly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;firebase.json&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"headers": [
  {
    "source": "/.well-known/apple-app-site-association",
    "headers": [{ "key": "Content-Type", "value": "application/json" }]
  },
  {
    "source": "/.well-known/assetlinks.json",
    "headers": [{ "key": "Content-Type", "value": "application/json" }]
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common Firebase issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serving files as text/html&lt;/li&gt;
&lt;li&gt;Rewrites overriding .well-known paths&lt;/li&gt;
&lt;li&gt;Deploying the wrong public folder&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Vercel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vercel requires custom headers in &lt;strong&gt;next.config.js&lt;/strong&gt; or &lt;strong&gt;vercel.json&lt;/strong&gt;. Without them, iOS may download your AASA file as an attachment — a guaranteed failure.&lt;/p&gt;

&lt;p&gt;Common pitfalls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preview domains (*.vercel.app) don’t match your production domain&lt;/li&gt;
&lt;li&gt;Wrong header defaults&lt;/li&gt;
&lt;li&gt;Missing .well-known folder in the root of your public directory&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick wrap-up
&lt;/h2&gt;

&lt;p&gt;Your domain is the foundation of Universal/App Links. If the OS can’t read your verification files exactly the way it expects, the rest of your setup won’t matter.&lt;/p&gt;

&lt;p&gt;Getting this part right ensures every link you send, email, SMS, notifications, shared content behaves consistently.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS implementation
&lt;/h2&gt;

&lt;p&gt;Once your domain is configured, the next step is making sure iOS actually knows your app is allowed to open those links. This depends on three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your app being correctly configured in the Apple Developer portal&lt;/li&gt;
&lt;li&gt;Xcode having the right entitlements&lt;/li&gt;
&lt;li&gt;Your AppDelegate forwarding incoming URLs to React Native&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any one of these pieces is incorrect, Universal Links will fall back to Safari, even if everything else looks fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apple Developer Portal setup
&lt;/h2&gt;

&lt;p&gt;iOS will only trust your Universal Links if your app’s bundle ID explicitly declares support for Associated Domains.&lt;/p&gt;

&lt;p&gt;Here’s what you need to do:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8oq1pe3d7i9sxjcb5pg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff8oq1pe3d7i9sxjcb5pg.webp" alt=" " width="800" height="863"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This step often gets missed. If you enable Associated Domains but don’t update your profiles, the device will never see the entitlement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Associated Domains in Xcode
&lt;/h2&gt;

&lt;p&gt;Next, you need to tell iOS which domains your app should handle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uef493p3zv6u79fcz2o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uef493p3zv6u79fcz2o.webp" alt=" " width="800" height="867"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;applinks:example.com
applinks:staging.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the domain and subdomain must match exactly what’s in your AASA file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt;&lt;br&gt;
After adding or modifying these domains, delete the app from your device and reinstall it. iOS only refreshes Universal Link permissions on install.&lt;/p&gt;
&lt;h2&gt;
  
  
  Handling universal links in the AppDelegate
&lt;/h2&gt;

&lt;p&gt;When iOS decides your app should open a Universal Link, it hands the URL to your &lt;strong&gt;AppDelegate&lt;/strong&gt;. If you use React Native, you need to forward it to &lt;strong&gt;RCTLinkingManager&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Add this to your &lt;strong&gt;AppDelegate&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#import &amp;lt;React/RCTLinkingManager.h&amp;gt;

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
            options:(NSDictionary&amp;lt;UIApplicationOpenURLOptionsKey,id&amp;gt; *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
 restorationHandler:(void (^)(NSArray *))restorationHandler
{
  return [RCTLinkingManager application:application
                    continueUserActivity:userActivity
                      restorationHandler:restorationHandler];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS sends Universal Links through &lt;strong&gt;continueUserActivity:&lt;/strong&gt; not &lt;strong&gt;openURL:&lt;/strong&gt; so both are required.&lt;/p&gt;

&lt;p&gt;Without this, the OS may open your app, but React Native won’t receive the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-environment support
&lt;/h2&gt;

&lt;p&gt;If you have different environments (prod, staging, dev), add each domain in Xcode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;applinks:example.com
applinks:staging.example.com
applinks:dev.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A matching AASA file&lt;/li&gt;
&lt;li&gt;HTTPS&lt;/li&gt;
&lt;li&gt;No redirects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;iOS treats each domain independently, so one broken environment won’t affect the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known iOS quirks
&lt;/h2&gt;

&lt;p&gt;Here are the behaviors that confuse most teams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Universal Links &lt;strong&gt;do NOT work in the Simulator&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Safari &lt;strong&gt;does not auto-open&lt;/strong&gt; apps from the address bar&lt;/li&gt;
&lt;li&gt;Safari shows an &lt;strong&gt;“Open in App” banner&lt;/strong&gt; instead — this is &lt;strong&gt;normal&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;iOS aggressively &lt;strong&gt;caches AASA files&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;If a domain verification fails once, &lt;strong&gt;reinstalling the app&lt;/strong&gt; is often required&lt;/li&gt;
&lt;li&gt;Testing from Gmail or Mail is &lt;strong&gt;mandatory&lt;/strong&gt;, many flows don’t trigger from Safari&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is expected iOS behavior, not a bug in your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it all comes down to
&lt;/h2&gt;

&lt;p&gt;On iOS, Universal Links depend entirely on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On iOS, Universal Links depend entirely on:&lt;/li&gt;
&lt;li&gt;A correct App ID configuration&lt;/li&gt;
&lt;li&gt;Valid provisioning profiles&lt;/li&gt;
&lt;li&gt;Correct Associated Domains in Xcode&lt;/li&gt;
&lt;li&gt;AASA served properly&lt;/li&gt;
&lt;li&gt;Native URL handling in AppDelegate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once these pieces are in place, iOS becomes very reliable, but getting them right requires precision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android implementation
&lt;/h2&gt;

&lt;p&gt;Android handles deep linking differently from iOS, but the idea is the same: the OS checks whether your domain is associated with your app, and if everything matches, it opens the app directly.&lt;/p&gt;

&lt;p&gt;The setup is straightforward on paper, but small mistakes in your manifest or assetlinks.json will cause Android to fall back to the browser every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Android needs to trust your link
&lt;/h2&gt;

&lt;p&gt;Android requires three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A valid &lt;strong&gt;assetlinks.json&lt;/strong&gt; file on your domain&lt;/li&gt;
&lt;li&gt;An intent filter in your &lt;strong&gt;AndroidManifest.xml&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A matching &lt;strong&gt;SHA-256 fingerprint&lt;/strong&gt; from your signing key&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of these don’t match exactly, Android won’t verify your domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add App Links intent filter in AndroidManifest
&lt;/h2&gt;

&lt;p&gt;Inside the &lt;strong&gt;&lt;/strong&gt; that should handle the link (usually &lt;strong&gt;MainActivity&lt;/strong&gt;), add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;intent-filter android:autoVerify="true"&amp;gt;
    &amp;lt;action android:name="android.intent.action.VIEW" /&amp;gt;
    &amp;lt;category android:name="android.intent.category.DEFAULT" /&amp;gt;
    &amp;lt;category android:name="android.intent.category.BROWSABLE" /&amp;gt;

    &amp;lt;data
        android:scheme="https"
        android:host="example.com"
        android:pathPrefix="/reset-password" /&amp;gt;
&amp;lt;/intent-filter&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few important notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;android:autoVerify="true"&lt;/strong&gt; tells Android to automatically check your assetlinks.json file when the app is installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;android:host&lt;/strong&gt; must match your domain exactly — subdomains included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;android:pathPrefix&lt;/strong&gt; defines which URLs should open the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need multiple routes, repeat the &lt;strong&gt;&lt;/strong&gt; block or add multiple prefixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-environment support
&lt;/h2&gt;

&lt;p&gt;If you have separate &lt;strong&gt;staging / dev / production&lt;/strong&gt; domains, add a &lt;strong&gt;&lt;/strong&gt; block for each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;data android:scheme="https" android:host="example.com" android:pathPrefix="/" /&amp;gt;
&amp;lt;data android:scheme="https" android:host="staging.example.com" android:pathPrefix="/" /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each domain must have its own matching &lt;strong&gt;assetlinks.json&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assetlinks.json requirements
&lt;/h2&gt;

&lt;p&gt;Your assetlinks file must look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": [
        "YOUR_SHA256_FINGERPRINT"
      ]
    }
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common mistakes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using the wrong fingerprint (debug vs release)&lt;/li&gt;
&lt;li&gt;Serving the file with redirects&lt;/li&gt;
&lt;li&gt;Incorrect Content-Type&lt;/li&gt;
&lt;li&gt;Missing JSON array wrapper&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Android will only open your app if the OS can verify:&lt;br&gt;
&lt;strong&gt;domain ↔ package name ↔ certificate fingerprint&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading the link inside MainActivity
&lt;/h2&gt;

&lt;p&gt;React Native will not receive a link unless you forward it from your Activity.&lt;br&gt;
In &lt;strong&gt;MainActivity.java&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Intent intent = getIntent();
    Uri data = intent.getData();
    // React Native picks this up through Linking
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React Native uses &lt;strong&gt;Linking.getInitialURL()&lt;/strong&gt; for cold starts and event listeners for runtime links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking Android’s domain verification
&lt;/h2&gt;

&lt;p&gt;You can check whether Android trusts your domain with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adb shell pm get-app-links com.example.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything is correct, you’ll see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VERIFIED: example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;strong&gt;UNVERIFIED&lt;/strong&gt;, it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;assetlinks.json is wrong&lt;/li&gt;
&lt;li&gt;Your fingerprint doesn’t match&lt;/li&gt;
&lt;li&gt;The OS couldn’t fetch your file&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Known Android quirks
&lt;/h2&gt;

&lt;p&gt;Android’s behavior varies across manufacturers and OS versions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain verification can take a few minutes after install&lt;/li&gt;
&lt;li&gt;Some devices show an app picker even when verification succeeds&lt;/li&gt;
&lt;li&gt;Using a debug build with a release fingerprint will never work&lt;/li&gt;
&lt;li&gt;Links from some apps (e.g., Slack) may show a dialog the first time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t bugs, they’re normal Android inconsistencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  To make Android App Links work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Configure a correct intent filter&lt;/li&gt;
&lt;li&gt;Deploy a valid assetlinks.json&lt;/li&gt;
&lt;li&gt;Use the correct SHA-256 fingerprint&lt;/li&gt;
&lt;li&gt;Confirm domain verification with ADB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once verified, Android reliably opens the app for every matching URL, unless something in the chain breaks, which we’ll cover in the debugging section.&lt;/p&gt;

&lt;h2&gt;
  
  
  React native integration
&lt;/h2&gt;

&lt;p&gt;Once iOS and Android know your app is allowed to open Universal/App Links, the final step is making sure React Native can actually use the incoming URL.&lt;/p&gt;

&lt;p&gt;This is where you define which screen should open, how URL paths map to routes, and how query parameters reach your components.&lt;/p&gt;

&lt;p&gt;React Native won’t handle any of this automatically; you need to configure it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How React Native receives URLs
&lt;/h2&gt;

&lt;p&gt;React Native gives you two entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cold start:&lt;br&gt;
Linking.getInitialURL() returns the URL that opened the app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;While the app is running:&lt;br&gt;
Linking.addEventListener('url', handler) fires whenever a link is tapped.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both rely on native code forwarding the URL, which we set up earlier in AppDelegate (iOS) and MainActivity (Android).&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a linking configuration
&lt;/h2&gt;

&lt;p&gt;If you’re using React Navigation (most apps do), you can define how URLs map to screens through the &lt;strong&gt;linking config&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const linking = {
  prefixes: [
    'https://example.com',
    'https://staging.example.com',
    'myapp://', // optional custom scheme
  ],
  config: {
    screens: {
      Root: {
        screens: {
          ResetPassword: {
            path: 'reset-password',
          },
          Invite: {
            path: 'invite',
          },
        },
      },
    },
  },
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells React Navigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any link starting with &lt;a href="https://example.com" rel="noopener noreferrer"&gt;https://example.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;With a path like /reset-password?token=123&lt;/li&gt;
&lt;li&gt;Should open the ResetPassword screen&lt;/li&gt;
&lt;li&gt;And pass token into route.params&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React Navigation handles the parsing automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping URL paths to screens
&lt;/h2&gt;

&lt;p&gt;A few conventions help keep things clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use lowercase, dash-separated paths&lt;/li&gt;
&lt;li&gt;Define one path per screen&lt;/li&gt;
&lt;li&gt;Explicitly map nested navigators to avoid ambiguity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/email-verification?oobCode=123
→ EmailVerification screen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures both platforms behave the same for the same URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting query params inside your screen
&lt;/h2&gt;

&lt;p&gt;React Navigation parses query parameters by default.&lt;/p&gt;

&lt;p&gt;In your screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const route = useRoute();
const { token } = route.params || {};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don’t need to manually parse the URL - React Navigation does it for you as long as the linking config is correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach linking to NavigationContainer
&lt;/h2&gt;

&lt;p&gt;Finally, plug everything into your navigation root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NavigationContainer } from '@react-navigation/native';

export default function App() {
  return (
    &amp;lt;NavigationContainer linking={linking}&amp;gt;
      &amp;lt;RootNavigator /&amp;gt;
    &amp;lt;/NavigationContainer&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your app knows how to resolve URLs like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/reset-password?token=abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and open the right screen on both iOS and Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full flow: from tap → screen
&lt;/h2&gt;

&lt;p&gt;Here’s the complete lifecycle when a user taps a Universal/App Link:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User taps &lt;a href="https://example.com/reset-password?token=123" rel="noopener noreferrer"&gt;https://example.com/reset-password?token=123&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;iOS/Android check domain verification&lt;/li&gt;
&lt;li&gt;OS decides to open your app&lt;/li&gt;
&lt;li&gt;Native layer receives the URL&lt;/li&gt;
&lt;li&gt;iOS → continueUserActivity&lt;/li&gt;
&lt;li&gt;Android → intent.getData()&lt;/li&gt;
&lt;li&gt;Native layer forwards the URL to React Native&lt;/li&gt;
&lt;li&gt;NavigationContainer reads the URL&lt;/li&gt;
&lt;li&gt;React Navigation finds a matching path&lt;/li&gt;
&lt;li&gt;Query params (e.g. token) become route.params&lt;/li&gt;
&lt;li&gt;The correct screen opens instantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When every layer is set up correctly, this flow is extremely reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling it all together
&lt;/h2&gt;

&lt;p&gt;React Native’s job is simple once the native setup is done:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define your URL prefixes&lt;/li&gt;
&lt;li&gt;Map paths to screens&lt;/li&gt;
&lt;li&gt;Read params using useRoute()&lt;/li&gt;
&lt;li&gt;Attach your linking config to the NavigationContainer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps your deep linking logic predictable and consistent across iOS and Android, and ensures users land exactly where they need to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing &amp;amp; debugging
&lt;/h2&gt;

&lt;p&gt;Universal Links and App Links don’t break because of one obvious error, instead they break because several small pieces need to align perfectly across the web, iOS, Android, and React Native. Testing is tricky, and debugging usually requires checking each layer step by step.&lt;/p&gt;

&lt;p&gt;This section covers the most reliable ways to test and the most common reasons things fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  General testing principles
&lt;/h2&gt;

&lt;p&gt;Before testing anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always use &lt;strong&gt;real devices&lt;/strong&gt; (iOS Simulator does not support Universal Links).&lt;/li&gt;
&lt;li&gt;Test using a &lt;strong&gt;fresh install&lt;/strong&gt;, iOS caches AASA files aggressively.&lt;/li&gt;
&lt;li&gt;Test from actual apps like Gmail or Mail, not from Safari’s address bar.&lt;/li&gt;
&lt;li&gt;Make sure your link is a &lt;strong&gt;fully qualified HTTPS link&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Avoid any kind of redirect, tracking wrapper, or shortened URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you skip these basics, you’ll end up chasing issues that aren’t actually your setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Universal Links fail
&lt;/h2&gt;

&lt;p&gt;Universal Links and App Links usually fail for the same handful of reasons. Here’s a breakdown:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These account for most failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong Content-Type (file served as HTML instead of JSON)&lt;/li&gt;
&lt;li&gt;Files behind redirects (301/302)&lt;/li&gt;
&lt;li&gt;Wrong file path (/.wellknown/ instead of /.well-known/)&lt;/li&gt;
&lt;li&gt;Wrong domain (e.g., &lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt; instead of example.com)&lt;/li&gt;
&lt;li&gt;Hosting platform rewriting requests to index.html&lt;/li&gt;
&lt;li&gt;Assetlinks/AASA file not publicly accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your domain fails verification, nothing else will work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;App configuration issues&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong Team ID or Bundle ID in AASA&lt;/li&gt;
&lt;li&gt;Wrong SHA-256 fingerprint in assetlinks.json&lt;/li&gt;
&lt;li&gt;Missing Associated Domains capability (iOS)&lt;/li&gt;
&lt;li&gt;Missing or incorrect intent filter (Android)&lt;/li&gt;
&lt;li&gt;Native app not forwarding the URL to React Native&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These issues are easy to introduce and easy to miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OS behavior issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some behaviors are “by design,” even if they feel like bugs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;iOS Simulator does not support Universal Links&lt;/li&gt;
&lt;li&gt;Safari doesn’t auto-open apps; it shows a banner instead&lt;/li&gt;
&lt;li&gt;iOS caches AASA results for days&lt;/li&gt;
&lt;li&gt;Android may take a few minutes to verify domain ownership&lt;/li&gt;
&lt;li&gt;First tap on some Android apps may show a picker dialog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing on real devices and reinstalling the app often fixes these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email-related issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Email clients break Universal Links more than anything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SendGrid / HubSpot / Mailchimp add tracking wrappers&lt;/li&gt;
&lt;li&gt;Outlook SafeLink rewrites URLs through a Microsoft domain&lt;/li&gt;
&lt;li&gt;Gmail may encode or escape characters in your query params&lt;/li&gt;
&lt;li&gt;Shortened URLs (bit.ly, etc.) remove the original domain entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A wrapped link will &lt;strong&gt;never&lt;/strong&gt; open your app, because the OS only trusts your domain, not the redirect domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging flow (step-by-step)
&lt;/h2&gt;

&lt;p&gt;Here’s the most efficient way to debug Universal Links:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe071d0m0thhrtdfkiw1b.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe071d0m0thhrtdfkiw1b.webp" alt=" " width="800" height="960"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Check the verification files directly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open in Safari or Chrome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/.well-known/apple-app-site-association
https://example.com/.well-known/assetlinks.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see raw JSON.&lt;br&gt;
If the file downloads instead of displaying → wrong Content-Type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check iOS AASA fetch logs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this on your Mac:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log stream --predicate 'subsystem == "com.apple.nsurlsessiond"' --info | grep apple-app-site-association
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;statusCode: 200&lt;/li&gt;
&lt;li&gt;Content-Type: application/json&lt;/li&gt;
&lt;li&gt;No redirects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anything else means iOS rejected your association.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Check Android domain verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use ADB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adb shell pm get-app-links com.example.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VERIFIED: example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it says &lt;strong&gt;UNVERIFIED&lt;/strong&gt;, the fingerprint or assetlinks.json is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Test a direct app-open from ADB&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adb shell am start \
  -a android.intent.action.VIEW \
  -d "https://example.com/reset-password?token=abc" \
  com.example.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the app opens → the manifest is correct.&lt;br&gt;
If not → the manifest or assetlinks.json doesn’t match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Test from email apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Send yourself real emails from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gmail&lt;/li&gt;
&lt;li&gt;Apple Mail&lt;/li&gt;
&lt;li&gt;Outlook&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing from real email apps reveals issues you won’t see with Safari.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to check
&lt;/h2&gt;

&lt;p&gt;Testing Universal Links isn’t just “tap the link and see what happens.”&lt;br&gt;
It’s running a systematic check across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Web hosting&lt;/li&gt;
&lt;li&gt;Verification files&lt;/li&gt;
&lt;li&gt;iOS entitlements&lt;/li&gt;
&lt;li&gt;Android signature matching&lt;/li&gt;
&lt;li&gt;React Native linking&lt;/li&gt;
&lt;li&gt;Real-world email clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When all layers are aligned, Universal Links are fast and reliable. But the moment one piece drifts even slightly, the entire flow breaks, often without any error message. A structured debugging approach is the only way to get to the root cause.&lt;/p&gt;
&lt;h2&gt;
  
  
  Quick reference (cheat sheet)
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Web setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AASA (iOS)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File at: /.well-known/apple-app-site-association&lt;/li&gt;
&lt;li&gt;No .json extension&lt;/li&gt;
&lt;li&gt;Served as application/json&lt;/li&gt;
&lt;li&gt;No redirects&lt;/li&gt;
&lt;li&gt;Publicly accessible&lt;/li&gt;
&lt;li&gt;Valid JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appIDs": ["TEAMID.com.example.app"],
        "components": [
          { "/": "/reset-password", "?": { "token": "*" } }
        ]
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;assetlinks.json (Android)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File at: /.well-known/assetlinks.json&lt;/li&gt;
&lt;li&gt;Served as application/json&lt;/li&gt;
&lt;li&gt;Contains correct package name&lt;/li&gt;
&lt;li&gt;SHA-256 fingerprint matches signing key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.app",
      "sha256_cert_fingerprints": [
        "YOUR_SHA256_FINGERPRINT"
      ]
    }
  }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  iOS setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Associated Domains&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enabled in Apple Developer Portal&lt;/li&gt;
&lt;li&gt;Enabled in Xcode&lt;/li&gt;
&lt;li&gt;Domains match AASA exactly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;applinks:example.com
applinks:staging.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AppDelegate forwarding&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#import &amp;lt;React/RCTLinkingManager.h&amp;gt;

- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
 restorationHandler:(void (^)(NSArray *))restorationHandler
{
  return [RCTLinkingManager application:application
                    continueUserActivity:userActivity
                      restorationHandler:restorationHandler];
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;iOS debug commands&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log stream --predicate 'subsystem == "com.apple.nsurlsessiond"' --info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Android setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manifest&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Intent filter added&lt;/li&gt;
&lt;li&gt;Correct host + pathPrefix&lt;/li&gt;
&lt;li&gt;autoVerify enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;intent-filter android:autoVerify="true"&amp;gt;
  &amp;lt;action android:name="android.intent.action.VIEW" /&amp;gt;
  &amp;lt;category android:name="android.intent.category.DEFAULT" /&amp;gt;
  &amp;lt;category android:name="android.intent.category.BROWSABLE" /&amp;gt;
  &amp;lt;data android:scheme="https" android:host="example.com" android:pathPrefix="/" /&amp;gt;
&amp;lt;/intent-filter&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;ADB verification&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adb shell pm get-app-links com.example.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Direct link test&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adb shell am start -a android.intent.action.VIEW \
  -d "https://example.com/reset-password?token=abc" \
  com.example.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  React Native setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prefixes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prefixes: [
  'https://example.com',
  'https://staging.example.com',
  'myapp://'
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Screen mapping&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;config: {
  screens: {
    ResetPassword: 'reset-password',
    Invite: 'invite',
  },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Getting params&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const route = useRoute();
const { token } = route.params || {};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Email considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Disable tracking for Universal Links&lt;/li&gt;
&lt;li&gt;Avoid shorteners (bit.ly, TinyURL)&lt;/li&gt;
&lt;li&gt;Avoid redirects of any kind&lt;/li&gt;
&lt;li&gt;Test on real Gmail, Apple Mail, Outlook&lt;/li&gt;
&lt;li&gt;Use raw HTTPS URLs only&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SendGrid (disable tracking):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;a data-sg-no-track="true" clicktracking="off"&amp;gt;
  https://example.com/reset-password?token=abc
&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Validation tools
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://branch.io/resources/aasa-validator/" rel="noopener noreferrer"&gt;AASA&lt;/a&gt;&lt;br&gt;
&lt;a href="https://yurl.chayev.com/?ref=blog.prototyp.digital" rel="noopener noreferrer"&gt;Yurl&lt;/a&gt;&lt;br&gt;
&lt;a href="https://docs.median.co/docs/deep-linking-validator" rel="noopener noreferrer"&gt;Median&lt;/a&gt;&lt;br&gt;
&lt;a href="https://developers.google.com/digital-asset-links/tools/generator?ref=blog.prototyp.digital" rel="noopener noreferrer"&gt;Statement List Generator and Tester&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In short...
&lt;/h2&gt;

&lt;p&gt;Universal Links have burned me enough times that I almost expect them to fail on the first try. Not because the concept is complicated, but because the smallest detail (a redirect, a header, a fingerprint) can quietly break everything.&lt;/p&gt;

&lt;p&gt;Once you finally stitch the pieces together, though, the whole thing becomes solid and predictable. You stop fighting the platform and start trusting your own setup.&lt;/p&gt;

&lt;p&gt;If you’re building flows that rely on deep linking, treat this part as foundational. A clean Universal Link setup removes an entire category of bugs before they ever reach your users, and it’s one of those things that, once done right, you never want to revisit again.&lt;/p&gt;

&lt;p&gt;To find out more interesting blogs check out:&lt;br&gt;
&lt;a href="https://prototyp.digital/blog" rel="noopener noreferrer"&gt;BLOG&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>mobile</category>
      <category>ios</category>
      <category>android</category>
    </item>
  </channel>
</rss>
