From 99ce4aeb9a72d798949c27cbf6b6e759464f7c47 Mon Sep 17 00:00:00 2001 From: Shinya Kumagai Date: Sun, 24 Dec 2023 18:29:51 +0900 Subject: [PATCH] Properly callback URL launch results for iOS --- flutter_custom_tabs/README.md | 2 +- flutter_custom_tabs/lib/src/launcher.dart | 2 +- .../lib/src/launcher_lite.dart | 2 +- .../customtabs/CustomTabsLauncher.java | 3 +- .../customtabs/CustomTabsLauncherTest.java | 9 ++++ .../RunnerTests/CustomTabsPluginTest.swift | 42 +++++++++++++++++++ .../ios/RunnerTests/MockLauncher.swift | 11 ++++- .../ios/Classes/CustomTabsPlugin.swift | 28 +++++++++++-- .../ios/Classes/Laucher.swift | 8 +++- 9 files changed, 95 insertions(+), 12 deletions(-) diff --git a/flutter_custom_tabs/README.md b/flutter_custom_tabs/README.md index fa254a21..5eb35dde 100644 --- a/flutter_custom_tabs/README.md +++ b/flutter_custom_tabs/README.md @@ -76,7 +76,7 @@ void _launchURL(BuildContext context) async { ), ); } catch (e) { - // An exception is thrown if browser app is not installed on Android device. + // If the URL launch fails, an exception will be thrown. (For example, if no browser app is installed on the Android device.) debugPrint(e.toString()); } } diff --git a/flutter_custom_tabs/lib/src/launcher.dart b/flutter_custom_tabs/lib/src/launcher.dart index 5c4068ce..9909e777 100644 --- a/flutter_custom_tabs/lib/src/launcher.dart +++ b/flutter_custom_tabs/lib/src/launcher.dart @@ -38,7 +38,7 @@ import 'package:flutter_custom_tabs_platform_interface/flutter_custom_tabs_platf /// ), /// ); /// } catch (e) { -/// // An exception is thrown if browser app is not installed on Android device. +/// // If the URL launch fails, an exception will be thrown. (For example, if no browser app is installed on the Android device.) /// } /// ``` /// diff --git a/flutter_custom_tabs/lib/src/launcher_lite.dart b/flutter_custom_tabs/lib/src/launcher_lite.dart index 0ec1067e..b46e7484 100644 --- a/flutter_custom_tabs/lib/src/launcher_lite.dart +++ b/flutter_custom_tabs/lib/src/launcher_lite.dart @@ -22,7 +22,7 @@ import 'types/launch_options.dart'; /// ), /// ); /// } catch (e) { -/// // An exception is thrown if browser app is not installed on Android device. +/// // If the URL launch fails, an exception will be thrown. (For example, if no browser app is installed on the Android device.) /// } @experimental Future launchUrl( diff --git a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java index d427932b..00552536 100644 --- a/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java +++ b/flutter_custom_tabs_android/android/src/main/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncher.java @@ -28,7 +28,8 @@ import static java.util.Objects.requireNonNull; class CustomTabsLauncher implements Messages.CustomTabsApi { - private static final String CODE_LAUNCH_ERROR = "LAUNCH_ERROR"; + @VisibleForTesting + static final String CODE_LAUNCH_ERROR = "LAUNCH_ERROR"; private static final int REQUEST_CODE_CUSTOM_TABS = 0; private final @NonNull CustomTabsFactory customTabsFactory; diff --git a/flutter_custom_tabs_android/android/src/test/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncherTest.java b/flutter_custom_tabs_android/android/src/test/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncherTest.java index a614e4b3..07846a97 100644 --- a/flutter_custom_tabs_android/android/src/test/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncherTest.java +++ b/flutter_custom_tabs_android/android/src/test/java/com/github/droibit/flutter/plugins/customtabs/CustomTabsLauncherTest.java @@ -59,6 +59,9 @@ public void launchWithoutActivity() { fail("error"); } catch (Exception e) { assertThat(e).isInstanceOf(FlutterError.class); + + final FlutterError actualError = ((FlutterError) e); + assertThat(actualError.code).isEqualTo(CustomTabsLauncher.CODE_LAUNCH_ERROR); } } @@ -122,6 +125,9 @@ public void launchExternalBrowserFailure() { fail("error"); } catch (Exception e) { assertThat(e).isInstanceOf(FlutterError.class); + + final FlutterError actualError = ((FlutterError) e); + assertThat(actualError.code).isEqualTo(CustomTabsLauncher.CODE_LAUNCH_ERROR); } } @@ -198,6 +204,9 @@ public void launchCustomTabsFailure() { fail("error"); } catch (Exception e) { assertThat(e).isInstanceOf(FlutterError.class); + + final FlutterError actualError = ((FlutterError) e); + assertThat(actualError.code).isEqualTo(CustomTabsLauncher.CODE_LAUNCH_ERROR); } } } \ No newline at end of file diff --git a/flutter_custom_tabs_ios/example/ios/RunnerTests/CustomTabsPluginTest.swift b/flutter_custom_tabs_ios/example/ios/RunnerTests/CustomTabsPluginTest.swift index a0f7a6a4..c2e99f44 100644 --- a/flutter_custom_tabs_ios/example/ios/RunnerTests/CustomTabsPluginTest.swift +++ b/flutter_custom_tabs_ios/example/ios/RunnerTests/CustomTabsPluginTest.swift @@ -19,6 +19,8 @@ final class CustomTabsPluginTest: XCTestCase { } func testPresentSFSafariViewController() { + launcher.setPresentCompletionHandlerResults(true) + let url = URL(string: "https://example.com")! let options = SFSafariViewControllerOptions() plugin.launchURL(url.absoluteString, prefersDeepLink: false, options: options) { result in @@ -33,6 +35,27 @@ final class CustomTabsPluginTest: XCTestCase { XCTAssertTrue(actualArgment.viewControllerToPresent is SFSafariViewController) } + func testFailedToPresentSFSafariViewController() { + launcher.setPresentCompletionHandlerResults(false) + + let url = URL(string: "https://example.com")! + let options = SFSafariViewControllerOptions() + plugin.launchURL(url.absoluteString, prefersDeepLink: false, options: options) { result in + if case let .failure(error) = result { + XCTAssertTrue(error is FlutterError) + let actualError = error as! FlutterError + XCTAssertEqual(actualError.code, FlutterError.erorCode) + } else { + XCTFail("error") + } + } + XCTAssertTrue(launcher.openArgumentStack.isEmpty) + XCTAssertEqual(launcher.presentArgumentStack.count, 1) + + let actualArgment = launcher.presentArgumentStack.first! + XCTAssertTrue(actualArgment.viewControllerToPresent is SFSafariViewController) + } + func testOpenExternalBrowser() throws { launcher.setOpenCompletionHandlerResults(true) @@ -47,6 +70,24 @@ final class CustomTabsPluginTest: XCTestCase { ]) } + func testFailedToOpenExternalBrowser() throws { + launcher.setOpenCompletionHandlerResults(false) + + let url = URL(string: "https://example.com")! + plugin.launchURL(url.absoluteString, prefersDeepLink: false, options: nil) { result in + if case let .failure(error) = result { + XCTAssertTrue(error is FlutterError) + let actualError = error as! FlutterError + XCTAssertEqual(actualError.code, FlutterError.erorCode) + } else { + XCTFail("error") + } + } + XCTAssertEqual(launcher.openArgumentStack, [ + .init(url: url, options: [:]), + ]) + } + // MARK: - Deep Linking func testDeepLinkToNativeApp() throws { @@ -82,6 +123,7 @@ final class CustomTabsPluginTest: XCTestCase { func testFallBackToSFSfariViewController() throws { launcher.setOpenCompletionHandlerResults(false) + launcher.setPresentCompletionHandlerResults(true) let url = URL(string: "https://example.com")! let options = SFSafariViewControllerOptions() diff --git a/flutter_custom_tabs_ios/example/ios/RunnerTests/MockLauncher.swift b/flutter_custom_tabs_ios/example/ios/RunnerTests/MockLauncher.swift index 64f9a6c0..de1a7f49 100644 --- a/flutter_custom_tabs_ios/example/ios/RunnerTests/MockLauncher.swift +++ b/flutter_custom_tabs_ios/example/ios/RunnerTests/MockLauncher.swift @@ -4,6 +4,7 @@ import Foundation class MockLauncher: Launcher { private var openCompletionHandlerResults = [Bool]() + private var presentCompletionHandlerResults = [Bool]() private(set) var openArgumentStack = [OpenArgument]() private(set) var presentArgumentStack = [PresentArgument]() @@ -13,6 +14,10 @@ class MockLauncher: Launcher { openCompletionHandlerResults.append(contentsOf: values) } + func setPresentCompletionHandlerResults(_ values: Bool...) { + presentCompletionHandlerResults.append(contentsOf: values) + } + override func open( _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], @@ -24,11 +29,13 @@ class MockLauncher: Launcher { completion?(opened) } - override func present(_ viewControllerToPresent: UIViewController, completion: (() -> Void)? = nil) { + override func present(_ viewControllerToPresent: UIViewController, completion: ((Bool) -> Void)? = nil) { presentArgumentStack.append( .init(viewControllerToPresent: viewControllerToPresent) ) - completion?() + + let presented = presentCompletionHandlerResults.removeFirst() + completion?(presented) } } diff --git a/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift b/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift index 61899973..8a1ee414 100644 --- a/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift +++ b/flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift @@ -46,15 +46,35 @@ public class CustomTabsPlugin: NSObject, FlutterPlugin, CustomTabsApi { completion: @escaping (Result) -> Void ) { guard let options else { - launcher.open(url) { _ in - completion(.success(())) + launcher.open(url) { opened in + if opened { + completion(.success(())) + } else { + completion(.failure( + FlutterError(message: "Failed to launch external browser.") + )) + } } return } let safariViewController = SFSafariViewController.make(url: url, options: options) - launcher.present(safariViewController) { - completion(.success(())) + launcher.present(safariViewController) { presented in + if presented { + completion(.success(())) + } else { + completion(.failure( + FlutterError(message: "Failed to launch SFSafariViewController.") + )) + } } } } + +extension FlutterError: Error { + convenience init(message: String) { + self.init(code: Self.erorCode, message: message, details: nil) + } + + static let erorCode = "LAUNCH_ERROR" +} diff --git a/flutter_custom_tabs_ios/ios/Classes/Laucher.swift b/flutter_custom_tabs_ios/ios/Classes/Laucher.swift index 4d21acb4..bedf93b0 100644 --- a/flutter_custom_tabs_ios/ios/Classes/Laucher.swift +++ b/flutter_custom_tabs_ios/ios/Classes/Laucher.swift @@ -11,13 +11,17 @@ open class Launcher { UIApplication.shared.open(url, options: options, completionHandler: completion) } - open func present(_ viewControllerToPresent: UIViewController, completion: (() -> Void)? = nil) { + open func present(_ viewControllerToPresent: UIViewController, completion: ((Bool) -> Void)? = nil) { if let topViewController = UIWindow.keyWindow?.topViewController() { dismissStack.append { [weak viewControllerToPresent] in viewControllerToPresent?.dismiss(animated: true) } topViewController - .present(viewControllerToPresent, animated: true, completion: completion) + .present(viewControllerToPresent, animated: true) { + completion?(true) + } + } else { + completion?(false) } }