Recently I had the privilege of consulting with a client and their team of mobile engineers on a React Native application. Working with them gave me a great deal of new experience and insights into developing React Native applications. One of the challenges I had the opportunity to help them with was adding deep link and universal link features to their mobile app.

In a mobile app deep links add a way for the app to recognize a link schema and handle incoming links, routing to the appropriate screen. The schema will look something like:

appName://path/to/screen

These links are often used in push notifications so that messages to a user can open to a desired place in the app, prompting the user to take action or presenting them with timely information. An app developer can also use deep links to allow third party apps to link to content or actions inside their app.

Universal links are similar to deep links. They are HTTPS-based URLs, most likely shared with a web domain. Once universal links are configured, a URL is able to open on the web or the app based on whether or not the app is installed. A universal link may look like:

https://app.example.com/path/to/screen

Universal links are often used to create a fluid experience for apps that enhance or extend a web experience. Companies such as Reddit use universal links to unify the experience between their web content and their app. When a universal link is opened in a mobile web browser, an app banner will be shown to let users know the company offers an app for additional features. Then if a user already has the app installed, the link will take them directly to the content inside the app.

Coffee Notes App

The work I did for our client is proprietary so for the purpose of demonstrating deep link setup I’ve created a basic React Native application, Coffee Notes. Coffee Notes is a very simple app for taking notes about one’s daily coffee brew. Source code for Coffee Notes is available on GitHub so feel free to build it and try it out.

Coffee Notes app

One of the many reasons to choose React Native for a mobile app is the ability to build both Android and iOS apps from the same code. React Native conveniently abstracts away many of the native concerns of iOS and Android. This means that for most parts of the application, developers are able to build screens and components using JavaScript or TypeScript without worrying about the implementation details of each mobile platform.

Some concerns, however, do require involvement in the native code—and adding deep links is one of those concerns. React Navigation, the recommended library for navigation in React Native, handles a good deal of deep link logic. Their documentation offers guides on the native code that will need to be added.

iOS Native Configuration

The first step is to tell the iOS app about the navigation package installed by React Navigation. To do that I’ll follow the documentation’s instructions to override the application:openURL:options: method of the iOS AppDelegate. AppDelegate.m is the entry point for iOS applications and is written in Objective-C.

// Add this header at the top of the file:
#import <React/RCTLinkingManager.h>

// Add the following functions inside `@implementation AppDelegate` above `@end`:
- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

Android Native Configuration

Continuing to follow React Navigation’s guide, some Android native changes need to be made too. AndroidManifest.xml is the root configuration definition for the Android application. Here the documentation instructs us to add a new intent-filterconfiguration section. In the Android configuration the intent-filter tag defines intents that an application activity can respond to. This example defines an intent that will respond to a deep link schema starting with coffeeNotes://.

<activity>
 ...
  <intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.DEFAULT" />
      <category android:name="android.intent.category.BROWSABLE" />
      <data android:scheme="coffeeNotes" />
  </intent-filter>
 ...
</activity>

Optional: Web Configuration for Universal Linking

I won’t be able to demonstrate universal links with the Coffee Notes example, because adding associated domains in Xcode requires an active Apple Developer account. But this is a very useful configuration, so I’ll cover the web side setup. When setting up universal links, your app is able to respond to a web URL. Apple and Android both require that app-to-web associations be verified, and that verification must be added on the web.

Apple specifies how to configure this in their documentation on supporting associated domains. Here’s an example of how I would set up the Coffee Notes app.

<site>/.well-known/apple-app-site-association

{
  "applinks": {
      "details": [
           {
             "appIDs": [ "App ID here" ],
             "components": [
               {
                  "/": "/*",
                  "comment": "Matches any URL to this subdomain"
               }
             ]
           }
       ]
   }
}

Android has a similar configuration file and describes it in their Verifying Android App Links documentation. Here is what I would add to a web domain associated with this app:

<site>/.well-known/assetlinks.json

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.coffeenotes",
      "sha256_cert_fingerprints":
        ["SHA256 fingerprints here"]
    }
  }
]

Configuration for React Navigation

Okay, we’re finally to the JavaScript part (or TypeScript if that’s your preference).

Reminder: this is the way to do this when using React Navigation - currently the recommended library for navigation in React Native applications according to their documentation.

The React Navigation documentation instructs us to wrap the whole application in a <NavigationContainer>. This provides all screens beneath it a context for navigating to other screens.

This is also where we need to add the React Native configuration for deep link paths. NavigationContainer accepts a prop called linking. Here I’ll tell React Native about the paths I expect it to be able to handle. For this Coffee Notes app I’d like to be able to deep link to the Home Screen and also a details screen I’ll call CoffeeNote.

// Example
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {NavigationContainer} from '@react-navigation/native';

const Stack = createNativeStackNavigator<RootStackParamList>();

const RootStack = () => {
  return (
    <Stack.Navigator initialRouteName="Home">
      <Stack.Screen name="Home" component={Home} />
      <Stack.Screen name="CoffeeNote" component={CoffeeNote} />
    </Stack.Navigator>
  );
};

export const LinkingConfig = {
  prefixes: ['coffeeNotes://'],
  config: {
    screens: {
      Home: 'home',
      CoffeeNote: 'coffee_note/:id?',
    },
  },
}

function App() {
  return (
    <NavigationContainer linking={LinkingConfig}>
      <RootStack />
    </NavigationContainer>
  )
}

At this point the Coffee Notes app should be able to handle two different deep links paths: /home and /coffee_notes/some_id. More complex paths can be configured, but this is a great start. Check out React Navigation’s documentation for all the details.

Let’s test out what we’ve done so far!

Admittedly that was a significant amount of boilerplate. Once all that configuration is in place, I’d like to make sure these deep links work as intended. The iOS Simulator and Android Emulator are a quick place to test out a React Native application.

I would warn that I’ve witnessed inconsistent behavior between simulators/emulators and physical devices. So I recommend verifying your changes on a physical iPhone and/or Android before releasing to app stores.

On simulators/emulators you can test your deep links from the command line or you may opt to test links on a real device. I find the command line tools convenient for repeatability so I’ll demonstrate that method here. A JavaScript package called uri-scheme provides a helper for running on either iOS or Android.

To simulate opening a deep link on your platform run:

npx uri-scheme open "coffeeNotes://" --[ios | android]
GIF showing a deep link opening the the app on iOS simulator

You should see the app open as a result of that command. Since no path was given the app won’t attempt to navigate anywhere. We can try again with coffeeNotes://home; this would navigate back to the Home screen from anywhere else in the app.

It would be nice to know we can link to actual content from deep links. So far I’ve shown a pretty empty version of the app. I’ve now added some data to the app, a few coffee notes, so that we can try linking to one of them.

Screenshot: Coffee Notes ap with some data

The coffee note IDs are basic integers starting at 1. Based on the configuration added earlier the Coffee Notes app should be able to handle a deep link to a specific note. To demonstrate I’ll run:

npx uri-scheme open coffeeNotes://coffee_note/1 --ios

The app should open to the note from November 5.

GIF showing a deep link opening a coffee note screen

That’s it! The Coffee Notes app has a minimal amount of navigation and linking that I can build upon as it grows.

Lewis M Sparlin

Hash An icon of a hash sign Code Name
Agent 00125
Location An icon of a map marker Location
Joplin, MO