Deep linking with react native

Yeled THEOPHANEYeled THEOPHANE
7 min read

Deep links are links that can trigger a mobile application to open or to be proposed as an option to open that link. Once opened by the deep link, an app should show the screen corresponding to that link.

There are three types of deep links :

Deep links

source https://developer.android.com

Deep links are URIs of any scheme that take users directly to a specific part of an app. A deep link is created by adding an intent filter in the AndroidManifest.xml :

<activity    android:name=".MyMapActivity"    android:exported="true"    ...>    
    <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="geo" />   
    </intent-filter>  
</activity>

Web links are deep links that use the HTTP and HTTPS schemes. On Android 12 and higher, clicking a web link (that is not an Android App Link) always shows content in a web browser.

A web link can be created with the intent-filter :

<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="http" />    
    <data android:host="yourdomain.com" />  
</intent-filter>

Android App Links, are links that allow your app to be directly opened whenever a user clicks on it. It's a special case of web links where the intent filter has the attribute autoVerify set to 'true'.

<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="http" />
    <data android:scheme="https" />
    <data android:host="myownpersonaldomain.com" />  
</intent-filter>

How to set up deep linking in React Native

we are going to use expo and react-navigation to achieve that.

Setup a simple project

For this purpose let's create a simple project. If you don't want to set everything up manually you can get the full code here: github.com/declaudefrancois/deep-links.

Init a new project.

npx create-expo-app -t blank-typescript <project_name>
cd project_name

Add react navigation (see https://reactnavigation.org/docs/getting-started/)

yarn add @react-navigation/native && npx expo install react-native-screens react-native-safe-area-context

Install the stack navigator

yarn add @react-navigation/native-stack

Your project should look like this :

Now let's set up our screens :

// App.tsx
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

import HomeScreen from "./screens/HomeScreen";
import ItemsScreen from "./screens/ItemsScreen";
import ItemDetailsScreen from "./screens/ItemDetailsScreen";
import { Text } from "react-native";


export type StackParamList = {
  Home: undefined;
  Items:
    | {
        // Query params
        id?: string;
      }
    | undefined;
  Item: {
    id: number;
  };
};

const Stack = createNativeStackNavigator<StackParamList>();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Items" component={ItemsScreen} />
        <Stack.Screen name="Item" component={ItemDetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
// `/screens/Home.tsx`
import { Button, Pressable, StyleSheet, Text, View } from "react-native";
import React from "react";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { StackParamList } from "../App";

type HomeScreenProps = NativeStackScreenProps<StackParamList, "Home">;

export default function HomeScreen({ navigation }: HomeScreenProps) {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome to deep-link tutorial !</Text>
      <Pressable
        onPress={() => navigation.navigate("Items")}
        style={({ pressed }) => ({
          backgroundColor: pressed ? "#039BE590" : "#039BE5",
          padding: 16,
          borderRadius: 8,
        })}
      >
        <Text style={{ color: "#fff" }}>Go to Items</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: "bold",
  },
});
// `/screens/Items.tsx`
import {
  FlatList,
  ListRenderItem,
  Pressable,
  StyleSheet,
  Text,
  View,
} from "react-native";
import React, { useCallback, useEffect } from "react";
import { StackParamList } from "../App";
import { NativeStackScreenProps } from "@react-navigation/native-stack";

interface Item {
  id: number;
  title: string;
  // Other properties
}

export const ITEMS: Item[] = [
  {
    id: 1,
    title: "Item 1",
  },
  {
    id: 2,
    title: "Item 2",
  },
  {
    id: 3,
    title: "Item 3",
  },
  {
    id: 4,
    title: "Item 4",
  },
];

type ItemsScreenProps = NativeStackScreenProps<StackParamList, "Items">;

export default function ItemsScreen({ navigation, route }: ItemsScreenProps) {
  const renderItem = useCallback<ListRenderItem<Item>>(({ item }) => {
    return (
      <Pressable
        style={styles.itemContainer}
        onPress={() =>
          navigation.navigate("Item", {
            id: item.id,
          })
        }
      >
        <View style={styles.itemImage} />
        <View style={styles.detailsSection}>
          <Text>Item</Text>
        </View>
      </Pressable>
    );
  }, []);

  useEffect(() => {
    if (route.params?.id) {
      navigation.replace("Item", {
        id: parseInt(`${route.params.id}`),
      });
    }
  }, [route.params?.id]);

  if (route.params?.id) {
    return null;
  }

  return (
    <View>
      <FlatList
        data={ITEMS}
        ListHeaderComponent={() => (
          <View style={{ marginBottom: 16 }}>
            <Text style={{ fontSize: 16, fontWeight: "bold" }}>Items page</Text>
          </View>
        )}
        contentContainerStyle={{ padding: 16, paddingBottom: 100 }}
        ItemSeparatorComponent={() => <View style={{ height: 16 }} />}
        renderItem={renderItem}
        keyExtractor={(item) => item.id.toString()}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  itemContainer: {
    backgroundColor: "#fff",
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: "#ccc",
    shadowColor: "#000",
    shadowOpacity: 0.2,
    shadowRadius: 1,
    shadowOffset: {
      width: 3,
      height: 3,
    },
    elevation: 3,
    justifyContent: "space-between",
    borderRadius: 16,
  },
  itemImage: {
    width: "100%",
    height: 150,
    backgroundColor: "#ccc",
    borderRadius: 8,
  },
  detailsSection: {
    paddingTop: 8,
  },
  itemTitle: {
    fontSize: 16,
    fontWeight: "bold",
  },
});
// `/screens/Item.tsx`
import { Button, StyleSheet, Text, View } from "react-native";
import React from "react";
import { NativeStackScreenProps } from "@react-navigation/native-stack";
import { StackParamList } from "../App";
import { ITEMS } from "./ItemsScreen";

type ItemDetailsScreenProps = NativeStackScreenProps<StackParamList, "Item">;

export default function ItemDetailsScreen({
  navigation,
  route,
}: ItemDetailsScreenProps) {
  const { id } = route.params;
  const item = ITEMS.find((item) => item.id === id);

  if (!item) {
    return (
      <View style={styles.container}>
        <Text>Item not found</Text>
        <Button
          title="Go to Items"
          onPress={() => {
            if (navigation.canGoBack()) {
              navigation.goBack();
            } else {
              navigation.navigate("Items");
            }
          }}
        />
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <View style={styles.itemImage} />
      <Text style={styles.itemTitle}>{item.title} #{item.id}</Text>

      <Text>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptates
        amet illo officiis animi ipsam dignissimos impedit eos eveniet quisquam
        molestiae quidem voluptas commodi ullam minima ut, consequatur maxime
        dolor non?
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    padding: 16,
  },
  itemImage: {
    width: "100%",
    height: 300,
    backgroundColor: "#ccc",
    borderRadius: 8,
  },
  itemTitle: {
    fontSize: 16,
    fontWeight: "bold",
  },
});

Step 1: Let's create an intent filter

Let's add the following content in our android/app/src/main/AndroidManifest.xml file, inside our MainActivity as shown in the image below:

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

The digital asset helps us to set up a trusted relation between your app and your website thus allowing your app to be the default handler for all links to your website.

First Get the SHA-256 signing key for your app

cd android
./gradlew signingReport

The output should be similar to this :

Next, create an assetlinks.json available at https://yourdomain.com/.well-known/assetlinks.json with the following content :

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.yth.deelinks",
      "sha256_cert_fingerprints": [
        "<Add your SHA-256 keys here>",
      ]
    }
  }
]

Then rebuild your app to make the changes take effect :

npx expo run:android

Install expo-linking

npx expo install expo-linking

And update the App.tsx file with the following content :

import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as Linking from "expo-linking";

import HomeScreen from "./screens/HomeScreen";
import ItemsScreen from "./screens/ItemsScreen";
import ItemDetailsScreen from "./screens/ItemDetailsScreen";
import { Text } from "react-native";

const prefix = Linking.createURL("/");

export type StackParamList = {
  Home: undefined;
  Items:
    | {
        // Query params
        id?: string;
      }
    | undefined;
  Item: {
    id: number;
  };
};

const Stack = createNativeStackNavigator<StackParamList>();

export default function App() {
  const linking = {
    prefixes: [prefix, "https://yourdomain.com"],
  };

  return (
    <NavigationContainer linking={linking} fallback={<Text>Loading...</Text>}>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Items" component={ItemsScreen} />
        <Stack.Screen name="Item" component={ItemDetailsScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

This can be useful for example if your app is not installed on the current device.

We can also let Expo automatically create the intent filters for us, by configuring them in the app.json file.

let's add the property "intentFilters" in expo["android"] :

{
  "expo": {
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "category": ["BROWSABLE", "DEFAULT"],
          "autoVerify": true,
          "data": [
            {
              "scheme": "https",
              "host": "yourdomain.com"
            }
          ]
        }
      ]
    },
  }
}

Tell Expo to update our AndroidManifest and rebuild your app :

npx expo prebuild --platform android && npx expo run:android

Using Android Debug bridge

We can use the Android Debug Bridge with the activity manager (am) tool to test our app links :

adb shell am start
        -W -a android.intent.action.VIEW
        -d <URI> <PACKAGE>

For example :

 adb shell am start
        -W -a android.intent.action.VIEW
        -d "deeplinks://Home" com.yth.deelinks


adb shell am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/Item?id=1"

Using with npx uri-scheme

npx uri-scheme open [your deep link] --android

For example

npx uri-scheme open deeplinks://Home --android
0
Subscribe to my newsletter

Read articles from Yeled THEOPHANE directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Yeled THEOPHANE
Yeled THEOPHANE