Universal Links work with Expo locally, but not Testflight or Simulator

Please provide the following:

  1. SDK Version: 39
  2. Platforms(Android/iOS/web/all): Android/iOS

We are stuck. We have universal links working locally - e.g. the app opens when you click a website url - but not when we create a build and use iOS Simulator or Testflight or Android APK (sideloaded).

Basically, this link works from Expo: exp://10.0.0.241:19000/–/?location_id=1051
And this does not, from a build: https://pinballmap.com/map/?location_id=1051

And it is really tough to troubleshoot. I know it’s a lot for someone else to come in and check/verify all this stuff, so I’m not expecting much here. But if anyone has the time/skill, we would greatly appreciate it!

If it works locally, but not with a build, does that definitely point to an issue with the AASA (or intent filter in the case of Android)? We’re just trying to narrow it down. Seems like the code itself works, if it works locally. So, so far we’ve been focusing on troubleshooting the AASA and stuff.

Code that works locally is here.

Our AASA is here (code view), or here (website url). We followed this post for the AASA format.

App.json is here.

And we enabled “Associated Domains” and cleared provisioning before creating the build, based on the docs.

Just to be sure, is this the correct way to clear the provisioning profile? I’m not certain about Would you like to use this profile? … yes

✔ Do you also want to revoke this Provisioning Profile on Apple Developer Portal? … yes
✔ Revoking Provisioning Profile on Apple Servers...
✔ App ID found on Apple Developer Portal.
✔ Getting Distribution Certificates from Apple...
✔ Successfully validated Distribution Certificate against Apple Servers
✔ Getting Push Keys from Apple...
✔ Successfully validated Push Key against Apple Servers
✔ Getting Provisioning Profiles from Apple...
✔ Provisioning Profile - ID: xxxyyy
    Name: Pinball Map Distribution
    Expiry: Mon Jun 01 2020 
 Would you like to use this profile? … yes
Using Provisioning Profile: xxxyyy
✔ Configuring existing Provisioning Profiles from Apple...

Of course, this part is only relevant to iOS, and it’s not working on Android, as well.

Thanks!

Hi, as far as I could see, there may be a few different issues happening here.

First: Android
In your app.json config, you configured the intent filter to open on the "*.pinballmap.com" domain. However, the URLs you posted do not have that dot character before the top level name.
*.pinballmap.com and *pinballmap.com and pinballmap.com are 3 different entities, and none can match the others. Thus, I think you should configure the intent filter for the domain pinballmap.com.
Also, I have noticed that your app.json config is missing a comma afterclosing the intentFilters array. This should not pass as a valid JSON during the build, so either you already fixed it or copied it wrong.
Also, please note that exp:// links open in the app only because the URL scheme (exp://) is associated with the expo client app. It actually only opens the expo client, with a link to a remotely available metro server instance (in this case, the IP 10.0.0.241 ...), but that’s what the expo client knows to handle. It’s fine for developing, but do not take it as a valid measure for real app links.

Second: iOS simulators.
In theory, the iOS simulator apps should be bound by the same rules that the real devices use. However, I have found many people reporting that they are unable to test applinks using Safari in the iOS simulator, and some other apps. They did manage to have a degree of success by using the messaging app (send a message with a short text and the url, then press the url in the message), or the notes app (again, post a note with the url and press it).

Third: The URLs
I have noticed that your testing url has a /-/ after the domain name, while the website url has /map/ instead.
First of all, ending urls or paths with a / may invalidate the path matching, both on iOS and Android. Usually browsers are able to ignore it, but the deep link algorithms may not.
Second, you did not specify such a subpath (/-/) in your android intentFilters config. I am not sure that this will work with the config you specified, but you should probably use the exact path to the /map subpath, pinball.com/map.

I haven’t experimented very much on android, I had simple urls in my case, so it worked on first try. But most likely the issues you are experiencing are related to the domain, path, or subpath matching.

1 Like

I greatly appreciate you taking the time to respond, and for looking at it in such detail. This is helpful, and you’ve given us a lot to consider. I’ll post more as I go through it and try things, and will make sure to note the solution, once we have it!

And yes, I had fixed that closing comma in the Intent Filter! Good eye.

Thus, I think you should configure the intent filter for the domain pinballmap.com

Just an update that this was indeed correct. It’s now working on Android. Thanks so much! Previously, I was hewing too closely to the example I found in the docs, and I assumed the *.pinballmap.com was for covering www and non-www paths, similar to how associatedDomains accounts for both of those.

Next, onward to iOS!

Sweet, working on iOS, using /map/* in the AASA. So, a path like /map/?by_location_id=* in the AASA doesn’t work while the simpler /map/* does. Very strange. But at least we’re getting somewhere.

And I tried a few more paths in the intent filters, and got some more strange results. @cristian-nxtl In the Android intent filter, do you have any idea why this would work:

"host": "pinballmap.com",
              "pathPrefix": "/"

but this does not:

"host": "pinballmap.com",
              "pathPrefix": "/*"

It doesn’t seem to like the trailing asterisk, but I don’t understand why.

I was looking at the Android documentation on the android:path formats:

The pathPrefix is (for lack of a better description) just a static string that the path must begin with. It does not treat any pattern matching expressions (like the * (0+ occurrences of the character preceding it) and . (0 or more occurrences of any character) ) and treats them as exact characters. Instead, you should probably use the pathPattern key if you want to use these pattern expressions.

As for the /map/?by_location_id=* link not working, I have just remembered the format that Apple used in their iOS13+ deep linking documentation:

{
	"/": "/help/*",
	"?": { "articleNumber": "????" },
	"comment": "Matches any URL whose path starts with /help/ and which has a query item with name 'articleNumber' and a value of exactly 4 characters"
}

I think that your by_location_id portion should be placed in the ? (query params) key of the configuration, with the * value:

{
	"/": "/map/*",
	"?": { "by_location_id": "*" },
	"comment": "Matches any URL whose path starts with /map and which has a query item with name 'by_location_id' and any value"
}

A configuration with only the path portion /map/* (no query params) will match such path regardless of whether it has any query params, fragments, or not - which is why your /map/* path works.
Please note however that the link passed to the app will still contain any query params and fragments, regardless of whether you specify them or not in the AASA configuration (e.g. /map/something?other=123 will match against the path /map/* and will pass through the query param other with the value 123).
Specifying any specific query params or fragments in the config is only necessary if you want to treat such links in a different manner (e.g., treat /map/search?by_name=new+york differently from /map/search?by_location_id=123) - or if you want to make sure that the link has a specific format that you know how to handle (e.g. if you specify the by_location_id query param, the path /map/search?whatever=not+a+value will not match it, and will not trigger the app on that specific path config).

1 Like

You are my hero.

I haven’t dug into the AASA configuration that you’re sharing. But this makes sense, and I’m glad there is support in the AASA for queries. Indeed we do want to treat queries differently (such as your examples of a direct location query, and then a geographical query).

However, for Android, I’m not seeing similar support for queries in the Intent Filters. Closest I found was sspPrefix, which doesn’t seem supported by Expo (I get a warning when I add one).

It took me a while to realize that the ?by_location_id portion of my url was technically not part of the path, but was instead a query.

What I didn’t explain about my urls is that we have pages, like /store and /faq, and then we have not only our map page with the queries (/map/?..), but also “regional maps” with queries, like /portland/?.. and /seattle/?..

If only I could simply say "anything that has /map/ should be universally linked, but otherwise do not. But I can’t, because we have about 100 of those regional map paths! And since the android intent filters do not support queries, “/portland/?..” is not any different than “/store” (because the queries always begin right after the /). So for Android, I think I just have to use / as the path, and then deal with “not found” pages on the app end of things.

You are my hero.

Glad to be of help :slight_smile:

As for the /map/<region> query, I think the best you can do is accept all links to /map, and run a regex to validate whether there exists any regional subpath:

The following regex only matches against URLs with a single subpath after /map:
/.+\/map\/(\w+)/i

The next one instead, matches against a any number of subpaths after /map:
/.+\/map((?:\/\w+)+)/i

Regexes tested on regex101.com - ignore the /gm flags in the screenshots, I used them to test against multiple URLs at once. For your needs, you may only need the /i (case insensitive) flag or no flag at all

Both the regexes above:

  • will not match (will return null) if there is no subpath after /map.
  • if they match, they will return the subpath (after /map) in the regex results at index 1
  • They use .+to match against the domain part, but please note that this will also match against invalid cases such as domain.com/archives/map/something. If you’d like to make sure this doesn’t happen, use them with the full domain string:
    /https:\/\/pinballmap\.com\/map\/(\w+)/i
    /https:\/\/pinballmap\.com\/map((?:\/\w+)+)

If you’re using React Navigation, you could make a custom linking config. But this most likely involves using the React Navigation links Advanced cases, which I find quite hard. Maybe, using React Native’s Linking module directly, and manually navigating to the proper screens is a better idea in this case.