Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[url_launcher] When not fully loaded, clicking close causes the callback to not be triggered correctly. #8582

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion packages/url_launcher/url_launcher_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## NEXT
## 6.3.3

* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
* Ensures the completion callback is invoked if the user dismisses the Safari view before the initial URL load completes.

## 6.3.2

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@ final class URLLauncherTests: XCTestCase {
XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true)
}

func testLaunchSafariViewControllerWithClose() {
let launcher = FakeLauncher()
let plugin = createPlugin(launcher: launcher)

let expectation = XCTestExpectation(description: "completion called")
plugin.openUrlInSafariViewController(url: "https://flutter.dev") { result in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that this test would flake if https://flutter.dev loads before the test calls closeSafariViewController. Is there some URL we could test that is known to hang forever? Or am I overthinking this? cc @stuartmorgan

Copy link
Contributor

@stuartmorgan stuartmorgan Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not expect it to be possible for that to happen. Unless SFSafariViewController's didCompleteInitialLoad callback happens on a non-main thread (which I wouldn't expect since it passes the view controller as a parameter), it shouldn't be possible for the callback to trigger until the runloop runs again, which I would think could not happen before the wait call.

There are some assumptions in there, so I wouldn't be confident in saying that it's impossible, but I'd be fine with waiting to see if it does actually happen before we add a bunch of extra logic (since the reliable way to avoid that would be dependency injection of a fake SFSafariViewController that we know won't load anything) to avoid I case that I strongly suspect is in fact impossible.

switch result {
case .success(let details):
XCTAssertEqual(details, .failedToLoad)
case .failure(let error):
XCTFail("Unexpected error: \(error)")
}
expectation.fulfill()
}
plugin.closeSafariViewController()
wait(for: [expectation], timeout: 30)
Copy link
Member

@loic-sharma loic-sharma Feb 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this timeout: 30 necessary or is timeout: 1 sufficient?

From my understanding plugin.closeSafariViewController() calls the completion callback, which in turn calls expectation.fulfill(). The expectation should be already be fulfilled at this point.

I apologize if this is a silly question, I'm not very familiar with XCTest.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our standard for expectation timeouts in the repo is 30 (older code still uses lower timeouts). Flutter style is not to have timeouts at all but that's awkward with XCTest APIs, so we just use a value that is absurdly long for unit tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under normal circumstances, wait(for: [expectation], timeout: 30) won’t actually wait, because the closure has already been triggered before it. The 30-second timeout is set to prevent accidentally removing this PR functionality. If this PR functionality is deleted, setting the wait time to 1 might not make sense, as the callback may never be reached for verification.

}

}

final private class FakeLauncher: NSObject, Launcher {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate {

private let completion: OpenInSafariCompletionHandler
private let url: URL
private var isLoadCompleted: Bool = false

/// The Safari view controller used for displaying the URL.
let safariViewController: SFSafariViewController
Expand Down Expand Up @@ -46,12 +47,16 @@ final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate {
} else {
completion(.success(.failedToLoad))
}
isLoadCompleted = true
}

/// Called when the user finishes using the Safari view controller.
///
/// - Parameter controller: The Safari view controller.
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
if !isLoadCompleted {
completion(.success(.failedToLoad))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is .failedToLoad the correct result here? Or we should add a new InAppLoadResult.dismissed result?

If this is the correct result, should we update this comment to indicate this case can happen if the user dismisses the Safari view before the initial load completes?

/// The URL did not load successfully.
failedToLoad,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first, I did consider adding a new InAppLoadResult to describe the close action, but it seems it could lead to confusion among developers. This is because, currently, after a successful load, clicking the close button does not trigger any callback.

Additionally, when the user clicks close before the loading is complete, triggering the failedToLoad callback seems reasonable in a broader sense, as the interruption of the loading process can be considered a loading failure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is the correct result, should we update this comment to indicate this case can happen if the user dismisses the Safari view before the initial load completes?

Do you think it’s possible to modify the comment like this:

// The URL did not load successfully (or close the SFSafariViewController earlier)

}
controller.dismiss(animated: true, completion: nil)
didFinish?()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/url_launcher/url_launcher_ios/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: url_launcher_ios
description: iOS implementation of the url_launcher plugin.
repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
version: 6.3.2
version: 6.3.3

environment:
sdk: ^3.4.0
Expand Down