Shared recipe links like https://recipes.example.com/r/42 open Safari instead of your React Native iOS app, with no error anywhere; how do you diagnose it?
answer
- silent means verification, not JavaScript
- fetch the AASA: 200, no redirect
- Team ID and bundle ID must match
- entitlement in the signed build
- cached at install, not per tap
basics
~20 sTreat it as failed verification: fetch the AASA and confirm HTTPS, 200 and no redirect; check its Team ID, bundle ID and paths; confirm the signed build carries applinks:host; then reinstall, because iOS caches the file.
solid answer
~40 sBecause nothing errors, the fault is almost always **verification**, not React Native code. First fetch `https://recipes.example.com/.well-known/apple-app-site-association` with `curl -i`: it must return 200 over HTTPS with **no redirect** (an apex-to-`www` or login redirect is a common culprit) and valid JSON. Then compare its `appID`/`appIDs` with the build's real `<TeamID>.<bundleID>` and confirm `/r/*` is covered and not excluded. Next inspect the signed app, not the project: the Associated Domains entitlement must contain `applinks:recipes.example.com` with no `https://`, and the provisioning profile must include the capability. iOS fetches the file at install or update and caches it, so **delete and reinstall** after any fix, and test by tapping the link in Notes or Messages rather than from a page on the same domain.
go deeper
Recall that the association file must be on the domain and the domain must be in the app's entitlements; if either is wrong the link opens Safari.
Explain why the failure is silent and walk through checking the file's response, its Team ID plus bundle ID, and the applinks: entitlement format.
Diagnose in order from curl to the signed entitlements to cache invalidation, and name the real-world causes: redirects, gated hosts, staging bundle IDs and same-domain taps.
Set a release process where AASA path changes ship with app releases and every claimed host is monitored, since a web-team redirect can silently break the app's links.
## Why the failure is silent A Universal Link that fails verification is indistinguishable from one for an app that is not installed: iOS simply opens the URL in Safari. No error reaches the React Native app, Metro or the console, because the app never launched. So the first step is to separate two very different problems: | Symptom | Likely layer | |---|---| | Safari opens, app never launches | **Verification** (AASA file, entitlement, caching, how the link was opened) | | App launches but shows the wrong screen or nothing | **URL handling** inside the app (`Linking`, navigation mapping) | This question is about the first row. ## Step 1: check the website half Fetch the file exactly where iOS looks for it: ```bash curl -i https://recipes.example.com/.well-known/apple-app-site-association ``` What to look for: - **Status 200 over HTTPS with a valid certificate.** A 404, a 5xx or a certificate problem means no verification. - **No redirect.** A `301` from `recipes.example.com` to `www.example.com`, or to a login page, breaks verification even if the target serves the right file. Each host the app claims must serve its own copy directly. - **Nothing in front of the file** that blocks non-browser clients, such as a password gate or bot protection. The fetch is made by Apple's infrastructure rather than by a user's browser session. - **Valid JSON under 128 KB uncompressed**, with the file name exactly `apple-app-site-association` (no `.json`). ## Step 2: check that the file names this build - `appID` or `appIDs` must equal `<TeamID>.<bundleID>` of the build on the device. Staging variants with a different bundle ID, such as `com.example.recipes.dev`, need their own entry. - The Team ID must be the team that signs the build, which is easy to get wrong when several teams exist. - The path must match: `/r/*` covers `/r/42`, but an earlier `components` entry with `"exclude": true` can shadow it, because iOS applies the first matching rule. ## Step 3: check the app half in the signed build Inspect the built app rather than trusting the project files: 1. The entitlements must contain `com.apple.developer.associated-domains` with `applinks:recipes.example.com`. The value is a host only; `applinks:https://recipes.example.com` is a common silent failure. 2. The provisioning profile must include the **Associated Domains** capability; otherwise the entitlement is not honoured. In an Expo project, `ios.associatedDomains` in app config generates the entitlement and EAS Build registers the capability. 3. The domain in the entitlement must match the host in the shared link exactly; `example.com` does not cover `recipes.example.com`. ## Step 4: rule out caching and the way the link was tapped - **Caching.** iOS downloads the association file when the app is installed or updated and does not re-check on each tap. After fixing the file, **delete and reinstall** the app to force a new fetch; for users in production, a changed path only takes effect as installs pick up the new file, typically with the next app update. During development, an entitlement entry with `?mode=developer` lets a development-signed build fetch directly from your server once associated-domains development is enabled in the device's Developer settings. - **Where the link was tapped.** Test from Notes or Messages. A link tapped on a page of the same domain in Safari navigates within Safari by design, and an app that opens links in its own in-app browser never hands them to iOS. - **A remembered user choice.** If the user previously chose to open the domain in Safari, iOS can keep doing so for that domain until they choose the app again. ## Step 5: only then look at React Native If the app now launches but lands on the wrong screen, the problem has moved to URL handling: a bare `AppDelegate` must forward the browsing activity to `RCTLinkingManager` so `Linking` emits the URL, and the navigation linking config must map `/r/:id`. Those are separate topics from verification. ## Why this order The steps run from the cheapest check with the widest blast radius (one `curl`) to the most specific. In practice the majority of silent failures are a redirect, a Team ID or bundle ID mismatch, or a stale cached file, all found before touching JavaScript.
- You added /collections/* to the live apple-app-site-association file; why do existing users still open those links in Safari?iOS fetched and cached the association file when the app was installed or last updated, and it does not re-check on each tap. Existing installs keep the old path list until they fetch the file again, which in practice comes with an app update. So a new path should ship together with an App Store release, and you should test it with a fresh install.
- How can you test Universal Links on a development build without waiting on Apple's cached copy?Add a developer-mode entry such as `applinks:recipes.example.com?mode=developer` to the development build's entitlements and enable associated-domains development in the device's Developer settings; iOS then fetches the file directly from your server for that build. Expo's docs also describe using `npx expo start --tunnel` with a fixed `EXPO_TUNNEL_SUBDOMAIN` to get a public HTTPS host for testing.
saying these in an interview costs you the question
- A Universal Link that opens Safari means the JavaScript Linking listener is broken
- iOS follows redirects, so apex-to-www on the AASA is harmless
- iOS re-downloads the AASA every time a link is tapped
- Tapping a link on a page of the same domain in Safari is a valid test
- A matching bundle ID is enough; the Team ID prefix does not matter
- Fixing the AASA fixes existing installs within seconds