Universal Link only redirecting to the App under specific circumstances

The Problem:

I am building an app that uses PayPal for payments, and I set up universal links so that the user gets redirected into the app after the user has confirmed the payment at PayPal's site.

Universal links are working in general. But here is what I have discovered:

The following scenario works:

  1. The app opens the nextActionUrl from PayPal using SFSafariViewController.
  2. User has to log in and presses the login button.
  3. User presses the confirmation button on the PayPal site.
  4. PayPal redirects to the return_url that was provided when creating the payment intent.
  5. The browser closes, and the user gets redirected into the app.

Now when the user has just recently logged into PayPal then PayPal does not show the login page, instead the user has to only press one button. This scenario fails:

  1. The app opens the nextActionUrl from PayPal using SFSafariViewController.
  2. User does not have to log in and presses the confirmation button on PayPal site.
  3. PayPal redirects to the return_url that was provided when creating the payment intent.
  4. The browser opens the website behind the universal link instead of redirecting to the app.

This scenario also works:

  1. The app opens the nextActionUrl from PayPal using SFSafariViewController.
  2. User does not have to log in, but actively logs out.
  3. User has to log in and presses the login button.
  4. User presses the confirmation button on the PayPal site.
  5. PayPal redirects to the return_url that was provided when creating the payment intent.
  6. The browser closes, and the user gets redirected into the app.

This happens consistently and reproducible.

Some additional details:

  1. I am developing the App using Flutter
  2. I set up the Runner.entitlements as follows:
...
<key>com.apple.developer.associated-domains</key>
<array>
	<string>applinks:sub.example.com</string>
</array>
...
  1. I set up the Info.plist as follows for deep links with different schema:
...
<key>CFBundleURLTypes</key>
<array>
	<dict>
		<key>CFBundleTypeRole</key>
		<string>Editor</string>
		<key>CFBundleURLName</key>
		<string>sub.example.com</string>
		<key>CFBundleURLSchemes</key>
		<array>
			<string>web+example</string>
		</array>
	</dict>
</array>
...
  1. I set up the apple-app-site-association file on the website at https://sub.example.com/.well-known/apple-app-site-association as follows:
{
  "activitycontinuation": {},
  "webcredentials": {},
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "XXXXXXXXXX.com.example.example",
        "paths": [ "/*" ]
      }
    ]
  }
}
  1. To open the urls from Flutter I am using the package url_launcher and simply call await launchUrl(nextActionUrl)

What I've tried and what I've learned:

I tried many things, before I discovered this consistent behavior. For example:

  1. I connected my device to the mac and opened the console for it in xcode to see how the logs look like when it works and how it looks like when it does not work. I found out that when it works there is a log entry that requests the switch to my App from the SpringBoard-process: [Received trusted open application request for <applicationId> from <FBProcess: ...], but in the cases where it does not work there isn't a rejected request or anything. The SafariViewController does not even seem to try to go the universal link route and instead opens the link as a website directly.
  2. For even more logs, I pressed and hold the volume buttons and clicked the lock screen button until a haptic feedback occurred. After that, I extracted the syslogs that were generated from the settings/privacy section via AirDrop on to the mac. I took a look at the swcutil_show.txt file and the universal link is definitely set up correctly there. I also skimmed around in the other logs, but I couldn't find anything helpful.
  3. I have inspected the url_launcher package to see how it works for iOS and if there may be something wrong, but I didn't really find a problem. I only discovered that the package is using the SFSafariViewController and after some googling I've read the WKWebView would be more suitable, but it didn't seem to be the problem.
  4. I googled a lot and found many problems related with setting up universal links correctly, but no topics about universal links sometimes working and sometimes not. The only thing close I found was that a universal link won't redirect you to your app if you type it into safari directly but if you click it for example from an email.

The work-a-round that works...

After I confirmed that it consistently works after logging in first, my theory was that it needs at least one navigation between opening the PayPal site in the browser and PayPal redirecting to the universal link.

To confirm my theory, I created an intermediate page that redirects the user immediately after the load event to the PayPal site, and I was disappointed. It didn't change anything.

Then I thought: Maybe it's the User-Interaction that is needed, and I change the intermediate page to only redirect if the user presses a button. And from there on, redirecting to the App using a universal link always worked.

But this isn't a nice solution - at max, this is an ugly work-a-round that may help someone.

My question(s)

Now I am not sure if this is a bug on Apple's side since I didn't find anything on the web or maybe even something strange on PayPal's side?

Am I doing something wrong?

Did someone experience the same thing and has a solution?

Thank you for your time and hopefully for your input on this!

Post not yet marked as solved Up vote post of HannTechGmbH Down vote post of HannTechGmbH
2.4k views

Replies

Hello HannTechGmbH,

Thank you for your time and investigation regarding Universal Links behaviours, it helped me solve a very similar problem when a mobile banking app wouldn't open on Chrome Browser unless user interaction is present.

I wasn't able to find a proper documentation that would describe these behaviours, but ChatGPT offered the following information:

Universal Links are a mechanism on iOS that allows web URLs to open the corresponding native app directly if it's installed on the device. The behavior of Universal Links is influenced by the origin of the event that triggers the link:

Trusted Events: When you tap on a link directly (a genuine user interaction), it's considered a trusted event. In the context of Universal Links, if the link you tapped on is associated with an app installed on your device, iOS will open the app directly instead of the web page in the browser.

Untrusted Events: If the navigation to a URL is triggered programmatically, such as via JavaScript (e.g., using window.location.href or a timed redirect), it's considered an untrusted event. In this case, even if the URL is associated with an app via Universal Links, iOS will not open the app. Instead, it will navigate to the web page in the browser. This is a security measure to prevent unexpected app launches without explicit user action.

The behavior you described fits this model:

  • When you're redirected to a web resource after a waiting time via JavaScript, it's an untrusted event, so the browser opens the web page in a new tab.
  • When you click on a link directly, it's a trusted event, so if the link is associated with an app via Universal Links, the app will open.

This distinction ensures that apps are only opened as a result of deliberate user actions, preventing potential misuse or unexpected behavior.

I know this is not an official reply from Apple, but it makes sense considering the log that you recorded in case the mobile app was opening:

[Received trusted open application request for <applicationId> from <FBProcess: ...]

Anyways, I hope this piece of information would help other developers with similar use-cases.

Since I'm unsure of how PayPal login/confirmation works exactly, I'm not sure how much this information will help you but I can provide some info.

As referenced in TN3155 Debugging Universal Links, Safari will always navigate to the next page on button click if the subdomain does not change. Safari views any click within Safari as navigation unless the subdomain or domain changes, in which case it will see the universal link (if there is one) and redirect to the app.

That being said, if PayPal changes the subdomain when the user logs in, and then changes it again when they press the confirmation button this could explain why the user needs to log in for your universal link to work. It could be the case that the subdomain is not being changed if they are already logged in.

One thing you can try could be to have sub.example.com redirect to sub2.example.com. Then, host an AASA file at https://sub2.example.com/.well-known/apple-app-site-association and use applinks:sub2.example.com as your universal link. This way, Safari may see the change in subdomain and open in your app.

Let me know the behavior if you do try this or if you have solved this in another way other than your workaround!