While developing different kinds of software, we often find ourselves in a need to navigate in between the platforms or services. As programmers, we have quite a few ways to do so, be it a simple URL link or a deep link. And while redirecting with an ordinary link seems quite easy, handling this redirection with aforementioned deep links, universal links or asset links might be a bit harder while having a mobile app in our domain. Especially for somebody who is new to the topic.
In this article I will walk you through the aforementioned deep links, universal links and app links: first starting with some theoretical knowledge in a nutshell, and then diving into a little bit of coding!
Deep links are a special kind of links that can automatically open up the mobile application and send the user straight to a certain screen within the app, significantly improving the UX and the time which the users would spend on navigating to a certain location themselves. This form of communication can also act as some kind of information carrier thanks to the URL params, enabling developers to pass data like tokens, pre-filled forms, and others.
When clicking on a deep link, the user will be asked whether he would like to open the app (on iOS) or he will see a dialog asking which app should be used to open it (on Android) – you can open a link to Google Maps in, e.g., Chrome or Google maps app.
As for the structure of deep links - they look different from your usual link: instead of the commonly known https:// URL scheme, we have this custom one defined by the developer. The rest of the link, like host, path and optional params, are just like a normal link.
Example deep link:
As mentioned before, deep links can significantly increase the UX of our applications. We can implement them in various ways, serving different kind of tasks, be it for example:
The possibilities are endless. Those mentioned are just a few.
To trigger the app open, we can treat a deep link just like a normal url - by simply passing it as href parameter when on web or using Linking module from React Native if we want to do it from within the mobile application (it works this way too 😎). We also have the option to programmatically update the window.location.href in the web browser, but we need to be aware that this is not the perfect solution as we simply do not know whether the app is present on the device. On top of that, we need to check the device type - we obviously cannot install any mobile app on desktops.
Let’s get further down the road and get through the setup of deep links for each platform.
Having gone through the theory, let’s see how we actually set up and use deep links on iOS, using the starter app I mentioned in the introduction.
First and foremost, we need to (1.) update the implementation file AppDelegate.m and add a few new methods there. Second, we need to tell the app which URL scheme it should listen for (2.). Both of these steps can easily be done using XCode.
Note that you can complement the knowledge presented in this article with this really awesome and thorough documentation: React Navigation Documentation -> [Deep Linking Setup].
Let’s tackle the AppDelegate.m file then (1.):
ios/OAuthReactNative/AppDelegate.m:
Don’t forget about importing RCTLinkingManager ☝🏽 :
ios/OAuthReactNative/AppDelegate.m:
What we have done here was adding three methods: openURL, yet another openURL and last but not least, continueUserActivity. The latter is not particularly needed for deep links to work, but it will be required when moving further along, for Universal Links.
Now in order to set up the deep links correctly, we need to add the URL scheme (2.). For the purpose of this article, let’s go with oauthrn:// for example 🦾.
That’s pretty much it as far as iOS is concerned. Now you can easily test if you’ve implemented everything correctly by firing up the app in the simulator and executing this command from your shell:
Next, yet another platform to be configured! This time, it’s Android. Shall we?
Same as with the iOS setup, complement your knowledge with React Navigation Documentation. In order to set up everything for Android, we have to go through two steps (all within the AndroidManifest.xml file):
This is the configuration from our app:
android/app/src/main/AndroidManifest.xml
We are pretty much done, there is no need to do anything more. Similarly to the iOS, we can test our setup with this command:
Given that we will be coding along a simple app that handles both kinds of links later, here is the starter repo with the initial setup done: React Native OAuth Linking App - deep links config
Universal and App Links are different from Deep Links. The former (Universal Links) are iOS-specific links and the latter (App Links) are Android-specific. In general, at first glance their behavior is pretty much similar to Deep Links - such links can open a mobile app and carry a certain kind of payload, significantly improving the UX and enriching multiple use cases.
The content assigned to a given URL is present for both mobile and web (meaning that e.g. a product page can be opened in the web browser, as an oridinary web app, or the link can open the mobile app which would automatically redirect you to a certain screen in the app with the same data about the same product - Amazon’s app and web page would be a great example), thus in case that anything goes wrong on the mobile side/redirection, the proper content is still being served to the user on the web. But this is not the case with Deep Links, as they have no content assigned to the web. We simply won’t access anything on web with this other than an error page (given that we don’t have the app installed).
As one would easily guess, there is one more major difference.
The main thing that differentiaties them from last chapter’s Deep Links is how they look - an Universal/App Link is simply just a regular URL like any other with the difference being that it is bound to the mobile application. By bounding, I mean that a certain application (judged by its bundle id/package name) has to be assigned to the domain. This is done by hosting a properly formatted file on your web server. On top of that, this must be followed by some further setup in the application codebase - adding Associated Domain entitlement on iOS and adding a new intent-filter on Android (similarly to Deep Links). This particular characteristic makes the Universal/App links a bit safer as two-way authentication is required - config on the application side and then on the web server side. During the app installation, the system downloads the config files from the web server and checks them against the app setup.
The ideal use cases for Universal/App Links would be the same as the ones proposed for Deep Links in one of the previous chapters. Although the use cases are no different, there is one thing that will make Universal/App Links stand out - they fallback to a web page.
How do we handle account confirmation? We probably already have it present in our web app at the moment of developing the mobile app. What we can do is simply assign the exact same link for account confirmation that we already have on the web to a linking configuration on mobile and upon clicking on the link in the email, open the mobile app and make proper calls to our BE. In case a user opens the link on desktop, they are simply presented with the web version. This makes it a better UX for a user and a better developer experience for us, developers.
Based on what we already know from the previous chapter, there is a need to host a proper file on our domain in order for Universal Links and App Links to work*. Let’s go through it in this section!
* You can actually ignore it for Android. As a result, before opening a link the user will be prompted with a dialog that will ask them how they want to handle the link - using a browser or a mobile app (like with a deep link). When we host the assetlinks.json file, no dialog will be opened.
The files that must be hosted on web server are:
Both of these files have to be hosted on your domain, at /.well-known/{FILE} path (for iOS, it can be either /.well-known or /). If you were to have the my-app.com domain, then these files should be accessible at:
Both should be content-type: application/json. Note that apple-app-site-association (AASA) does not have any extension. Host it as just apple-app-site-association (NOT! apple-app-site-association.json 👨🏽🏫).
Side note: none of these files have to be encrypted.
Now that we know where to host them, we should also take a look at their content. Let’s start with iOS and its AASA file.
Contrary to Android (assetlinks.json, more on it later), Apple requires us to put all the paths config in the AASA file itself, whereas on Android (assetlinks.json) all this config is done in the app code.
Let’s start and create the AASA for our application.
The file is basically a JSON, so most developers should not be surprised. Note that a correctly formatted file is a must for it to work (e.g. use double quotes, not single ones etc.).
Here is an example:
Like in the example above, the AASA file is an object. The config for the universal links must be located under the applinks key. The aforementioned JSON object can also contain a different configuration, e.g., webcredentials if we were to implement credentials saving from our forms to the iOS keychain. But this won’t be our focus as it is beyond the scope of this article.
The value of applinks is another object in which we have to include the apps property, which is an empty array. We can exclude it only if we do not want to support iOS versions 12 and below.
Next, we have the details property and one can say that this is the core config. Inside, appID or appIDs fields have to include the app id (either just a string for appID or an array of strings for appIDs). App ID consists of a team ID and the application’s Bundle Identifier split with a dot. The former can be found for instance on Apple Developer Page, in the upper right corner (see: screenshot from below, it has this 10 character long string form - ABCD1234AB). The latter is simply the app id - you can check it in XCode, under the General -> Bundle Identifier tab.
The paths and components fields represent the paths config - which path should be “attached” to the mobile app. And although both of these properties result in the same thing, the components property is much more configurable and is a newer addition to the AASA. That said, you can add both though if you want to support older iOS versions. If you’ll be using both components and paths, the parser will ignore the paths setup in favor of components when installing/updating the app (on iOS versions that supports components property).
So in order for the pattern to match a set of paths on your domain, you can do the following for paths array:
Pattern matching for components setup is much more sophisticated and can look more or less like this:
As for the wildcards used above, * will match every string, whereas ? will match any one character.
To learn more, see the links below. You can find far more information about configuring the Universal Links with AASA file (like substitution variables):
How does associating the mobile app with the AASA file works behind the scenes? Every time that the application is being installed or updated, the iOS downloads this file from your server and validates it. Since iOS 14 and macOS BigSur, Apple has introduced the Apple CDN server which caches these files - every app install will check if this file is cached on the Apple’s CDN and if not, it will try to download it from your server. This way, it may significantly decrease the load on your server.
After we have sucessfully edited and hosted the AASA file, there is still one thing we have to change in the app files - we have to add the Associated Domains capability in the XCode. Open your .xcworkspace, select your project and simply click on + Capability , just like in the video below. After adding this capability (it is available only for accounts with an active developer programme), you have to add the domain name.
It is as simple as adding a new entry with a string that starts with applinks: and is appended with the domain name. When adding the domain, remember that you have to omit the www and https part. An example entry would be:
If you happen to have multiple subdomains, you can use a wildcard:
We have a quite similar situation for the Android platform. As mentioned above, we have to upload assetlinks.json file on our server in order to connect the mobile app with our website.
Contrary to Apple, in case of assetlinks.json file, the path config is to be handled in the application code, not in the hosted file itself. The file should contain only the neccesary information like package name and sha256 fingerprints of your app’s signing certificate.
The example file looks like this:
The key elements, as stated before, are package_name and sha256_cert_fingerprints.
package_name should be the same as the one from your AndroidManifest file. sha256 fingerprints is an array so you can include values, e.g., for your release and debug applications. To quickly generate each fingerprint, you can use
to generate this. Don’t forget to exchange my-key with the correct name of your key :)
All the remaining properties, like relation or namespace, are constant for every configuration and required for the system to correctly associate the web side with the mobile application.
What about when we want to associate multiple applications? The assetlinks.json contains an array of this configs, so it is just a matter of appending this array with another JSON object containing the appropriate package name and at least one set of sha256 fingerprints, like so:
Now let’s go to the paths config. Paths config is similar to deep links with the difference being that the intent-filter should have the https scheme, not the custom one.
Intent-filter should also have the android:host, android:path and android:pathPrefix properties.
An example config can look like this:
All intent-filters should be nested in the MainActivity similarily to deep links.
Notice the autoVerify flag. When android:autoVerify=“true” is present on any one of your intent filters, installing your app on devices with Android 6.0 and higher causes the system to attempt to verify all hosts associated with the URLs in any of your app’s intent filters
How does it work?
The system will check for intent-filters including:
and for each unique host name located in the declared intent filters, Android will then query the corresponding URLs for the assetlinks.json file at https://my_domain.app/.well-known/assetlinks.json.
As for the pattern matching the paths - find examples with some comments below:
When trying to visit https://my_domain.app/my_path or https://my_domain.app/my_path?param links, the app will open!
By now, you should have an idea of what are the main differences between deep links and universal (iOS) / app (Android) links.
To recap, here are the main conclusions in a nutshell:
Perhaps you will also find the following table useful for comparison 🙃
In this part we will start working on our aforementioned app and focus on implementing a prototype of two simple features - google login with deep links and account confirmation with universal / app links.
If you want to code along using my setup, feel free to start from the starter repo. You can also take a look at the config files that I hosted on my domain oauth.danielgrychtol.com and get some inspiration. The domain is also added in the Associated Domains capability. The same goes for the Android setup -> the starter repo is already configured for this domain.
My config files:
Also, for the purpose of the article, I’ve set up a simple backend app that is hosted on the same domain. You can find the source code on GitHub as well - OAuth Google Backend.
If you’ve followed through and set up everything, your locally run app should be opened upon clicking on a deep link / universal link. What we still lack though is the ability to redirect a user to a certain screen based on the URL that is opened. We can easily do it with react-navigation library. Our starter has the library already installed (link to the repo), along with the linking config passed to NavigationContainer. We just have to correctly populate all the data. More information on linking setup available in the docs to react navigation linking
We want to sync two URLs with our app: the first one being our deep links and the second being our domain that has the AASA and assetlinks.json files hosted.
Let’s get our hands and keyboard dirty and add the domains (deep link scheme and domain oauth.danielgrychtol.com) to the prefixes array in src/navigation/Linking.tsx:
src/navigation/Linking.tsx
src/constants/Domains.ts (create one):
Next, we want to assign two paths to our routing: one for handling the callback from Google OAuth and the second one for handling account confirmation:
src/navigation/Linking.ts
In a real life scenario, it may be likely be that you are going to have some nested navigators. Handling the redirection in such case is not that hard, it is just as simple as adding a new screens key to a given screen.
src/navigation/Linking.ts
But we don’t need it in our simple app.
Now, both of our routes need to handle the URL params. By declaring the path like we just did above, we can simply extract them by using the useRoute hook and get the e.g. the token value from the URL params. Take a look at the below example:
Next, let’s edit our Confirm Account and Redirect screens and handle extracting the token from params and then fire up some dummy logic which you can later substitute with calls to the back end:
types/MainNavigation.ts
src/screens/Redirect.tsx
src/screens/AccountConfirm.tsx
AccountConfirm screen above has no styles or imports whatsoever, remember to add them yourself or use the base of the file from building-app or master branch of the demo app ☺️
At this point, when we build our app and try out the linking, the app should know which screens to open. We covered the topic of how to test-open links in one of the previous chapters*. You can also use the Notes app or send an email with a link to yourself. All methods should open our apps:
It should behave similarly with the second screen.
Now that we: 1) have our app synched with both deep links and universal/app links, 2) paths are assigned to a certain screens and 3) we are able to fetch the route params, we can now start implementing two features:
As mentioned at the beginning of this chapter, we have the backend ready. If we were to visit https://oauth.danielgrychtol.com/auth/google, we would be prompted with the Google OAuth form.
If we successfully authorize with Google, we end up on the https://oauth.danielgrychtol.com/auth/oauth_redirect/:TOKEN path. Now, we want to fetch the token from the path and pass it to the mobile app so that we can use it to get user data on our app side.
To do that, we will use react-native-inapp-browser-reborn. It listens for a deep link and will allow us to get the link and do whatever we want with it. In our case we will get the token from the link and display it in an Alert. In your production app, you will most likely trigger a user fetch or something similar
Let’s first install the in-app browser package:
Proceed with the rest of the setup by following these steps - React Native InApp Browser Reborn
Now that we have the in-app browser installed, we can try to implement the Google OAuth. Let’s add a button in our Main.tsx file:
src/screens/Main.tsx
Nice! Now let’s add a function which would display the in-app browser with the OAuth flow handled by our backend:
Please note that the InAppBrowser’s openAuth method assumes a parameter that is the path which is supposed to resolve the action triggered by the library. It won’t work if the exact same path is added to the AASA file (issue is iOS only)
src/services/oauth.ts
Remember to add oauthWithGoogleRoute const in your constants file at src/constants/Domains
and let’s invoke it in our Main.tsx screen after pressing the button:
src/screens/Main.tsx
Now, if we were to try to press the button, we would be presented with the Google web-based OAuth. When we finish, the route will fire up the deep link which will result in our app being brought back to the foreground. Thanks to the setup, we have access to the token from our BE, which we could use to fetch the user data from backend :)
Congrats! The registration prototype is working. You can freely keep going and add another functionality, i.e., account confirmation.
Usually, the account confirmation feature works in a following way in most apps: a user registers and receives an e-mail with some kind of a confirmation link. When he clicks on it and is redirected to a page / app, the account gets confirmed by means of a call to the backend that takes place behind the scenes. We are going to mimic this in this article and code it in the form of a prototype.
The backend app that I’ve prepared is supposed to send an email containing a proper link every time we authenticate. Clicking the link should open our mobile app and since we already have a screen and redirection configured, we are basically done with the app at this moment. The current behavior of the app would then be, in our case, displaying an alert with the extracted token from the URL. Going further with the feature would be out of scope and by now you should already have an idea of how we can set up such features and code them.
I hope that you found this article helpful and interesting. In case of any questions, please do not hesitate to ask on our Discord server or drop us a message on GitHub. Happy coding!