Skip to content

Commit

Permalink
Properly callback URL launch results for iOS
Browse files Browse the repository at this point in the history
  • Loading branch information
droibit committed Dec 24, 2023
1 parent 7961c62 commit 99ce4ae
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 12 deletions.
2 changes: 1 addition & 1 deletion flutter_custom_tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Expand Down
2 changes: 1 addition & 1 deletion flutter_custom_tabs/lib/src/launcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
/// }
/// ```
///
Expand Down
2 changes: 1 addition & 1 deletion flutter_custom_tabs/lib/src/launcher_lite.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> launchUrl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]()

Expand All @@ -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] = [:],
Expand All @@ -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)
}
}

Expand Down
28 changes: 24 additions & 4 deletions flutter_custom_tabs_ios/ios/Classes/CustomTabsPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,35 @@ public class CustomTabsPlugin: NSObject, FlutterPlugin, CustomTabsApi {
completion: @escaping (Result<Void, Error>) -> 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"
}
8 changes: 6 additions & 2 deletions flutter_custom_tabs_ios/ios/Classes/Laucher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down

0 comments on commit 99ce4ae

Please sign in to comment.