xref: /expo/docs/pages/guides/deep-linking.mdx (revision a16ac082)
1---
2title: Deep linking
3description: Learn how to use app/universal links to open your app from a standard web URL.
4---
5
6import { ConfigReactNative } from '~/components/plugins/ConfigSection';
7import { Collapsible } from '~/ui/components/Collapsible';
8import { Step } from '~/ui/components/Step';
9import { CODE } from '~/ui/components/Text';
10import { Terminal } from '~/ui/components/Snippet';
11
12Universal links are different from standard deep links as they use regular HTTPS links to direct users to a specific route in your app. If a user doesn't have your app installed, the link will take them to the associated website. This allows you to send notification emails with links that work seamlessly on desktop web browsers while opening the content in your app on mobile. Android refers to this concept as **app links**, and iOS refers to it as **universal links**. This guide specifically discusses universal links that do not use a custom URL scheme.
13
14> Deferred deep links can be implemented with [`react-native-branch`](https://github.com/expo/config-plugins/tree/main/packages/react-native-branch).
15
16Before you can use an app/universal links, you've to setup the two-way association between the website and app for both Android and iOS:
17
181. **Native app verification:** This requires some form of code signing that references the target website domain (URL).
192. **Website verification:** This requires a file to be hosted on the target website in the **/.well-known** directory.
20
21After the two-way association is setup, you've to setup the runtime routing of the link in your app. This is done in JavaScript and must be configured for every route, then coordinated between web and native. Expo offers a fully automated solution for this called [Expo Router](/routing/introduction/).
22
23> Universal links cannot be tested in the Expo Go app. You need to create a [development build](/develop/development-builds/introduction).
24
25## Universal links on iOS
26
27Universal links on iOS require a paid Apple Developer account as you must associate your fully qualified **Apple Developer Team ID**.
28
29### Native Apple configuration
30
31After deploying your apple-app-site-association (AASA) file, you must also configure your app to use your associated domain:
32
33Add [`expo.ios.associatedDomains`](/versions/latest/config/app/#associateddomains) to your [app config](/workflow/configuration/), and make sure to follow [Apple's specified format](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_associated-domains). Make sure _not_ to include the protocol (`https`) in your URL. This is a common mistake that will result in the universal links not working.
34
35For example, if an associated website is `https://expo.dev/`, the app links are:
36
37```json app.json
38{
39  "expo": {
40    "ios": {
41      "associatedDomains": ["applinks:expo.dev"]
42    }
43  }
44}
45```
46
47Build your native app with EAS Build to ensure the entitlement is registered with Apple.
48
49<ConfigReactNative title="Manual native configuration">
50
51Apps that don't use [Continuous Native Generation](/workflow/continuous-native-generation) (`npx expo prebuild`) must [manually configure](/build-reference/ios-capabilities#manual-setup) the **Associated Domains** capability for their bundle identifier.
52
53If you enable through the [Apple Developer Console](/build-reference/ios-capabilities#apple-developer-console), then be sure to add the following entitlements in your `ios/[app]/[app].entitlements` file:
54
55```xml
56<key>com.apple.developer.associated-domains</key>
57<array>
58  <string>applinks:expo.dev</string>
59</array>
60```
61
62</ConfigReactNative>
63
64### AASA configuration
65
66On the web-side, you've to host a config file from **/.well-known/apple-app-site-association** (with no extension). This file JSON specifies your Apple Developer Team ID, Bundler ID, and a list of supported paths to redirect to the native app.
67
68> You can run the **experimental** CLI command `npx setup-safari` to automatically register a bundle identifier to your Apple account, assign entitlements to the ID, and create an iTunes app entry in the store. The local setup will be printed and you can skip most the following. This is the easiest way to get started with universal links on iOS.
69
70If you're using [Expo Router](/routing/introduction/) to build your website (or another modern React framework like Remix, Next.js, and so on), create the file at **public/.well-known/apple-app-site-association**. Legacy Expo webpack projects can create the file at **web/.well-known/apple-app-site-association**.
71
72```json public/.well-known/apple-app-site-association
73{
74  // This section enables Universal Links
75  "applinks": {
76    "apps": [],
77    "details": [
78      {
79        // Example: "QQ57RJ5UTD.com.acme.myapp"
80        "appID": "{APPLE_TEAM_ID}.{BUNDLE_ID}",
81        // All paths that should support redirecting
82        "paths": ["*"]
83      }
84    ]
85  },
86  // This section enables Apple Handoff
87  "activitycontinuation": {
88    "apps": ["{APPLE_TEAM_ID}.{BUNDLE_ID}"]
89  },
90  // This section enable Shared Web Credentials
91  "webcredentials": {
92    "apps": ["{APPLE_TEAM_ID}.{BUNDLE_ID}"]
93  }
94}
95```
96
97This snippet tells iOS that any links to `https://www.myapp.io/records/*` (with wildcard matching for the record ID) should be opened directly by the app with a matching bundle identifier. It is a combination of the Team ID and the app bundle identifier. The Team ID can be found under the membership details in the Apple Developer account.
98
99> The `activitycontinuation` and `webcredentials` objects are optional, but recommended.
100
101After you've setup the AASA file, deploy your website to a server that supports HTTPS (most modern web hosts).
102
103See [Apple's documentation](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html) for further details on the format of the AASA. Branch provides an [AASA validator](https://branch.io/resources/aasa-validator/) which can help you confirm that your AASA is correctly deployed and has a valid format.
104
105> The `*` wildcard does **not** match domain or path separators (periods and slashes).
106
107As of iOS 13, [a new `details` format is supported](https://developer.apple.com/documentation/xcode/supporting-associated-domains) which allows you to specify:
108
109- `appIDs` instead of `appID`, which makes it easier to associate multiple apps with the same configuration
110- an array of `components`, which allows you to specify fragments, exclude specific paths, and add comments
111
112<Collapsible summary="Here's an example AASA JSON from Apple's documentation">
113
114```json
115{
116  "applinks": {
117    "details": [
118      {
119        "appIDs": ["ABCDE12345.com.example.app", "ABCDE12345.com.example.app2"],
120        "components": [
121          {
122            "#": "no_universal_links",
123            "exclude": true,
124            "comment": "Matches any URL whose fragment equals no_universal_links and instructs the system not to open it as a universal link"
125          },
126          {
127            "/": "/buy/*",
128            "comment": "Matches any URL whose path starts with /buy/"
129          },
130          {
131            "/": "/help/website/*",
132            "exclude": true,
133            "comment": "Matches any URL whose path starts with /help/website/ and instructs the system not to open it as a universal link"
134          },
135          {
136            "/": "/help/*",
137            "?": {
138              "articleNumber": "????"
139            },
140            "comment": "Matches any URL whose path starts with /help/ and which has a query item with name 'articleNumber' and a value of exactly 4 characters"
141          }
142        ]
143      }
144    ]
145  }
146}
147```
148
149</Collapsible>
150
151To support all iOS versions, you can provide both the above formats in your `details` key, but we recommend placing the configuration for more recent iOS versions first.
152
153Note that iOS will download your AASA when your app is first installed and when updates are installed from the App Store, but it will not refresh any more frequently. If you wish to change the paths in your AASA for a production app, you will need to issue a full update via the App Store so that all of your users' apps re-fetch your AASA and recognize the new paths.
154
155Now, a link to your website on your mobile device should open your app. If it doesn't, re-check the previous steps to ensure that your AASA is valid, the path is specified in the AASA, and you have correctly configured your App ID in the [Apple Developer Console](https://developer.apple.com/account/resources/identifiers/list). Once you've got your app opened, move to the [Handling links into your app](/guides/linking/#handling-links) section for details on how to handle the inbound link and show the user the content they requested.
156
157### Apple Smart Banner
158
159If a user doesn't have your app installed, they'll be directed to the website. You can use the [Apple Smart Banner](https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners) to show a banner at the top of the page that prompts the user to install the app. The banner only shows if the user is on a mobile device and doesn't have the app installed.
160
161To enable the banner, add the following meta tag to the `<head>` of your website, replacing `{ITUNES_ID}` with your app's iTunes ID:
162
163```html
164<meta name="apple-itunes-app" content="app-id={ITUNES_ID}" />
165```
166
167If you're having trouble setting up the banner, run the following command to automatically generate the meta tag for your project:
168
169<Terminal cmd={['$ npx setup-safari']} />
170
171If you're building a [statically rendered website with Expo Router](/router/reference/static-rendering), then add this HTML tag to the `<head>` component in your [**app/+html.js** file](/router/reference/static-rendering#root-html).
172
173```jsx app/+html.js
174export default function Root({ children }) {
175  return (
176    <html lang="en">
177      <head>
178        <meta charSet="utf-8" />
179        <meta httpEquiv="X-UA-Compatible" content="IE=edge" />
180
181        <meta name="apple-itunes-app" content="app-id={ITUNES_ID}" />
182
183        {/* Other head elements... */}
184      </head>
185      <body>{children}</body>
186    </html>
187  );
188}
189```
190
191Next time you deploy your website, the banner should appear when you visit it on a mobile device that doesn't have your app installed.
192
193## Deep links on Android
194
195Implementing deep links on Android (without a custom URL scheme) is somewhat simpler than on iOS. You need to add `intentFilters` to the [`android`](/versions/latest/config/app/#android) section of your [app config](/workflow/configuration/). The following basic configuration will cause your app to be presented in the standard Android dialog as an option for handling any record links to `myapp.io`:
196
197```json app.json
198{
199  "expo": {
200    "android": {
201      "intentFilters": [
202        {
203          "action": "VIEW",
204          /* @info Add the autoVerify property to the intent filter in app config to enable app links.*/
205          "autoVerify": true,
206          /* @end */
207          "data": [
208            {
209              "scheme": "https",
210              "host": "*.myapp.io",
211              "pathPrefix": "/records"
212            }
213          ],
214          "category": ["BROWSABLE", "DEFAULT"]
215        }
216      ]
217    }
218  }
219}
220```
221
222### Android Asset Links
223
224It may be desirable for links to your domain to always open your app (without presenting the user a dialog where they can choose the browser or a different handler). You can implement this with Android App Links, which use a similar verification process as Universal Links on iOS.
225
226Create a JSON file for the website verification (a.k.a. [digital asset links](https://developers.google.com/digital-asset-links/v1/getting-started) file) at **public/.well-known/assetlinks.json** (or **web/.well-known/assetlinks.json** for legacy Expo webpack websites), and collect the following information:
227
228- `package_name`: The Android [application ID](/versions/latest/config/app/#package) of your app (for example, `com.bacon.app`). This can be found in the **app.json** file under `expo.android.package`.
229- `sha256_cert_fingerprints`: The SHA256 fingerprints of your app’s signing certificate. This can be obtained in one of two ways:
230  1. After building an Android app with EAS Build, run `eas credentials -p android` and select the profile you wish to obtain the fingerprint for. The fingerprint will be listed under `SHA256 Fingerprint`.
231  2. By visiting the [Play Console](https://play.google.com/console/) developer account under **Release > Setup > App Integrity > App Signing**. If you do, then you'll also find the correct Digital Asset Links JSON snippet for your app on the same page. The value will look like `14:6D:E9:83...`
232
233```json public/.well-known/assetlinks.json
234[
235  {
236    "relation": ["delegate_permission/common.handle_all_urls"],
237    "target": {
238      "namespace": "android_app",
239      "package_name": "{package_name}",
240      "sha256_cert_fingerprints": [
241        // Supports multiple fingerprints for different apps and keys
242        "{sha256_cert_fingerprints}"
243      ]
244    }
245  }
246]
247```
248
249Installing the native app on a device will trigger the [Android app verification](https://developer.android.com/training/app-links/verify-android-applinks#web-assoc) process, which can take up to 20 seconds.
250
251<ConfigReactNative title="Manual native configuration">
252
253If you're not using EAS to manage code signing, you can find the **sha256_cert_fingerprints** by building and submitting your app manually, then visiting the [Google Play Console](https://play.google.com/console/) developer account under **Release > Setup > App Integrity > App Signing**; if you do, then you'll also find the correct **Digital Asset Links JSON** snippet for your app on the same page. The value will look like `14:6D:E9:83...`. Copy this value into your `public/.well-known/assetlinks.json` file.
254
255</ConfigReactNative>
256
257It may be desirable for links to your domain to always open your app (without presenting the user a dialog where they can choose the browser or a different handler). You can implement this with Android App Links, which use a similar verification process as Universal Links on iOS.
258
259<Collapsible summary={<>Handle App Links on Android for <CODE>expo-dev-client</CODE> version 1.2.1 and below</>}>
260
261From Android 12 onwards, there is an issue reported when verifying the App Links with [`expo-dev-client`](/develop/development-builds/introduction/#what-is-an-expo-dev-client) version 1.2.1 and below.
262
263In app config, when `expo.android.intentFilters` is used and `"autoVerify"` is set to `true`, the `expo-dev-client` adds a scheme `<data android:scheme="exp+<slug>" />` to the intent filter. This scheme breaks the App Links verification.
264
265An example of the `exp+` scheme breaking the verification process:
266
267```xml AndroidManifest.xml
268<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
269  <intent-filter>
270    <action android:name="android.intent.action.MAIN"/>
271    <category android:name="android.intent.category.LAUNCHER"/>
272  </intent-filter>
273  <intent-filter>
274    <action android:name="android.intent.action.VIEW"/>
275    <category android:name="android.intent.category.DEFAULT"/>
276    <category android:name="android.intent.category.BROWSABLE"/>
277    <data android:scheme="<slug>"/>
278    <data android:scheme="<package>"/>
279    <data android:scheme="exp+<slug>"/>
280  </intent-filter>
281  <intent-filter android:autoVerify="true" data-generated="true">
282    <action android:name="android.intent.action.VIEW"/>
283    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
284    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
285    <data android:scheme="https" android:host="<name>.onelink.me" android:pathPrefix="/XXXX"/>
286    <!-- @info -->
287    <data android:scheme="exp+<slug>"/>
288    <!-- @end -->
289    <category android:name="android.intent.category.BROWSABLE"/>
290    <category android:name="android.intent.category.DEFAULT"/>
291  </intent-filter>
292</activity>
293```
294
295You can fix this issue by creating a custom [Config Plugin](/config-plugins/introduction/) that removes the `exp+` schemes when verifying `intentFilters`. In your project, create a new file called **withAndroidVerifiedLinksWorkaround.js** with the following code snippet:
296
297```js withAndroidVerifiedLinksWorkaround.js
298const { createRunOncePlugin, withAndroidManifest } = require('@expo/config-plugins');
299
300/**
301 * @typedef {import('@expo/config-plugins').ConfigPlugin} ConfigPlugin
302 * @typedef {import('@expo/config-plugins').AndroidManifest} AndroidManifest
303 */
304
305/**
306 * Remove the custom Expo dev client scheme from intent filters, which are set to `autoVerify=true`.
307 * The custom scheme `<data android:scheme="exp+<slug>"/>` seems to block verification for these intent filters.
308 * This plugin makes sure there is no scheme in the autoVerify intent filters, that starts with `exp+`.
309 *
310 * @type {ConfigPlugin}
311 */
312const withAndroidVerifiedLinksWorkaround = config =>
313  withAndroidManifest(config, config => {
314    config.modResults = removeExpoSchemaFromVerifiedIntentFilters(config.modResults);
315    return config;
316  });
317
318/**
319 * Iterate over all `autoVerify=true` intent filters, and pull out schemes starting with `exp+`.
320 *
321 * @param {AndroidManifest} androidManifest
322 */
323function removeExpoSchemaFromVerifiedIntentFilters(androidManifest) {
324  for (const application of androidManifest.manifest.application || []) {
325    for (const activity of application.activity || []) {
326      if (activityHasSingleTaskLaunchMode(activity)) {
327        for (const intentFilter of activity['intent-filter'] || []) {
328          if (intentFilterHasAutoVerification(intentFilter) && intentFilter?.data) {
329            intentFilter.data = intentFilterRemoveSchemeFromData(intentFilter, scheme =>
330              scheme?.startsWith('exp+')
331            );
332          }
333        }
334        break;
335      }
336    }
337  }
338
339  return androidManifest;
340}
341
342/**
343 * Determine if the activity should contain the intent filters to clean.
344 *
345 */
346function activityHasSingleTaskLaunchMode(activity) {
347  return activity?.$?.['android:launchMode'] === 'singleTask';
348}
349
350/**
351 * Determine if the intent filter has `autoVerify=true`.
352 */
353function intentFilterHasAutoVerification(intentFilter) {
354  return intentFilter?.$?.['android:autoVerify'] === 'true';
355}
356
357/**
358 * Remove schemes from the intent filter that matches the function.
359 */
360function intentFilterRemoveSchemeFromData(intentFilter, schemeMatcher) {
361  return intentFilter?.data?.filter(entry => !schemeMatcher(entry?.$['android:scheme'] || ''));
362}
363
364module.exports = createRunOncePlugin(
365  withAndroidVerifiedLinksWorkaround,
366  'withAndroidVerifiedLinksWorkaround',
367  '1.0.0'
368);
369```
370
371Next, in your **app.json**, add the path to the plugin under `expo.plugins`:
372
373```json app.json
374{
375  "plugins": ["./plugins/withAndroidVerifiedLinksWorkaround"]
376}
377```
378
379If you are using [EAS Build](/build/introduction/), you will have to create a new build after adding these changes to your project so that they are reflected in your Android app.
380
381</Collapsible>
382
383## Debugging
384
385Expo CLI enables you to test your universal links without deploying a website. Utilizing the [`--tunnel` functionality](/more/expo-cli/#tunneling), you can forward your dev server to a publicly available https URL.
386
3871. Set the environment variable `EXPO_TUNNEL_SUBDOMAIN=my-custom-domain` where "my-custom-domain" is a unique string that you will use during development. This will ensure that your tunnel URL is consistent across dev server restarts.
3882. Setup universal links as described above, but this time using an Ngrok URL: `my-custom-domain.ngrok.io`
3893. Start your dev server with the `--tunnel` flag:
390
391<Terminal cmd={['$ npx expo start --tunnel --dev-client']} />
392
3934. Compile the development build on your device:
394
395<Terminal
396  cmd={[
397    '# Build your native Android project',
398    '$ npx expo run:android',
399    '',
400    '# Build your native iOS project',
401    '$ npx expo run:ios',
402  ]}
403/>
404
405## Troubleshooting
406
407- Ensure your apple app site association file is valid by using a [validator tool](https://branch.io/resources/aasa-validator/).
408- Ensure your website is served over HTTPS.
409- The uncompressed `apple-app-site-association` file cannot be [larger than 128kb](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html).
410- [Verify Android app links](https://developer.android.com/training/app-links/verify-android-applinks)
411- Ensure both website verification files are served with `content-type` `application/json`.
412- Android verification may take up to 20 seconds to take effect.
413- If you update your web files, rebuild the native app to trigger a server update on the vendor side (Google, Apple).
414
415## When to not use deep links
416
417This is the easiest way to set up deep links in your app because it requires a minimal amount of configuration.
418
419The main problem is that if the user does not have your app installed and follows a link to your app with its custom scheme, their operating system will indicate that the page couldn't be opened but not give much more information. This is not a great experience. There is no way to work around this in the browser.
420
421Additionally, many messaging apps do not autolink URLs with custom schemes -- for example, `exp://u.expo.dev/[project-id]?channel-name=[channel-name]&runtime-version=[runtime-version]` might just show up as plain text in your browser rather than as a link ([exp://u.expo.dev/[project-id]?channel-name=[channel-name]&runtime-version=[runtime-version]](#)).
422
423An example of this is Gmail which strips the `href` property from the links of most apps, a trick to use is to link to a regular HTTPS URL instead of your app's custom scheme, this will open the user's web browser. Browsers do not usually strip the `href` property so you can host a file online that redirects the user to your app's custom schemes.
424
425Instead of linking to `example://path/into/app`, you could link to `https://example.com/redirect-to-app.html` and `redirect-to-app.html` would contain the following code:
426
427```js
428<script>window.location.replace("example://path/into/app");</script>
429```
430