Repository navigation
Remove manual Safari fallback for unhandled universal links - #185430
Conversation
There was a problem hiding this comment.
Code Review
This pull request modifies the iOS platform code to prevent unhandled deep links from being relayed to the system during a cold start. It introduces a hasBeenActive property in FlutterAppDelegate to track the application's lifecycle state, ensuring that unhandled URLs are only passed back to iOS once the app has been active. Additionally, FlutterSceneLifeCycle was updated to disable relaying during initial scene connection, and new test cases were added to verify the logic for both cold and warm start scenarios. I have no feedback to provide.
|
Huh, did you verify that the bug was a regression? Like did it work with an older version of Flutter? I would not expect the app launch sequence to be related if it's related to a specific Flutter version (unless it was UIScene related, but that was introduced in Flutter 3.38). Were you able to recreate locally and verify that this fixed it? |
|
@vashworth I didn't test prior Flutter versions since it was bisected to #164247 and I was able to reproduce the bug locally. I also verified that the fix works The fix was applied to both the Appdelegate and the UIScene since they both have their process for handling the deeplink |
| // response does not mean the link is genuinely unsupported — it may just be too early. Only | ||
| // relay unhandled https links back to iOS (which opens Safari) once the app has been active. | ||
| // | ||
| // Trade-off: if the app genuinely can't handle the route on cold start (e.g., AASA claims |
There was a problem hiding this comment.
Apple App Site Association. It basically tells ios to open the app instead of the website. With this approach, If a user taps on the link and it genuinely fails on a cold start either because it isnt ready or the app doesnt have the route, it simply takes the user to the app(usually the home screen) which i think is the lesser evil
@okorohelijah Can you try reverting to the old API and see if it works (it's one line of code so you don't have to bisect)? From your PR description this doesn't seem related to that change. |
|
@hellohuanlin I tested by reverting my change and #164247 separately: If I revert #164247 (returning to the deprecated The issue is that
When that happens, Dart's |
What do you mean by that? Does using the deprecated 1-arg openURL work? |
|
@vashworth Yes, the deprecated API "works" in the sense that it doesn't bounce to Safari, but it does so by failing silently. When the engine tries to relay back to the system via the legacy [UIApplication openURL:url], iOS sees the app is in the middle of a cold-start transition and simply doesn't execute the command. |
But it doesn't route the deeplink, yes? It opens the app but just to the main screen? |
|
@vashworth the link routing opens because the routing is correct. The problem in this case is that the initialization is just late but because of the timeout, |
|
Okay I think I understand. Basically what this PR does is makes it so if a url is not handled by the flutter app, then on cold start it fails silently (keeping the app open and not redirecting to safari) and on warm start it redirects to safari. From the issue report, it sounds like the users have come to expect cold start to silently fail because on their older version of Flutter it would call deprecated I wonder how it works natively. I'm pretty sure that it does not bounce to safari like we do. The AASA should handle directing invalid urls to safari. @okorohelijah Can you look through the git history and see if you can figure out why we introduced relaying it to the system in the first place? |
|
@hellohuanlin @vashworth I discovered something new: The 3-second waitForFirstFrame timeout is NOT being hit! Here is the exact timeline of what happens: iOS wakes up the app and hands the Deep Link to the Engine. When |
|
I still propose this fix because Apple's native design pattern for continueUserActivity allows native apps to return false if they receive a link they aren't ready for, and iOS natively handles this by silently dropping the link and letting the user stay inside the app. The implementation rn intercepts that false and forces a Safari bounce. wdyt? |
There was a problem hiding this comment.
Code Review
This pull request modifies the iOS platform integration to prevent unhandled universal links from being relayed back to the system during cold starts, as the Dart framework may not yet be ready to handle them. Relaying is now only enabled once the application has transitioned to an active state, tracked via a new hasBeenActive property. The feedback suggests observing UIApplicationDidBecomeActiveNotification rather than relying on applicationDidBecomeActive: to ensure robustness if subclasses override the method without calling super.
@vashworth I looked through the history (specifically PR #52643) to trace why this manual I tested by creating a native app and I confirm that the invalid urls(custom and universal links) just remain on the app instead of the safari bounce. The safari bounce happens only if the app doesnt open in the first place. That is, the path isnt registered in the AASA but once the app opens iOS handsoff completely |
hellohuanlin
left a comment
There was a problem hiding this comment.
So this turns out to be unrelated to #164247?
There was a problem hiding this comment.
@okorohelijah Since you determined that native apps don't bounce to Safari, shouldn't we just remove the throw back altogether?
There was a problem hiding this comment.
Yep! and just too add for future reference: the modern API (UISceneDelegate) changed the continueUserActivity return type to void, so iOS no longer receives feedback on whether the link was handled and natively stopped bouncing to Safari.
The old legacy API (UIApplicationDelegate) returned a BOOL. Because Flutter had to return YES synchronously to the system while waiting for Dart, PR #52643 originally added this manual openURL: fallback to emulate Apple's old behavior if Dart eventually returned false.
Since modern iOS has abandoned the Safari fallback natively, I completely agree we should just remove our engine-level fallback entirely.
|
Thanks a lot for the detailed clarification, that's crystal clear! I see the PR is on its way to being merged, so perfect. 🙏 |
|
The PR description should be updated to reflect changes -- e.g. |
cbracken
left a comment
There was a problem hiding this comment.
Overall lgtm -- just a few nits.
|
autosubmit label was removed for flutter/flutter/185430, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label. |

When a universal link (https) opens an iOS app on a cold start, the Dart framework may not be ready to handle routes yet — onGenerateRoute can return null simply because initialization hasn't completed. The engine interprets this as "unhandled" and relays the URL back to iOS via openURL:, which opens Safari. This is incorrect and disruptive.
This PR completely removes the relay-to-system fallback for unhandled universal links (for both warm and cold starts). The modern iOS API for scenes (
UISceneDelegate continueUserActivity) returnsvoid, meaning iOS natively no longer expects feedback on whether a link was handled, and natively stopped bouncing unrecognized universal links to Safari. Removing our manual[UIApplication openURL:]fallback aligns Flutter's engine with Apple's modern native behavior.PR with the help of Gemini
Fixes #170665
If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.