Enhancing User Experience With Universal Links (iOS) and Deep Links (Android) by Using React Native

Enhancing User Experience With Universal Links (iOS) and Deep Links (Android) by Using React Native

·

9 min read

Andy Smith

In this article, we will enhance the user experience across both Android and iOS platforms by refining link behaviour through the power of React Native 🚀.

Picture this: a user of an iOS or Android device taps on a link, perhaps via an email or text message that is tied to our specific domain. Instead of the link opening a browser window, the associated app is automatically launched taking the user to the intended page or screen.

This is the effect we’re aiming for.

A very happy user!

This concept is known on Apple devices as Universal Links and on Android they’re referred to as Deep Links.

We’ll make use of the Linking API in React Native that allows us to create custom URL schemes to set up the link to our apps. Additionally, the Linking API comes with methods to help check if an app is installed and open it if necessary, as well as handle the incoming Universal/Deep Links.

Getting started

Prerequisites:

  • Configured public-facing HTTPS domain with client or server implementation (no redirects)

  • Existing React Native project (does not need to be published, can be a sample app)

  • Xcode

  • Android Studio

We will be focusing on applications built with native code, so if you use Expo for your React Native project there will be some differences and you should check Expo’s documentation on Linking to establish what they are. However, generally, the first step at least will still apply.

Step 1 – serve some structured JSON to establish trust

To enable linking for our React-Native application, we first need to create two files and serve them from each subdomain you want to provide links for.

So if you want to use blog.example.com and example.com you must serve the file from both subdomains independently. For the sake of brevity, we’ll focus on a single domain in this article.

In order to establish trust with Apple for our application and domain, we need to host a file publicly that their CDN can scrape. Create a new file named apple-app-site-association to be served over HTTPS from the root of the domain.

💡 Some documentation specifies adding to the .well-known folder but this seems to be used primarily by legacy implementations. Apple recommends adding this to the root.

It must return a MIME type of application/json and contain no file type, i.e. do not add .json to the file name.

The fully qualified path of the file should be:

https://example.com/apple-app-site-association

{
  "applinks": {
    "apps": [],
    "details": [
      {       // "DEVELOPMENT_TEAM.PRODUCT_BUNDLE_IDENTIFIER"
        "appID": "ABCDE12345.example.com",
        "paths": [
          "*"
        ]
      }
    ]
  }
}

We have the option here to map paths manually with custom behaviour, which you can learn more about on the Apple Developers website. However, to avoid repetition or having a separate config for iOS and Android, we’ll just handle paths with our logic in React Native.

After making this file available it may take up to 48 hours for Apple’s CDN to scrape and validate when the app is first installed and a universal link is first used. This can be frustrating when it comes to troubleshooting, so ensure your config is valid and serve the file correctly.

It’s important to note that validity requests for linking are triggered on a user’s device when it is first installed. This can affect testing when linking is in fact working as intended. Just uninstall and reinstall for a sanity test — this can all be done in the simulator.

Android Deep Links use the Digital Asset Links API to establish trust between your app and websites, granting auto link-opening permissions for the specified domain. After successful verification of URL ownership, the system will automatically direct URL intents to your app.

To begin, we need to create a ‘Digital Asset Links’ JSON file to be served from each subdomain we want to provide Deep Links to.

Create a new file named assetlinks.json to be served from this folder: https://example.com/.well-known

If you’re updating an existing app, verify whether you’re using Play App Signing for the app in your Play Console developer account under Release > Setup > App Integrity. This is required to obtain the sha256_cert_fingerprints. If you do this then the correct Digital Asset Links JSON snippet should be on this page.

Otherwise, if you’re just working with a sample app, experimenting, or don’t sign your app, the simplest way to generate the required JSON is to use Android Studio.

Navigate to Tools > App Links Assistance > Step 3 - Associate website, enter your domain and application ID and choose Generate Digital Asset Links File. This will generate the JSON you need to serve from your domain.

From here you’ll be able to link and verify that the file is structured and served correctly.

It should look something like this:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "example.app",
    "sha256_cert_fingerprints":
    ["FA:C6:17:45:DC:09:03:78:6F:B1:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
  }
}]

Unlike Apple’s CDN, you shouldn’t need to wait for Google to crawl the page before Deep Links become available on the user’s device — from testing, it appears the page is immediately queried for the JSON when loading a configured application.

Step 2 – Update our native code

This post was written specifically for version 0.71 of React Native. It is important to check the Linking documentation for RN for the most up-to-date information on how to implement it. This step will provide some additional context.

iOS native code

To configure our iOS application, in Xcode we first need to enable ’Associated Domains’ in the ‘Capabilities’ section of the project settings.

Add the associated domains in the format applinks:example.com, where example.com is the domain hosting the apple-app-site-association file. Once added, you should see that your project’s entitlements file has been updated with the necessary XML.

Now we need to add a link to the React Native LinkingIOS Node modules folder in the application’s header search paths. To add the library path, in Xcode navigate to your project’s ‘Build Settings’. From there, filter or search manually for ‘Header Search Paths’ and add the following path to your library:

$(PODS_ROOT)/../../node_modules/react-native/Libraries/LinkingIOS

With this in place, we now need to add the following code to our AppDelegate.mm file inside of our Xcode project:

#import <React/RCTLinkingManager.h>

- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
 restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
{
 return [RCTLinkingManager application:application
                  continueUserActivity:userActivity
                    restorationHandler:restorationHandler];
}

That’s it! We should now be able to start receiving Universal Links with React Native. More on this in the next step.

Android native code

Setting up our native code for Android is much simpler and we can again in Android Studio use the App Links Assistance tool.

Inside the tool, from Step 1, Add URL intent filters and click the Open URL Mapping Editor button. From here, add your domain as the host, leave your path settings empty and ensure your main activity is selected.

Now, in Step 2, we can use the tool to automatically add logic to our implementation in the form of an XML config update to the AndroidManifest.xml

It should update this with the following:

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="example.com" />
</intent-filter>

Now we’re set up we can move on to the React Native implementation. Once this is done you should return back to the App Links Assistance tool and use Step 4 of the tool to test them on a device or simulator.

Step 3 – Get set up in React Native

React navigation

Typically most projects make use of the popular @react-navigation/native package whose NavigationContainer component accepts a linking object.

This object configuration allows us to associate our ‘screens’ with incoming paths in a simple concise way. The documentation for ‘Configuring Links` is very comprehensive and should be followed to suit your app’s setup.

It’s worth noting that in order for universal and Deep Links to work, you must add your fully qualified domain as prefix:

const linking = {
  prefixes: [Linking.createURL('/'), 'https://example.com'],
};

Once configured in React Native’s side that should be it! You will now have a configured app for both platforms that opens the associated application when a link is tapped from the device.

React Native WebViews

If you also take advantage of React Natives WebViews or need some additional functionality based on some parameters in the URL for example then we can just use the Linking API directly.

There are 2 cases that we need to cater for this:

Cold start

The user opens the link whilst the application is closed

We need to asynchronously call getInitialURL from the Linking API in order to get the path the application was opened with.

You could implement something similar to this hook:

import { useState, useEffect } from 'react'
import { Linking } from 'react-native'

const useInitialURL = () => {
  const [initialURL, setInitialURL] = useState(null)
  const [isProcessing, setIsProcessing] = useState(true)

  useEffect(() => {
    const fetchInitialURL = async () => {
      try {
        const url = await Linking.getInitialURL()
        if (url) {
          setInitialURL(url)
        }
      } catch (error) {
        console.error('Error fetching initial URL:', error)
      } finally {
        setIsProcessing(false)
      }
    }

    fetchInitialURL()
  }, [])

  return [initialURL, isProcessing]
}

With this in place, we can consume this hook where needed in our React Native JS code and use it to set the URI for the WebView. We can also deconstruct the URL to get subdomains, the path or any query/hash-based properties.

💡 It’s worth noting that, by default, the built-in JS Browser-compatible URL class won’t work out of the box correctly with React Native. Use the react-native-url-polyfill library as a replacement.

Warm start

Our final case to cover is a warm start — where the application is already open in the background.

Here we add an event listener to the Linking API that sets the updated URL into state:

import { useState, useEffect } from 'react'
import { Linking } from 'react-native'

const useLinkingURL = () => {
  const [linkingUrl, setLinkingUrl] = useState(null)

  useEffect(() => {
    const onLinkingEvent = async (event) => {
      if (event.url) setLinkingUrl(event.url)
    }

    Linking.addEventListener('url', onLinkingEvent)
    return () => Linking.removeEventListener('url', onLinkingEvent)
  }, [])

  return linkingUrl
}

This should be enough to get you up and running and working with WebViews and being able to process the opened paths in your application!

Working with simulators

At last, we have all the necessary components in place. But we still need to effectively develop and interact with simulators.

You can set up the simulator manually by inputting the information in the OS directly or by creating a webpage that contains a list of desired paths, allowing you to click on a link each time, which we should continue doing for sanity checks and testing purposes.

But this method is not practical from a development perspective and we can streamline the process a little by utilising the commands provided below:

adb shell am start -a android.intent.action.VIEW -d https://example.com

xcrun simctl openurl booted https://example.com

Ok, that’s it!

Time for a brew.

All images in this blog post (except for the featured image) were generated by Midjourney.

Â