Universal links are a powerful tool in your company’s marketing toolbox. With the right architecture, a single URL can direct users to a coordinated experience regardless of platform.
Even under the ordinary iOS ecosystem, universal links come with a large list of gotchas. The scope of this blog post is the solutions to these and additional gotchas that come from getting universal links to work within React Native the same way they do with vanilla Cocoa (ha!).
Universal links work through a relationship between the app’s capabilities and your website’s
apple-app-site-association file. This file must have the following properties.
- It must be served via HTTPS
- It must be served without redirects
- It must be at the root
/of the website or inside the
The contents of this file are in the following form:
The definitive location of the TEAM_IDENTIFIER is in the membership section of your Apple developer account. You can also find it scattered about XCode, but there aren’t any indicators of which team’s identifier you are looking at necessarily (mileage varies by XCode version).
The site association file interfaces with your application’s associated domains. In order to get this relationship working, you have to:
- Create/modify the provisioning profile to allow associated domains.
2. Modify your target that is backed by that provisioning profile to have the appropriate Associated Domains.
Regardless of where you are serving your
apple-app-site-associations file (.well-known or root) keep the
applinks:example.com in the same format.
Application Launch Code Hooks
After getting all of that set up, tapping on a universal link should open your app, but it won’t do anything. In order for anything to happen, besides the app opening, you have to implement the correct application life-cycle callback.
Handling the universal links in iOS is not the focus of this article. In order to pass universal link handling off to React Native, use these life-cycle methods to call out to
(Note: this article does not support Swift as we’re not using Swift in our React Native implementation)
Gotcha 1: Serving HTTPS over localhost
So you want a tight feedback loop in verifying that your universal link setup works correctly. You have a simulator running your app that is able to point at localhost. Unfortunately, because the
apple-site-association-file must be served over https you’re going to need to do just a little bit more.
Getting an https tunnel set up on localhost is tedious and while it can be done, my recommendation is to use ngrok to do it for you. With very minimal setup, you can use ngrok to temporarily host your localhost website on a publicly addressable https endpoint. After installation it’s as simple as running
ngrok http <port number> Make sure you add your temporary ngrok address to the application’s Associated Domains.
Gotcha 2: Universal Link source and format
On the iOS device / simulator, it can be difficult to find a way to input arbitrary universal links since directly inputting the address URL into Safari will not have the desired effect. As it turns out, the best place to input arbitrary clickable links on a device is the Notes app. On a simulator it’s the Messages app.
The Messages app also requires that any params be URL encoded.
Gotcha 3: app-links caching
apple-app-site-association file is cached by your application! It is initially downloaded on the install of the app and then is periodically (our hypothesis is every 24 hours from initial app install) re-installed. If changes to the available universal link routes appear to not be working, try deleting the app and re-installing it.
Handling Universal Links from Within React Native
Now for the fun part!
Linking callbacks via
AppState callbacks for the most seamless integration.
Short note before we get started, when you see
Navigation in the code, it is the react-native-navigation library that abstracts modifying the view controller stack. Through experimentation, I’ve found that it’s not as good as the react-navigation library, however. Substitute it for whatever your team uses to manage the backbone view controller hierarchy of your app.
The way that the iOS native code that handles universal links gets translated into React Native code is via the app life-cycle hook we installed earlier. The call to
RCTLinkingManager goes in one end as Objective-C code and then comes out the other via a callback in React Native on the
Linking object. What this means is we have to begin to get clever about how we handle universal links because React Native is hosted inside the iOS application.
Essentially, we have a problem where there is some design required around which objects handle the
Linking callbacks because they may or may not be ready on app launch. In our particular case, we use CodePush to ensure that our users are on the latest version of the app. React Native docs recommend that you perform the initial URL fetch and callback registration from within a mounted component, however since we wish to delay execution of handling the URL until after checking the user’s credentials and app version mounted components might get blown away by the latest version download.
Additionally, due to a bug that is expected to be resolved in iOS 13, network calls made before the application’s networking layer is fully online will fail with the fairly cryptic
Software caused connection abort.
What we need then is a solution that takes into account the states of Linking (initialURL or callback) as well as AppState (active or not).
This is some of the first code that gets run in the React Native portion of our app. We register for callbacks that happen when the app state changes (the app gets backgrounded, the app comes back from background, the app terminates). Then we register for Linking callbacks, events where the user has launched or foregrounded the event via a universal link. Notably, neither of these callbacks fire at this point because the app has already launched (potentially via a universal link).
We then perform some middle layer of operation. For us this includes checking to see if the user has valid credentials and waiting for the app to update (if necessary). We also kick back to the log in screen if the user does not have valid credentials. Finally, we see if the app happened to be opened via a universal link
Linking.getInitialURL() and handle it at that point.
This is the callback code that fires when the
Linking listener gets called. It also gets called when
Linking has an
initialURL. Our universal links need to make network calls in order to get the data about the resource that the link represents (a pretty typical thing that will need to happen). In order to avoid the bug I brought up earlier, we perform that network operation
await route.navigationOptions() if and only if the
AppState is currently
If the AppState is not currently active, we create a “pseudononymous” callback that waits until the AppState change callback is called at which point it deregisters itself and calls the next handler. We don’t check the app’s state inside the pseudononymous callback because it’s checked in
Here we have
_genericStateChange which gets fired every time the application changes its state allowing us to pause for updates when it’s foregrounded or when it’s launched.
We also have
_universalLinksStateChange which is code that only gets executed if we launched the app from a universal link but had to defer execution of those links due to the app otherwise not being ready. It’s what is called in the self-deregistering “pseudononymous” listener from above.
These two methods feed into the primary callback
_stateChangeFromSource where we update the app and then conditionally handle the universal links if that’s how the app opened.
And that’s it! We have a kind of mutex that conditionally waits until two different listener methods are called before proceeding and this allows us to fit a nice sweet spot between the primary bundle of our application and the native code that is trying to affect change upon it.
Hope you enjoyed this article and/or found it useful and I’d love to hear additional ideas and feedback if you have them. Happy coding!