From 1d5609f71c36311c4e0a8de3f4ec761204cfefd7 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Thu, 17 Aug 2023 21:19:53 -0500 Subject: [PATCH 01/29] converting url_launcher from objc to swift --- .../ios/Runner.xcodeproj/project.pbxproj | 58 ++++- .../ios/RunnerTests/URLLauncherTests.swift | 62 ++++++ .../ios/Classes/FLTURLLauncherPlugin.h | 10 - .../ios/Classes/FLTURLLauncherPlugin.m | 202 ------------------ .../ios/Classes/FLTURLLauncherPlugin_Test.h | 11 - .../ios/Classes/FULLauncher.h | 19 -- .../ios/Classes/Launcher.swift | 18 ++ .../ios/Classes/URLLaunchSession.swift | 40 ++++ .../ios/Classes/URLLauncherPlugin.swift | 111 ++++++++++ .../ios/Classes/URLLauncherPlugin_Test.swift | 0 .../url_launcher_ios/ios/Classes/messages.g.h | 40 ---- .../url_launcher_ios/ios/Classes/messages.g.m | 126 ----------- .../ios/Classes/messages.g.swift | 127 +++++++++++ .../ios/url_launcher_ios.podspec | 8 +- .../url_launcher_ios/lib/src/messages.g.dart | 12 +- .../url_launcher_ios/pigeons/messages.dart | 4 +- 16 files changed, 419 insertions(+), 429 deletions(-) create mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift delete mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h delete mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m delete mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin_Test.h delete mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/FULLauncher.h create mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift create mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift create mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift create mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin_Test.swift delete mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.h delete mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.m create mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index d61abc72446..d8ee82f5906 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1D9FF3342A90170000F89C85 /* URLLauncherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */; }; 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -51,6 +52,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLLauncherTests.swift; sourceTree = ""; }; 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -189,6 +191,7 @@ children = ( F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */, F7151F4C26604CFB0028CB91 /* Info.plist */, + 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -274,11 +277,10 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = S8QB4VV633; }; F7151F4726604CFB0028CB91 = { CreatedOnToolsVersion = 12.5; - ProvisioningStyle = Automatic; + LastSwiftMigration = 1430; TestTargetID = 97C146ED1CF9000F007C117D; }; F7151F5526604D060028CB91 = { @@ -351,7 +353,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -366,7 +368,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -426,6 +428,7 @@ buildActionMask = 2147483647; files = ( F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */, + 1D9FF3342A90170000F89C85 /* URLLauncherTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -583,13 +586,17 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -604,13 +611,17 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -625,12 +636,22 @@ baseConfigurationReference = 666BCD7C181C34F8BE58929B /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Debug; @@ -640,12 +661,21 @@ baseConfigurationReference = D25C434271ACF6555E002440 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Release; @@ -655,7 +685,11 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -668,7 +702,11 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift new file mode 100644 index 00000000000..5cb3eb27607 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import XCTest + +@testable import url_launcher_ios + +class URLLauncherTests: XCTestCase { + var plugin: URLLauncherPlugin! + + override func setUp() { + let launcher = FakeLauncher() + plugin = URLLauncherPlugin(launcher: launcher) + } + + func testCanLaunchSuccess() { + let result = try? plugin.canLaunchUrl(url: "good://url") + + XCTAssertTrue(result!) + } + + func testCanLaunchFailure() { + let result = try? plugin.canLaunchUrl(url: "bad://url") + + XCTAssertFalse(result!) + } + + func testCanLaunchFailureWithInvalidURL() { + XCTAssertThrowsError(try plugin.canLaunchUrl(url: "not a url")) + } + + func testLaunchSuccess() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + +} + +class FakeLauncher: Launcher { + var passedOptions: [UIApplication.OpenExternalURLOptionsKey: Any]? + + func canOpenURL(_ url: URL) -> Bool { + return url.scheme == "good" + } + + func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], completionHandler: ((Bool) -> Void)? = nil) { + self.passedOptions = options + completionHandler?(url.scheme == "good") + } +} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h deleted file mode 100644 index 7b3480e3d47..00000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -#import "messages.g.h" - -@interface FLTURLLauncherPlugin : NSObject -@end diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m deleted file mode 100644 index 5d6a75f97aa..00000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -#import "FLTURLLauncherPlugin.h" -#import "FLTURLLauncherPlugin_Test.h" -#import "FULLauncher.h" -#import "messages.g.h" - -typedef void (^OpenInSafariVCResponse)(NSNumber *_Nullable, FlutterError *_Nullable); - -@interface FLTURLLaunchSession : NSObject - -@property(copy, nonatomic) OpenInSafariVCResponse completion; -@property(strong, nonatomic) NSURL *url; -@property(strong, nonatomic) SFSafariViewController *safari; -@property(nonatomic, copy) void (^didFinish)(void); - -@end - -@implementation FLTURLLaunchSession - -- (instancetype)initWithURL:url completion:completion { - self = [super init]; - if (self) { - self.url = url; - self.completion = completion; - self.safari = [[SFSafariViewController alloc] initWithURL:url]; - self.safari.delegate = self; - } - return self; -} - -- (void)safariViewController:(SFSafariViewController *)controller - didCompleteInitialLoad:(BOOL)didLoadSuccessfully { - if (didLoadSuccessfully) { - self.completion(@YES, nil); - } else { - self.completion( - nil, [FlutterError - errorWithCode:@"Error" - message:[NSString stringWithFormat:@"Error while launching %@", self.url] - details:nil]); - } -} - -- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { - [controller dismissViewControllerAnimated:YES completion:nil]; - self.didFinish(); -} - -- (void)close { - [self safariViewControllerDidFinish:self.safari]; -} - -@end - -#pragma mark - - -/// Default implementation of FULLancher, using UIApplication. -@interface FULUIApplicationLauncher : NSObject -@end - -@implementation FULUIApplicationLauncher -- (BOOL)canOpenURL:(nonnull NSURL *)url { - return [[UIApplication sharedApplication] canOpenURL:url]; -} - -- (void)openURL:(nonnull NSURL *)url - options:(nonnull NSDictionary *)options - completionHandler:(void (^_Nullable)(BOOL))completion { - [[UIApplication sharedApplication] openURL:url options:options completionHandler:completion]; -} - -@end - -#pragma mark - - -@interface FLTURLLauncherPlugin () - -@property(strong, nonatomic) FLTURLLaunchSession *currentSession; -@property(strong, nonatomic) NSObject *launcher; - -@end - -@implementation FLTURLLauncherPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init]; - FULUrlLauncherApiSetup(registrar.messenger, plugin); -} - -- (instancetype)init { - return [self initWithLauncher:[[FULUIApplicationLauncher alloc] init]]; -} - -- (instancetype)initWithLauncher:(NSObject *)launcher { - if (self = [super init]) { - _launcher = launcher; - } - return self; -} - -- (nullable NSNumber *)canLaunchURL:(NSString *)urlString - error:(FlutterError *_Nullable *_Nonnull)error { - NSURL *url = [NSURL URLWithString:urlString]; - if (!url) { - *error = [self invalidURLErrorForURLString:urlString]; - return nil; - } - return @([self.launcher canOpenURL:url]); -} - -- (void)launchURL:(NSString *)urlString - universalLinksOnly:(NSNumber *)universalLinksOnly - completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion { - NSURL *url = [NSURL URLWithString:urlString]; - if (!url) { - completion(nil, [self invalidURLErrorForURLString:urlString]); - return; - } - NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly}; - [self.launcher openURL:url - options:options - completionHandler:^(BOOL success) { - completion(@(success), nil); - }]; -} - -- (void)openSafariViewControllerWithURL:(NSString *)urlString - completion:(OpenInSafariVCResponse)completion { - NSURL *url = [NSURL URLWithString:urlString]; - if (!url) { - completion(nil, [self invalidURLErrorForURLString:urlString]); - return; - } - self.currentSession = [[FLTURLLaunchSession alloc] initWithURL:url completion:completion]; - __weak typeof(self) weakSelf = self; - self.currentSession.didFinish = ^(void) { - weakSelf.currentSession = nil; - }; - [self.topViewController presentViewController:self.currentSession.safari - animated:YES - completion:nil]; -} - -- (void)closeSafariViewControllerWithError:(FlutterError *_Nullable *_Nonnull)error { - [self.currentSession close]; -} - -- (UIViewController *)topViewController { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // TODO(stuartmorgan) Provide a non-deprecated codepath. See - // https://github.com/flutter/flutter/issues/104117 - return [self topViewControllerFromViewController:[UIApplication sharedApplication] - .keyWindow.rootViewController]; -#pragma clang diagnostic pop -} - -/** - * This method recursively iterate through the view hierarchy - * to return the top most view controller. - * - * It supports the following scenarios: - * - * - The view controller is presenting another view. - * - The view controller is a UINavigationController. - * - The view controller is a UITabBarController. - * - * @return The top most view controller. - */ -- (UIViewController *)topViewControllerFromViewController:(UIViewController *)viewController { - if ([viewController isKindOfClass:[UINavigationController class]]) { - UINavigationController *navigationController = (UINavigationController *)viewController; - return [self - topViewControllerFromViewController:[navigationController.viewControllers lastObject]]; - } - if ([viewController isKindOfClass:[UITabBarController class]]) { - UITabBarController *tabController = (UITabBarController *)viewController; - return [self topViewControllerFromViewController:tabController.selectedViewController]; - } - if (viewController.presentedViewController) { - return [self topViewControllerFromViewController:viewController.presentedViewController]; - } - return viewController; -} - -/** - * Creates an error for an invalid URL string. - * - * @param url The invalid URL string - * @return The error to return - */ -- (FlutterError *)invalidURLErrorForURLString:(NSString *)url { - return [FlutterError errorWithCode:@"argument_error" - message:@"Unable to parse URL" - details:[NSString stringWithFormat:@"Provided URL: %@", url]]; -} -@end diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin_Test.h b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin_Test.h deleted file mode 100644 index 112682a9469..00000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin_Test.h +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTURLLauncherPlugin.h" -#import "FULLauncher.h" - -/// APIs exposed for testing. -@interface FLTURLLauncherPlugin (Test) -- (instancetype)initWithLauncher:(NSObject *)launcher; -@end diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FULLauncher.h b/packages/url_launcher/url_launcher_ios/ios/Classes/FULLauncher.h deleted file mode 100644 index 63f8e04b66d..00000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FULLauncher.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -NS_ASSUME_NONNULL_BEGIN - -/// Protocol for UIApplication methods relating to launching URLs. -/// -/// This protocol exists to allow injecting an alternate implementation for testing. -@protocol FULLauncher -- (BOOL)canOpenURL:(NSURL *)url; -- (void)openURL:(NSURL *)url - options:(NSDictionary *)options - completionHandler:(void (^__nullable)(BOOL success))completion; -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift new file mode 100644 index 00000000000..1e697ebaa58 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +protocol Launcher { + func canOpenURL(_ url: URL) -> Bool + func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) +} + +class UIApplicationLauncher: Launcher { + func canOpenURL(_ url: URL) -> Bool { + return UIApplication.shared.canOpenURL(url) + } + + func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { + UIApplication.shared.open(url, options: options, completionHandler: completion) + } +} \ No newline at end of file diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift new file mode 100644 index 00000000000..332beb75f2a --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import SafariServices + +typealias OpenInSafariVCResponse = (Result) -> Void + +class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { + + private let completion: OpenInSafariVCResponse + private let url: URL + let safari: SFSafariViewController + var didFinish: (() -> Void)? + + init(url: URL, completion: @escaping OpenInSafariVCResponse) { + self.url = url + self.completion = completion + self.safari = SFSafariViewController(url: url) + super.init() + self.safari.delegate = self + } + + func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { + if didLoadSuccessfully { + completion(Result.success(true)) + } else { + completion(Result.failure(GeneralError(code: "Error", message: "Error while launching \(url)", details: nil))) + } + } + + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + controller.dismiss(animated: true, completion: nil) + didFinish?() + } + + func close() { + safariViewControllerDidFinish(safari) + } +} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift new file mode 100644 index 00000000000..375994f30c9 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter + +public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { + + public static func register(with registrar: FlutterPluginRegistrar) { + let plugin = URLLauncherPlugin() + registrar.publish(plugin) + } + + private var currentSession: URLLaunchSession? + private let launcher: Launcher + + init(launcher: Launcher = UIApplicationLauncher()) { + self.launcher = launcher + } + + func canLaunchUrl(url: String) throws -> Bool { + guard let url = URL(string: url) else { return false } + return launcher.canOpenURL(url) + } + + func launchUrl(url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) { + guard let url = URL(string: url) else { + completion(Result.failure(invalidURLError(for: url))) + return + } + let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] + launcher.openURL(url, options: options) { success in + completion(Result.success(success)) + } + } + + func openUrlInSafariViewController(url: String, completion: @escaping (Result) -> Void) { + guard let url = URL(string: url) else { + completion(Result.failure(invalidURLError(for: url))) + return + } + + currentSession = URLLaunchSession(url: url, completion: completion) + guard let session = currentSession else { return } + + session.didFinish = { [weak self] in + self?.currentSession = nil + } + topViewController?.present(session.safari, animated: true, completion: nil) + } + + func closeSafariViewController() { + currentSession?.close() + } + + var topViewController: UIViewController? { + // TODO(stuartmorgan) Provide a non-deprecated codepath. See + // https://github.com/flutter/flutter/issues/104117 + UIApplication.shared.keyWindow?.rootViewController?.topViewController + } + + /** + * Creates an error for an invalid URL string. + * + * @param url The invalid URL string + * @return The error to return + */ + + func invalidURLError(for url: String) -> Error { + GeneralError(code: "argument_error", message: "Unable to parse URL", details: "Provided URL: \(url)") + } +} + +/** + * This method recursively iterate through the view hierarchy + * to return the top most view controller. + * + * It supports the following scenarios: + * + * - The view controller is presenting another view. + * - The view controller is a UINavigationController. + * - The view controller is a UITabBarController. + * + * @return The top most view controller. + */ +extension UIViewController { + var topViewController: UIViewController { + if let navigationController = self as? UINavigationController { + return navigationController.viewControllers.last?.topViewController ?? navigationController.visibleViewController ?? navigationController + } + if let tabBarController = self as? UITabBarController { + return tabBarController.selectedViewController?.topViewController ?? tabBarController + } + if let presented = presentedViewController { + return presented.topViewController + } + return self + } +} + +class GeneralError: Error { + let code: String + let message: String + let details: String? + + init(code: String, message: String, details: String? = nil) { + self.code = code + self.message = message + self.details = details + } +} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin_Test.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin_Test.swift new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.h b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.h deleted file mode 100644 index 3a63e072271..00000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.h +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -#import - -@protocol FlutterBinaryMessenger; -@protocol FlutterMessageCodec; -@class FlutterError; -@class FlutterStandardTypedData; - -NS_ASSUME_NONNULL_BEGIN - -/// The codec used by FULUrlLauncherApi. -NSObject *FULUrlLauncherApiGetCodec(void); - -@protocol FULUrlLauncherApi -/// Returns true if the URL can definitely be launched. -/// -/// @return `nil` only when `error != nil`. -- (nullable NSNumber *)canLaunchURL:(NSString *)url error:(FlutterError *_Nullable *_Nonnull)error; -/// Opens the URL externally, returning true if successful. -- (void)launchURL:(NSString *)url - universalLinksOnly:(NSNumber *)universalLinksOnly - completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; -/// Opens the URL in an in-app SFSafariViewController, returning true -/// when it has loaded successfully. -- (void)openSafariViewControllerWithURL:(NSString *)url - completion: - (void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; -/// Closes the view controller opened by [openUrlInSafariViewController]. -- (void)closeSafariViewControllerWithError:(FlutterError *_Nullable *_Nonnull)error; -@end - -extern void FULUrlLauncherApiSetup(id binaryMessenger, - NSObject *_Nullable api); - -NS_ASSUME_NONNULL_END diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.m b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.m deleted file mode 100644 index 4a38efbe4f0..00000000000 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.m +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -#import "messages.g.h" -#import - -#if !__has_feature(objc_arc) -#error File requires ARC to be enabled. -#endif - -static NSArray *wrapResult(id result, FlutterError *error) { - if (error) { - return @[ - error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] - ]; - } - return @[ result ?: [NSNull null] ]; -} -static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { - id result = array[key]; - return (result == [NSNull null]) ? nil : result; -} - -NSObject *FULUrlLauncherApiGetCodec(void) { - static FlutterStandardMessageCodec *sSharedObject = nil; - sSharedObject = [FlutterStandardMessageCodec sharedInstance]; - return sSharedObject; -} - -void FULUrlLauncherApiSetup(id binaryMessenger, - NSObject *api) { - /// Returns true if the URL can definitely be launched. - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl" - binaryMessenger:binaryMessenger - codec:FULUrlLauncherApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(canLaunchURL:error:)], - @"FULUrlLauncherApi api (%@) doesn't respond to @selector(canLaunchURL:error:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSString *arg_url = GetNullableObjectAtIndex(args, 0); - FlutterError *error; - NSNumber *output = [api canLaunchURL:arg_url error:&error]; - callback(wrapResult(output, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - /// Opens the URL externally, returning true if successful. - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.UrlLauncherApi.launchUrl" - binaryMessenger:binaryMessenger - codec:FULUrlLauncherApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(launchURL:universalLinksOnly:completion:)], - @"FULUrlLauncherApi api (%@) doesn't respond to " - @"@selector(launchURL:universalLinksOnly:completion:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSString *arg_url = GetNullableObjectAtIndex(args, 0); - NSNumber *arg_universalLinksOnly = GetNullableObjectAtIndex(args, 1); - [api launchURL:arg_url - universalLinksOnly:arg_universalLinksOnly - completion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; - }]; - } else { - [channel setMessageHandler:nil]; - } - } - /// Opens the URL in an in-app SFSafariViewController, returning true - /// when it has loaded successfully. - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController" - binaryMessenger:binaryMessenger - codec:FULUrlLauncherApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(openSafariViewControllerWithURL:completion:)], - @"FULUrlLauncherApi api (%@) doesn't respond to " - @"@selector(openSafariViewControllerWithURL:completion:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSString *arg_url = GetNullableObjectAtIndex(args, 0); - [api openSafariViewControllerWithURL:arg_url - completion:^(NSNumber *_Nullable output, - FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; - }]; - } else { - [channel setMessageHandler:nil]; - } - } - /// Closes the view controller opened by [openUrlInSafariViewController]. - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController" - binaryMessenger:binaryMessenger - codec:FULUrlLauncherApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(closeSafariViewControllerWithError:)], - @"FULUrlLauncherApi api (%@) doesn't respond to " - @"@selector(closeSafariViewControllerWithError:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - [api closeSafariViewControllerWithError:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } -} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift new file mode 100644 index 00000000000..79e665197dd --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#else +#error("Unsupported platform.") +#endif + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)" + ] +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return (value as Any) as! T? +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol UrlLauncherApi { + /// Returns true if the URL can definitely be launched. + func canLaunchUrl(url: String) throws -> Bool + /// Opens the URL externally, returning true if successful. + func launchUrl(url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) + /// Opens the URL in an in-app SFSafariViewController, returning true + /// when it has loaded successfully. + func openUrlInSafariViewController(url: String, completion: @escaping (Result) -> Void) + /// Closes the view controller opened by [openUrlInSafariViewController]. + func closeSafariViewController() throws +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class UrlLauncherApiSetup { + /// The codec used by UrlLauncherApi. + /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { + /// Returns true if the URL can definitely be launched. + let canLaunchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", binaryMessenger: binaryMessenger) + if let api = api { + canLaunchUrlChannel.setMessageHandler { message, reply in + let args = message as! [Any] + let urlArg = args[0] as! String + do { + let result = try api.canLaunchUrl(url: urlArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + canLaunchUrlChannel.setMessageHandler(nil) + } + /// Opens the URL externally, returning true if successful. + let launchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.launchUrl", binaryMessenger: binaryMessenger) + if let api = api { + launchUrlChannel.setMessageHandler { message, reply in + let args = message as! [Any] + let urlArg = args[0] as! String + let universalLinksOnlyArg = args[1] as! Bool + api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + launchUrlChannel.setMessageHandler(nil) + } + /// Opens the URL in an in-app SFSafariViewController, returning true + /// when it has loaded successfully. + let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController", binaryMessenger: binaryMessenger) + if let api = api { + openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in + let args = message as! [Any] + let urlArg = args[0] as! String + api.openUrlInSafariViewController(url: urlArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + openUrlInSafariViewControllerChannel.setMessageHandler(nil) + } + /// Closes the view controller opened by [openUrlInSafariViewController]. + let closeSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController", binaryMessenger: binaryMessenger) + if let api = api { + closeSafariViewControllerChannel.setMessageHandler { _, reply in + do { + try api.closeSafariViewController() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + closeSafariViewControllerChannel.setMessageHandler(nil) + } + } +} diff --git a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec index 50c4ffad474..8ebc0f8a0c2 100644 --- a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec +++ b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec @@ -13,8 +13,12 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios' } s.documentation_url = 'https://pub.dev/packages/url_launcher' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.swift_version = '5.0' + s.source_files = 'Classes/**/*.swift' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } s.dependency 'Flutter' s.platform = :ios, '11.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index 562a408bd1c..41399a91cbd 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -54,8 +54,8 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_url, arg_universalLinksOnly]) as List?; + final List? replyList = + await channel.send([arg_url, arg_universalLinksOnly]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -81,8 +81,7 @@ class UrlLauncherApi { /// when it has loaded successfully. Future openUrlInSafariViewController(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', - codec, + 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_url]) as List?; @@ -112,7 +111,8 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; + final List? replyList = + await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart index f6935cbd882..7fdff617067 100644 --- a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart +++ b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart @@ -6,9 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - objcOptions: ObjcOptions(prefix: 'FUL'), - objcHeaderOut: 'ios/Classes/messages.g.h', - objcSourceOut: 'ios/Classes/messages.g.m', + swiftOut: 'ios/Classes/messages.g.swift', copyrightHeader: 'pigeons/copyright.txt', )) @HostApi() From d5727ddfdd74343fdb8d347497bad6ed73b52408 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 21:34:13 -0500 Subject: [PATCH 02/29] converting tests to swift --- .../ios/Runner.xcodeproj/project.pbxproj | 12 +- .../ios/RunnerTests/URLLauncherTests.m | 157 ------------------ .../ios/RunnerTests/URLLauncherTests.swift | 82 ++++++++- .../ios/RunnerUITests/URLLauncherUITests.m | 42 ----- .../ios/Classes/Launcher.swift | 4 +- .../ios/Classes/URLLauncherPlugin_Test.swift | 0 6 files changed, 81 insertions(+), 216 deletions(-) delete mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m delete mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m delete mode 100644 packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin_Test.swift diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index d8ee82f5906..5799416b610 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 1D9FF3342A90170000F89C85 /* URLLauncherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */; }; + 1DBE6EDE2A92479000B6525C /* URLLauncherUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBE6EDD2A92479000B6525C /* URLLauncherUITests.swift */; }; 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -17,8 +18,6 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B8140773523F70A044426500 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 487A1B5A2ECB3E406FD62FE3 /* libPods-RunnerTests.a */; }; - F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */; }; - F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F5826604D060028CB91 /* URLLauncherUITests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -53,6 +52,7 @@ /* Begin PBXFileReference section */ 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLLauncherTests.swift; sourceTree = ""; }; + 1DBE6EDD2A92479000B6525C /* URLLauncherUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLLauncherUITests.swift; sourceTree = ""; }; 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -74,10 +74,8 @@ A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; D25C434271ACF6555E002440 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; F7151F4826604CFB0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherTests.m; sourceTree = ""; }; F7151F4C26604CFB0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F7151F5626604D060028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F5826604D060028CB91 /* URLLauncherUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherUITests.m; sourceTree = ""; }; F7151F5A26604D060028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -189,7 +187,6 @@ F7151F4926604CFB0028CB91 /* RunnerTests */ = { isa = PBXGroup; children = ( - F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */, F7151F4C26604CFB0028CB91 /* Info.plist */, 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */, ); @@ -199,8 +196,8 @@ F7151F5726604D060028CB91 /* RunnerUITests */ = { isa = PBXGroup; children = ( - F7151F5826604D060028CB91 /* URLLauncherUITests.m */, F7151F5A26604D060028CB91 /* Info.plist */, + 1DBE6EDD2A92479000B6525C /* URLLauncherUITests.swift */, ); path = RunnerUITests; sourceTree = ""; @@ -427,7 +424,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */, 1D9FF3342A90170000F89C85 /* URLLauncherTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -436,7 +432,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */, + 1DBE6EDE2A92479000B6525C /* URLLauncherUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m deleted file mode 100644 index 64a17994b6f..00000000000 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import Flutter; -@import url_launcher_ios; -@import XCTest; - -@interface FULFakeLauncher : NSObject -@property(copy, nonatomic) NSDictionary *passedOptions; -@end - -@implementation FULFakeLauncher -- (BOOL)canOpenURL:(NSURL *)url { - return [url.scheme isEqualToString:@"good"]; -} - -- (void)openURL:(NSURL *)url - options:(NSDictionary *)options - completionHandler:(void (^__nullable)(BOOL success))completion { - self.passedOptions = options; - completion([url.scheme isEqualToString:@"good"]); -} -@end - -#pragma mark - - -@interface URLLauncherTests : XCTestCase -@end - -@implementation URLLauncherTests - -- (void)testCanLaunchSuccess { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - - FlutterError *error; - NSNumber *result = [plugin canLaunchURL:@"good://url" error:&error]; - - XCTAssertTrue(result.boolValue); - XCTAssertNil(error); -} - -- (void)testCanLaunchFailure { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - - FlutterError *error; - NSNumber *result = [plugin canLaunchURL:@"bad://url" error:&error]; - - XCTAssertNotNil(result); - XCTAssertFalse(result.boolValue); - XCTAssertNil(error); -} - -- (void)testCanLaunchInvalidURL { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - - FlutterError *error; - NSNumber *result = [plugin canLaunchURL:@"urls can't have spaces" error:&error]; - - XCTAssertNil(result); - XCTAssertEqualObjects(error.code, @"argument_error"); - XCTAssertEqualObjects(error.message, @"Unable to parse URL"); - XCTAssertEqualObjects(error.details, @"Provided URL: urls can't have spaces"); -} - -- (void)testLaunchSuccess { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - [plugin launchURL:@"good://url" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - XCTAssertTrue(result.boolValue); - XCTAssertNil(error); - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -- (void)testLaunchFailure { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - [plugin launchURL:@"bad://url" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - XCTAssertNotNil(result); - XCTAssertFalse(result.boolValue); - XCTAssertNil(error); - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -- (void)testLaunchInvalidURL { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - [plugin launchURL:@"urls can't have spaces" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - XCTAssertNil(result); - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.code, @"argument_error"); - XCTAssertEqualObjects(error.message, @"Unable to parse URL"); - XCTAssertEqualObjects(error.details, @"Provided URL: urls can't have spaces"); - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -- (void)testLaunchWithoutUniversalLinks { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - FlutterError *error; - [plugin launchURL:@"good://url" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; - XCTAssertNil(error); - XCTAssertFalse( - ((NSNumber *)launcher.passedOptions[UIApplicationOpenURLOptionUniversalLinksOnly]).boolValue); -} - -- (void)testLaunchWithUniversalLinks { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - FlutterError *error; - [plugin launchURL:@"good://url" - universalLinksOnly:@YES - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; - XCTAssertNil(error); - XCTAssertTrue( - ((NSNumber *)launcher.passedOptions[UIApplicationOpenURLOptionUniversalLinksOnly]).boolValue); -} - -@end diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index 5cb3eb27607..95159b19a5c 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -9,26 +9,29 @@ import XCTest class URLLauncherTests: XCTestCase { var plugin: URLLauncherPlugin! + var launcher: FakeLauncher! override func setUp() { - let launcher = FakeLauncher() - plugin = URLLauncherPlugin(launcher: launcher) + launcher = FakeLauncher() + plugin = URLLauncherPlugin(launcher: launcher, binaryMessenger: FakeFlutterBinaryMessenger()) } func testCanLaunchSuccess() { - let result = try? plugin.canLaunchUrl(url: "good://url") + let result = plugin.canLaunchUrl(url: "good://url") - XCTAssertTrue(result!) + XCTAssertTrue(result) } func testCanLaunchFailure() { - let result = try? plugin.canLaunchUrl(url: "bad://url") + let result = plugin.canLaunchUrl(url: "bad://url") - XCTAssertFalse(result!) + XCTAssertFalse(result) } func testCanLaunchFailureWithInvalidURL() { - XCTAssertThrowsError(try plugin.canLaunchUrl(url: "not a url")) + let result = plugin.canLaunchUrl(url: "urls can't have spaces") + + XCTAssertFalse(result) } func testLaunchSuccess() { @@ -46,6 +49,71 @@ class URLLauncherTests: XCTestCase { wait(for: [expectation], timeout: 1) } + func testLaunchFailure() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "bad://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertFalse(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testLaunchFailureWithInvalidURL() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in + switch result { + case .success(_): + XCTFail("Expected an error") + case .failure(let error): + let generalError = error as! GeneralError + XCTAssertEqual(generalError.code, "argument_error") + XCTAssertEqual(generalError.message, "Unable to parse URL") + XCTAssertEqual(generalError.details, "Provided URL: urls can't have spaces") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testLaunchWithoutUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, false) + } + + func testLaunchWithUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true) + } + } class FakeLauncher: Launcher { diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m deleted file mode 100644 index b6d3bceff03..00000000000 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import XCTest; -@import os.log; - -@interface URLLauncherUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication *app; -@end - -@implementation URLLauncherUITests - -- (void)setUp { - self.continueAfterFailure = NO; - - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testLaunch { - XCUIApplication *app = self.app; - - NSArray *buttonNames = @[ - @"Launch in app", @"Launch in app(JavaScript ON)", @"Launch in app(DOM storage ON)", - @"Launch a universal link in a native app, fallback to Safari.(Youtube)" - ]; - for (NSString *buttonName in buttonNames) { - XCUIElement *button = app.buttons[buttonName]; - XCTAssertTrue([button waitForExistenceWithTimeout:30.0]); - XCTAssertEqual(app.webViews.count, 0); - [button tap]; - XCUIElement *webView = app.webViews.firstMatch; - XCTAssertTrue([webView waitForExistenceWithTimeout:30.0]); - XCTAssertTrue([app.buttons[@"ForwardButton"] waitForExistenceWithTimeout:30.0]); - XCTAssertTrue(app.buttons[@"Share"].exists); - XCTAssertTrue(app.buttons[@"OpenInSafariButton"].exists); - [app.buttons[@"Done"] tap]; - } -} - -@end diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift index 1e697ebaa58..97beb74057f 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift @@ -9,10 +9,10 @@ protocol Launcher { class UIApplicationLauncher: Launcher { func canOpenURL(_ url: URL) -> Bool { - return UIApplication.shared.canOpenURL(url) + UIApplication.shared.canOpenURL(url) } func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { UIApplication.shared.open(url, options: options, completionHandler: completion) } -} \ No newline at end of file +} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin_Test.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin_Test.swift deleted file mode 100644 index e69de29bb2d..00000000000 From c4e55fbde482158282c0e6a8d99cabe0c30bb20f Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 21:34:29 -0500 Subject: [PATCH 03/29] converting tests to swift --- .../RunnerUITests/URLLauncherUITests.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift new file mode 100644 index 00000000000..fbe478435bc --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +class URLLauncherUITests: XCTestCase { + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + } + + func testLaunch() { + let app = self.app! + + let buttonNames: [String] = [ + "Launch in app", "Launch in app(JavaScript ON)", "Launch in app(DOM storage ON)", + "Launch a universal link in a native app, fallback to Safari.(Youtube)" + ] + for buttonName in buttonNames { + let button = app.buttons[buttonName] + XCTAssertTrue(button.waitForExistence(timeout: 30.0)) + XCTAssertEqual(app.webViews.count, 0) + button.tap() + let webView = app.webViews.firstMatch + XCTAssertTrue(webView.waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["ForwardButton"].waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.buttons["OpenInSafariButton"].exists) + app.buttons["Done"].tap() + } + } +} From 050d6b3f10a385643943da0af7f0380f8a04aaf4 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 21:36:27 -0500 Subject: [PATCH 04/29] fixing issue where pigeon and method channels were not setup right --- .../example/ios/RunnerTests/URLLauncherTests.swift | 12 ++++++++++++ .../ios/Classes/URLLauncherPlugin.swift | 8 +++++--- packages/url_launcher/url_launcher_ios/pubspec.yaml | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index 95159b19a5c..a77d8714fe2 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -128,3 +128,15 @@ class FakeLauncher: Launcher { completionHandler?(url.scheme == "good") } } + +class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { + func send(onChannel channel: String, message: Data?) { } + + func send(onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil) { } + + func setMessageHandlerOnChannel(_ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil) -> FlutterBinaryMessengerConnection { + 123 + } + + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) { } +} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index 375994f30c9..d51d6c533ff 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -7,18 +7,20 @@ import Flutter public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { public static func register(with registrar: FlutterPluginRegistrar) { - let plugin = URLLauncherPlugin() + let plugin = URLLauncherPlugin(binaryMessenger: registrar.messenger()) registrar.publish(plugin) } private var currentSession: URLLaunchSession? private let launcher: Launcher - init(launcher: Launcher = UIApplicationLauncher()) { + init(launcher: Launcher = UIApplicationLauncher(), binaryMessenger: FlutterBinaryMessenger) { self.launcher = launcher + super.init() + UrlLauncherApiSetup.setUp(binaryMessenger: binaryMessenger, api: self) } - func canLaunchUrl(url: String) throws -> Bool { + func canLaunchUrl(url: String) -> Bool { guard let url = URL(string: url) else { return false } return launcher.canOpenURL(url) } diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 9062954db63..0a48496c3a3 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -13,7 +13,7 @@ flutter: implements: url_launcher platforms: ios: - pluginClass: FLTURLLauncherPlugin + pluginClass: URLLauncherPlugin dartPluginClass: UrlLauncherIOS dependencies: From 5278199f5d305d7aeba716535a03461183e29bb0 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 21:46:06 -0500 Subject: [PATCH 05/29] formatting --- .../url_launcher_ios/lib/src/messages.g.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index 41399a91cbd..0c35d9a0c1e 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -54,8 +54,8 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_url, arg_universalLinksOnly]) as List?; + final List? replyList = await channel + .send([arg_url, arg_universalLinksOnly]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -81,7 +81,8 @@ class UrlLauncherApi { /// when it has loaded successfully. Future openUrlInSafariViewController(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', codec, + 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_url]) as List?; @@ -111,8 +112,7 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', From 55a345ba78b615ec308ef8a1e397a7794b6d4297 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 21:52:50 -0500 Subject: [PATCH 06/29] reverting unintentional local changes --- .../example/ios/Runner.xcodeproj/project.pbxproj | 12 ++---------- .../ios/Classes/URLLauncherPlugin.swift | 1 - 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index 5799416b610..b0a7480cfdb 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -274,10 +274,11 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = S8QB4VV633; }; F7151F4726604CFB0028CB91 = { CreatedOnToolsVersion = 12.5; - LastSwiftMigration = 1430; + ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; F7151F5526604D060028CB91 = { @@ -582,7 +583,6 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -607,7 +607,6 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -633,9 +632,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -645,8 +642,6 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; @@ -658,9 +653,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -670,7 +663,6 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index d51d6c533ff..67b7ac2e9ce 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -67,7 +67,6 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { * @param url The invalid URL string * @return The error to return */ - func invalidURLError(for url: String) -> Error { GeneralError(code: "argument_error", message: "Unable to parse URL", details: "Provided URL: \(url)") } From 8356d7a2d33da19e38fabddba63caae4d99877da Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 22:45:21 -0500 Subject: [PATCH 07/29] fixing issues with Ui tests --- .../example/ios/Runner.xcodeproj/project.pbxproj | 2 ++ .../url_launcher_ios/lib/src/messages.g.dart | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index b0a7480cfdb..dc5aefce899 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -681,6 +681,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Runner; }; name = Debug; @@ -698,6 +699,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Runner; }; name = Release; diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index 0c35d9a0c1e..41399a91cbd 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -54,8 +54,8 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel - .send([arg_url, arg_universalLinksOnly]) as List?; + final List? replyList = + await channel.send([arg_url, arg_universalLinksOnly]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -81,8 +81,7 @@ class UrlLauncherApi { /// when it has loaded successfully. Future openUrlInSafariViewController(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', - codec, + 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_url]) as List?; @@ -112,7 +111,8 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; + final List? replyList = + await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', From 120b2d4024a7f785c92ff14f1c412977af8ca3d6 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 23:01:00 -0500 Subject: [PATCH 08/29] formating --- .../url_launcher_ios/lib/src/messages.g.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index 41399a91cbd..0c35d9a0c1e 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -54,8 +54,8 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_url, arg_universalLinksOnly]) as List?; + final List? replyList = await channel + .send([arg_url, arg_universalLinksOnly]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -81,7 +81,8 @@ class UrlLauncherApi { /// when it has loaded successfully. Future openUrlInSafariViewController(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', codec, + 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_url]) as List?; @@ -111,8 +112,7 @@ class UrlLauncherApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send(null) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', From 7bd3553c7fdfd276925084cf3bcd2cf0fdb07fb0 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Mon, 21 Aug 2023 23:15:37 -0500 Subject: [PATCH 09/29] updating version and change log --- packages/url_launcher/url_launcher_ios/CHANGELOG.md | 3 ++- packages/url_launcher/url_launcher_ios/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index 58b88d23d5b..092a58d82f6 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 6.1.5 +* Migrates unit tests, UI test, pigeon files, and the plugin from objc to swift * Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. ## 6.1.4 diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 20ca4b62c74..c4a26a2129e 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -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.1.4 +version: 6.1.5 environment: sdk: ">=2.19.0 <4.0.0" From 2741de42e3447fd6cd7cdc124c099059a2949131 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Tue, 22 Aug 2023 12:16:27 -0500 Subject: [PATCH 10/29] converting tests to swift --- .../ios/Runner.xcodeproj/project.pbxproj | 20 +-- .../ios/RunnerTests/URLLauncherTests.m | 157 ------------------ .../ios/RunnerTests/URLLauncherTests.swift | 142 ++++++++++++++++ .../ios/RunnerUITests/URLLauncherUITests.m | 42 ----- .../RunnerUITests/URLLauncherUITests.swift | 37 +++++ .../ios/url_launcher_ios.podspec | 6 +- 6 files changed, 194 insertions(+), 210 deletions(-) delete mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m create mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift delete mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m create mode 100644 packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index d61abc72446..536da942f1c 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 1D9FF3342A90170000F89C85 /* URLLauncherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */; }; + 1DBE6EDE2A92479000B6525C /* URLLauncherUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBE6EDD2A92479000B6525C /* URLLauncherUITests.swift */; }; 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -16,8 +18,6 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B8140773523F70A044426500 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 487A1B5A2ECB3E406FD62FE3 /* libPods-RunnerTests.a */; }; - F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */; }; - F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F5826604D060028CB91 /* URLLauncherUITests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,6 +51,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLLauncherTests.swift; sourceTree = ""; }; + 1DBE6EDD2A92479000B6525C /* URLLauncherUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLLauncherUITests.swift; sourceTree = ""; }; 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -72,10 +74,8 @@ A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; D25C434271ACF6555E002440 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; F7151F4826604CFB0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherTests.m; sourceTree = ""; }; F7151F4C26604CFB0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F7151F5626604D060028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F5826604D060028CB91 /* URLLauncherUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLLauncherUITests.m; sourceTree = ""; }; F7151F5A26604D060028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -187,8 +187,8 @@ F7151F4926604CFB0028CB91 /* RunnerTests */ = { isa = PBXGroup; children = ( - F7151F4A26604CFB0028CB91 /* URLLauncherTests.m */, F7151F4C26604CFB0028CB91 /* Info.plist */, + 1D9FF3332A90170000F89C85 /* URLLauncherTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -196,8 +196,8 @@ F7151F5726604D060028CB91 /* RunnerUITests */ = { isa = PBXGroup; children = ( - F7151F5826604D060028CB91 /* URLLauncherUITests.m */, F7151F5A26604D060028CB91 /* Info.plist */, + 1DBE6EDD2A92479000B6525C /* URLLauncherUITests.swift */, ); path = RunnerUITests; sourceTree = ""; @@ -351,7 +351,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -366,7 +366,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -425,7 +425,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F4B26604CFB0028CB91 /* URLLauncherTests.m in Sources */, + 1D9FF3342A90170000F89C85 /* URLLauncherTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -433,7 +433,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F5926604D060028CB91 /* URLLauncherUITests.m in Sources */, + 1DBE6EDE2A92479000B6525C /* URLLauncherUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m deleted file mode 100644 index 64a17994b6f..00000000000 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import Flutter; -@import url_launcher_ios; -@import XCTest; - -@interface FULFakeLauncher : NSObject -@property(copy, nonatomic) NSDictionary *passedOptions; -@end - -@implementation FULFakeLauncher -- (BOOL)canOpenURL:(NSURL *)url { - return [url.scheme isEqualToString:@"good"]; -} - -- (void)openURL:(NSURL *)url - options:(NSDictionary *)options - completionHandler:(void (^__nullable)(BOOL success))completion { - self.passedOptions = options; - completion([url.scheme isEqualToString:@"good"]); -} -@end - -#pragma mark - - -@interface URLLauncherTests : XCTestCase -@end - -@implementation URLLauncherTests - -- (void)testCanLaunchSuccess { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - - FlutterError *error; - NSNumber *result = [plugin canLaunchURL:@"good://url" error:&error]; - - XCTAssertTrue(result.boolValue); - XCTAssertNil(error); -} - -- (void)testCanLaunchFailure { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - - FlutterError *error; - NSNumber *result = [plugin canLaunchURL:@"bad://url" error:&error]; - - XCTAssertNotNil(result); - XCTAssertFalse(result.boolValue); - XCTAssertNil(error); -} - -- (void)testCanLaunchInvalidURL { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - - FlutterError *error; - NSNumber *result = [plugin canLaunchURL:@"urls can't have spaces" error:&error]; - - XCTAssertNil(result); - XCTAssertEqualObjects(error.code, @"argument_error"); - XCTAssertEqualObjects(error.message, @"Unable to parse URL"); - XCTAssertEqualObjects(error.details, @"Provided URL: urls can't have spaces"); -} - -- (void)testLaunchSuccess { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - [plugin launchURL:@"good://url" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - XCTAssertTrue(result.boolValue); - XCTAssertNil(error); - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -- (void)testLaunchFailure { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - [plugin launchURL:@"bad://url" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - XCTAssertNotNil(result); - XCTAssertFalse(result.boolValue); - XCTAssertNil(error); - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -- (void)testLaunchInvalidURL { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - [plugin launchURL:@"urls can't have spaces" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - XCTAssertNil(result); - XCTAssertNotNil(error); - XCTAssertEqualObjects(error.code, @"argument_error"); - XCTAssertEqualObjects(error.message, @"Unable to parse URL"); - XCTAssertEqualObjects(error.details, @"Provided URL: urls can't have spaces"); - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; -} - -- (void)testLaunchWithoutUniversalLinks { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - FlutterError *error; - [plugin launchURL:@"good://url" - universalLinksOnly:@NO - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; - XCTAssertNil(error); - XCTAssertFalse( - ((NSNumber *)launcher.passedOptions[UIApplicationOpenURLOptionUniversalLinksOnly]).boolValue); -} - -- (void)testLaunchWithUniversalLinks { - FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init]; - FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher]; - XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; - - FlutterError *error; - [plugin launchURL:@"good://url" - universalLinksOnly:@YES - completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { - [resultExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:5 handler:nil]; - XCTAssertNil(error); - XCTAssertTrue( - ((NSNumber *)launcher.passedOptions[UIApplicationOpenURLOptionUniversalLinksOnly]).boolValue); -} - -@end diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift new file mode 100644 index 00000000000..a77d8714fe2 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -0,0 +1,142 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import XCTest + +@testable import url_launcher_ios + +class URLLauncherTests: XCTestCase { + var plugin: URLLauncherPlugin! + var launcher: FakeLauncher! + + override func setUp() { + launcher = FakeLauncher() + plugin = URLLauncherPlugin(launcher: launcher, binaryMessenger: FakeFlutterBinaryMessenger()) + } + + func testCanLaunchSuccess() { + let result = plugin.canLaunchUrl(url: "good://url") + + XCTAssertTrue(result) + } + + func testCanLaunchFailure() { + let result = plugin.canLaunchUrl(url: "bad://url") + + XCTAssertFalse(result) + } + + func testCanLaunchFailureWithInvalidURL() { + let result = plugin.canLaunchUrl(url: "urls can't have spaces") + + XCTAssertFalse(result) + } + + func testLaunchSuccess() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testLaunchFailure() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "bad://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertFalse(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testLaunchFailureWithInvalidURL() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in + switch result { + case .success(_): + XCTFail("Expected an error") + case .failure(let error): + let generalError = error as! GeneralError + XCTAssertEqual(generalError.code, "argument_error") + XCTAssertEqual(generalError.message, "Unable to parse URL") + XCTAssertEqual(generalError.details, "Provided URL: urls can't have spaces") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + + func testLaunchWithoutUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, false) + } + + func testLaunchWithUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true) + } + +} + +class FakeLauncher: Launcher { + var passedOptions: [UIApplication.OpenExternalURLOptionsKey: Any]? + + func canOpenURL(_ url: URL) -> Bool { + return url.scheme == "good" + } + + func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], completionHandler: ((Bool) -> Void)? = nil) { + self.passedOptions = options + completionHandler?(url.scheme == "good") + } +} + +class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { + func send(onChannel channel: String, message: Data?) { } + + func send(onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil) { } + + func setMessageHandlerOnChannel(_ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil) -> FlutterBinaryMessengerConnection { + 123 + } + + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) { } +} diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m deleted file mode 100644 index b6d3bceff03..00000000000 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.m +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import XCTest; -@import os.log; - -@interface URLLauncherUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication *app; -@end - -@implementation URLLauncherUITests - -- (void)setUp { - self.continueAfterFailure = NO; - - self.app = [[XCUIApplication alloc] init]; - [self.app launch]; -} - -- (void)testLaunch { - XCUIApplication *app = self.app; - - NSArray *buttonNames = @[ - @"Launch in app", @"Launch in app(JavaScript ON)", @"Launch in app(DOM storage ON)", - @"Launch a universal link in a native app, fallback to Safari.(Youtube)" - ]; - for (NSString *buttonName in buttonNames) { - XCUIElement *button = app.buttons[buttonName]; - XCTAssertTrue([button waitForExistenceWithTimeout:30.0]); - XCTAssertEqual(app.webViews.count, 0); - [button tap]; - XCUIElement *webView = app.webViews.firstMatch; - XCTAssertTrue([webView waitForExistenceWithTimeout:30.0]); - XCTAssertTrue([app.buttons[@"ForwardButton"] waitForExistenceWithTimeout:30.0]); - XCTAssertTrue(app.buttons[@"Share"].exists); - XCTAssertTrue(app.buttons[@"OpenInSafariButton"].exists); - [app.buttons[@"Done"] tap]; - } -} - -@end diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift new file mode 100644 index 00000000000..fbe478435bc --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest + +class URLLauncherUITests: XCTestCase { + var app: XCUIApplication! + + override func setUp() { + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + } + + func testLaunch() { + let app = self.app! + + let buttonNames: [String] = [ + "Launch in app", "Launch in app(JavaScript ON)", "Launch in app(DOM storage ON)", + "Launch a universal link in a native app, fallback to Safari.(Youtube)" + ] + for buttonName in buttonNames { + let button = app.buttons[buttonName] + XCTAssertTrue(button.waitForExistence(timeout: 30.0)) + XCTAssertEqual(app.webViews.count, 0) + button.tap() + let webView = app.webViews.firstMatch + XCTAssertTrue(webView.waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["ForwardButton"].waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.buttons["OpenInSafariButton"].exists) + app.buttons["Done"].tap() + } + } +} diff --git a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec index 50c4ffad474..0fc19900e48 100644 --- a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec +++ b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec @@ -13,8 +13,12 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/url_launcher/url_launcher_ios' } s.documentation_url = 'https://pub.dev/packages/url_launcher' + s.swift_version = '5.0' s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } s.dependency 'Flutter' s.platform = :ios, '11.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } From 495de26facb37bad2857fca64cdc01d516ced7cd Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Tue, 22 Aug 2023 22:53:29 -0500 Subject: [PATCH 11/29] converting unit tests to swift --- .../ios/Runner.xcodeproj/project.pbxproj | 38 +++- .../ios/RunnerTests/URLLauncherTests.swift | 201 +++++++++--------- .../RunnerUITests/URLLauncherUITests.swift | 48 ++--- 3 files changed, 156 insertions(+), 131 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index 536da942f1c..e40cd34be9d 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -589,7 +589,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -610,7 +613,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -627,10 +633,15 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Debug; @@ -642,10 +653,15 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Release; @@ -655,10 +671,15 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Runner; }; name = Debug; @@ -668,10 +689,15 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TEST_TARGET_NAME = Runner; }; name = Release; diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index a77d8714fe2..871c1838491 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -8,135 +8,134 @@ import XCTest @testable import url_launcher_ios class URLLauncherTests: XCTestCase { - var plugin: URLLauncherPlugin! - var launcher: FakeLauncher! + var plugin: FLTURLLauncherPlugin! + var launcher: FakeLauncher! - override func setUp() { - launcher = FakeLauncher() - plugin = URLLauncherPlugin(launcher: launcher, binaryMessenger: FakeFlutterBinaryMessenger()) - } + override func setUp() { + launcher = FakeLauncher() + plugin = FLTURLLauncherPlugin(launcher: launcher) + } - func testCanLaunchSuccess() { - let result = plugin.canLaunchUrl(url: "good://url") + func testCanLaunchSuccess() { + var error: FlutterError? + let result = plugin.canLaunchURL("good://url", error: &error) - XCTAssertTrue(result) - } + XCTAssertTrue(result!.boolValue) + XCTAssertNil(error) + } - func testCanLaunchFailure() { - let result = plugin.canLaunchUrl(url: "bad://url") + func testCanLaunchFailure() { + var error: FlutterError? + let result = plugin.canLaunchURL("bad://url", error: &error) - XCTAssertFalse(result) - } + XCTAssertFalse(result!.boolValue) + } - func testCanLaunchFailureWithInvalidURL() { - let result = plugin.canLaunchUrl(url: "urls can't have spaces") + func testCanLaunchFailureWithInvalidURL() { + var error: FlutterError? + let result = plugin.canLaunchURL("urls can't have spaces", error: &error) - XCTAssertFalse(result) - } + XCTAssertNil(result) + XCTAssertNotNil(error) + XCTAssertEqual(error!.code, "argument_error") + XCTAssertEqual(error!.message, "Unable to parse URL") + XCTAssertEqual(error!.details as! String, "Provided URL: urls can't have spaces") + } - func testLaunchSuccess() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in - switch result { - case .success(let success): - XCTAssertTrue(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) + func testLaunchSuccess() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchURL("good://url", universalLinksOnly: false) { result, error in + XCTAssertTrue(result!.boolValue) + XCTAssertNil(error) + expectation.fulfill() } - - func testLaunchFailure() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "bad://url", universalLinksOnly: false) { result in - switch result { - case .success(let success): - XCTAssertFalse(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) + + wait(for: [expectation], timeout: 1) + } + + func testLaunchFailure() { + let expectation = XCTestExpectation(description: "completion called") + + plugin.launchURL("bad://url", universalLinksOnly: false) { result, error in + XCTAssertFalse(result!.boolValue) + XCTAssertNil(error) + expectation.fulfill() } - func testLaunchFailureWithInvalidURL() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in - switch result { - case .success(_): - XCTFail("Expected an error") - case .failure(let error): - let generalError = error as! GeneralError - XCTAssertEqual(generalError.code, "argument_error") - XCTAssertEqual(generalError.message, "Unable to parse URL") - XCTAssertEqual(generalError.details, "Provided URL: urls can't have spaces") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) + wait(for: [expectation], timeout: 1) + } + + func testLaunchFailureWithInvalidURL() { + let expectation = XCTestExpectation(description: "completion called") + + plugin.launchURL("urls can't have spaces", universalLinksOnly: false) { result, error in + XCTAssertNil(result) + XCTAssertNotNil(error) + XCTAssertEqual(error!.code, "argument_error") + XCTAssertEqual(error!.message, "Unable to parse URL") + XCTAssertEqual(error!.details as! String, "Provided URL: urls can't have spaces") + + expectation.fulfill() } - func testLaunchWithoutUniversalLinks() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in - switch result { - case .success(let success): - XCTAssertTrue(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) - XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, false) + wait(for: [expectation], timeout: 1) + } + + func testLaunchWithoutUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchURL("good://url", universalLinksOnly: false) { result, error in + XCTAssertNil(error) + expectation.fulfill() } - func testLaunchWithUniversalLinks() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in - switch result { - case .success(let success): - XCTAssertTrue(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) - XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true) + wait(for: [expectation], timeout: 1) + + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, false) + } + + func testLaunchWithUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + + plugin.launchURL("good://url", universalLinksOnly: true) { result, error in + XCTAssertNil(error) + expectation.fulfill() } - + + wait(for: [expectation], timeout: 1) + + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true) + } + } -class FakeLauncher: Launcher { +class FakeLauncher: NSObject, FULLauncher { var passedOptions: [UIApplication.OpenExternalURLOptionsKey: Any]? - - func canOpenURL(_ url: URL) -> Bool { + + func canOpen(_ url: URL) -> Bool { return url.scheme == "good" } - - func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], completionHandler: ((Bool) -> Void)? = nil) { + + func open( + _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], + completionHandler: ((Bool) -> Void)? = nil + ) { self.passedOptions = options completionHandler?(url.scheme == "good") } } class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { - func send(onChannel channel: String, message: Data?) { } + func send(onChannel channel: String, message: Data?) {} - func send(onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil) { } + func send( + onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil + ) {} - func setMessageHandlerOnChannel(_ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil) -> FlutterBinaryMessengerConnection { - 123 - } + func setMessageHandlerOnChannel( + _ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil + ) -> FlutterBinaryMessengerConnection { + 123 + } - func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) { } + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} } diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift index fbe478435bc..39ec39e30c4 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift @@ -5,33 +5,33 @@ import XCTest class URLLauncherUITests: XCTestCase { - var app: XCUIApplication! + var app: XCUIApplication! - override func setUp() { - continueAfterFailure = false + override func setUp() { + continueAfterFailure = false - app = XCUIApplication() - app.launch() - } + app = XCUIApplication() + app.launch() + } - func testLaunch() { - let app = self.app! + func testLaunch() { + let app = self.app! - let buttonNames: [String] = [ - "Launch in app", "Launch in app(JavaScript ON)", "Launch in app(DOM storage ON)", - "Launch a universal link in a native app, fallback to Safari.(Youtube)" - ] - for buttonName in buttonNames { - let button = app.buttons[buttonName] - XCTAssertTrue(button.waitForExistence(timeout: 30.0)) - XCTAssertEqual(app.webViews.count, 0) - button.tap() - let webView = app.webViews.firstMatch - XCTAssertTrue(webView.waitForExistence(timeout: 30.0)) - XCTAssertTrue(app.buttons["ForwardButton"].waitForExistence(timeout: 30.0)) - XCTAssertTrue(app.buttons["Share"].exists) - XCTAssertTrue(app.buttons["OpenInSafariButton"].exists) - app.buttons["Done"].tap() - } + let buttonNames: [String] = [ + "Launch in app", "Launch in app(JavaScript ON)", "Launch in app(DOM storage ON)", + "Launch a universal link in a native app, fallback to Safari.(Youtube)", + ] + for buttonName in buttonNames { + let button = app.buttons[buttonName] + XCTAssertTrue(button.waitForExistence(timeout: 30.0)) + XCTAssertEqual(app.webViews.count, 0) + button.tap() + let webView = app.webViews.firstMatch + XCTAssertTrue(webView.waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["ForwardButton"].waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.buttons["OpenInSafariButton"].exists) + app.buttons["Done"].tap() } + } } From 6a6c8d5adb086ebf956f986b9636ba7f0b00456e Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Tue, 22 Aug 2023 23:02:52 -0500 Subject: [PATCH 12/29] updating change log --- packages/url_launcher/url_launcher_ios/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index 58b88d23d5b..8f3e4eb5a2c 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,5 +1,6 @@ ## NEXT +* Migrates unit tests and UI test from objc to swift * Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. ## 6.1.4 From 869f18f8e6dcc1d9c6a5b7d22f6bc8aa88e669b4 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Tue, 22 Aug 2023 23:24:09 -0500 Subject: [PATCH 13/29] formatting --- .../ios/RunnerTests/URLLauncherTests.swift | 197 +++++++++--------- .../RunnerUITests/URLLauncherUITests.swift | 48 ++--- .../ios/Classes/Launcher.swift | 23 +- .../ios/Classes/URLLaunchSession.swift | 62 +++--- .../ios/Classes/URLLauncherPlugin.swift | 168 +++++++-------- 5 files changed, 259 insertions(+), 239 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index a64efb81295..b134182dfc3 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -8,112 +8,112 @@ import XCTest @testable import url_launcher_ios class URLLauncherTests: XCTestCase { - var plugin: URLLauncherPlugin! - var launcher: FakeLauncher! + var plugin: URLLauncherPlugin! + var launcher: FakeLauncher! - override func setUp() { - launcher = FakeLauncher() - plugin = URLLauncherPlugin(launcher: launcher, binaryMessenger: FakeFlutterBinaryMessenger()) - } + override func setUp() { + launcher = FakeLauncher() + plugin = URLLauncherPlugin(launcher: launcher, binaryMessenger: FakeFlutterBinaryMessenger()) + } - func testCanLaunchSuccess() { - let result = plugin.canLaunchUrl(url: "good://url") + func testCanLaunchSuccess() { + let result = plugin.canLaunchUrl(url: "good://url") - XCTAssertTrue(result) - } + XCTAssertTrue(result) + } - func testCanLaunchFailure() { - let result = plugin.canLaunchUrl(url: "bad://url") + func testCanLaunchFailure() { + let result = plugin.canLaunchUrl(url: "bad://url") - XCTAssertFalse(result) - } + XCTAssertFalse(result) + } - func testCanLaunchFailureWithInvalidURL() { - let result = plugin.canLaunchUrl(url: "urls can't have spaces") + func testCanLaunchFailureWithInvalidURL() { + let result = plugin.canLaunchUrl(url: "urls can't have spaces") - XCTAssertFalse(result) - } + XCTAssertFalse(result) + } - func testLaunchSuccess() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in - switch result { - case .success(let success): - XCTAssertTrue(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) + func testLaunchSuccess() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } - func testLaunchFailure() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "bad://url", universalLinksOnly: false) { result in - switch result { - case .success(let success): - XCTAssertFalse(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) + wait(for: [expectation], timeout: 1) + } + + func testLaunchFailure() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "bad://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertFalse(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } - func testLaunchFailureWithInvalidURL() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in - switch result { - case .success(_): - XCTFail("Expected an error") - case .failure(let error): - let generalError = error as! GeneralError - XCTAssertEqual(generalError.code, "argument_error") - XCTAssertEqual(generalError.message, "Unable to parse URL") - XCTAssertEqual(generalError.details, "Provided URL: urls can't have spaces") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) + wait(for: [expectation], timeout: 1) + } + + func testLaunchFailureWithInvalidURL() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in + switch result { + case .success(_): + XCTFail("Expected an error") + case .failure(let error): + let generalError = error as! GeneralError + XCTAssertEqual(generalError.code, "argument_error") + XCTAssertEqual(generalError.message, "Unable to parse URL") + XCTAssertEqual(generalError.details, "Provided URL: urls can't have spaces") + } + expectation.fulfill() } - func testLaunchWithoutUniversalLinks() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in - switch result { - case .success(let success): - XCTAssertTrue(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) - XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, false) + wait(for: [expectation], timeout: 1) + } + + func testLaunchWithoutUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } - func testLaunchWithUniversalLinks() { - let expectation = XCTestExpectation(description: "completion called") - plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in - switch result { - case .success(let success): - XCTAssertTrue(success) - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1) - XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true) + wait(for: [expectation], timeout: 1) + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, false) + } + + func testLaunchWithUniversalLinks() { + let expectation = XCTestExpectation(description: "completion called") + plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } + wait(for: [expectation], timeout: 1) + XCTAssertEqual(launcher.passedOptions?[.universalLinksOnly] as? Bool, true) + } + } class FakeLauncher: Launcher { @@ -123,20 +123,27 @@ class FakeLauncher: Launcher { return url.scheme == "good" } - func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], completionHandler: ((Bool) -> Void)? = nil) { + func openURL( + _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], + completionHandler: ((Bool) -> Void)? = nil + ) { self.passedOptions = options completionHandler?(url.scheme == "good") } } class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { - func send(onChannel channel: String, message: Data?) { } + func send(onChannel channel: String, message: Data?) {} - func send(onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil) { } + func send( + onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil + ) {} - func setMessageHandlerOnChannel(_ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil) -> FlutterBinaryMessengerConnection { - 123 - } + func setMessageHandlerOnChannel( + _ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil + ) -> FlutterBinaryMessengerConnection { + 123 + } - func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) { } + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} } diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift index fbe478435bc..39ec39e30c4 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerUITests/URLLauncherUITests.swift @@ -5,33 +5,33 @@ import XCTest class URLLauncherUITests: XCTestCase { - var app: XCUIApplication! + var app: XCUIApplication! - override func setUp() { - continueAfterFailure = false + override func setUp() { + continueAfterFailure = false - app = XCUIApplication() - app.launch() - } + app = XCUIApplication() + app.launch() + } - func testLaunch() { - let app = self.app! + func testLaunch() { + let app = self.app! - let buttonNames: [String] = [ - "Launch in app", "Launch in app(JavaScript ON)", "Launch in app(DOM storage ON)", - "Launch a universal link in a native app, fallback to Safari.(Youtube)" - ] - for buttonName in buttonNames { - let button = app.buttons[buttonName] - XCTAssertTrue(button.waitForExistence(timeout: 30.0)) - XCTAssertEqual(app.webViews.count, 0) - button.tap() - let webView = app.webViews.firstMatch - XCTAssertTrue(webView.waitForExistence(timeout: 30.0)) - XCTAssertTrue(app.buttons["ForwardButton"].waitForExistence(timeout: 30.0)) - XCTAssertTrue(app.buttons["Share"].exists) - XCTAssertTrue(app.buttons["OpenInSafariButton"].exists) - app.buttons["Done"].tap() - } + let buttonNames: [String] = [ + "Launch in app", "Launch in app(JavaScript ON)", "Launch in app(DOM storage ON)", + "Launch a universal link in a native app, fallback to Safari.(Youtube)", + ] + for buttonName in buttonNames { + let button = app.buttons[buttonName] + XCTAssertTrue(button.waitForExistence(timeout: 30.0)) + XCTAssertEqual(app.webViews.count, 0) + button.tap() + let webView = app.webViews.firstMatch + XCTAssertTrue(webView.waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["ForwardButton"].waitForExistence(timeout: 30.0)) + XCTAssertTrue(app.buttons["Share"].exists) + XCTAssertTrue(app.buttons["OpenInSafariButton"].exists) + app.buttons["Done"].tap() } + } } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift index 97beb74057f..731ed2b0dab 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift @@ -3,16 +3,21 @@ // found in the LICENSE file. protocol Launcher { - func canOpenURL(_ url: URL) -> Bool - func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) + func canOpenURL(_ url: URL) -> Bool + func openURL( + _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], + completionHandler completion: ((Bool) -> Void)?) } class UIApplicationLauncher: Launcher { - func canOpenURL(_ url: URL) -> Bool { - UIApplication.shared.canOpenURL(url) - } - - func openURL(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) { - UIApplication.shared.open(url, options: options, completionHandler: completion) - } + func canOpenURL(_ url: URL) -> Bool { + UIApplication.shared.canOpenURL(url) + } + + func openURL( + _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], + completionHandler completion: ((Bool) -> Void)? + ) { + UIApplication.shared.open(url, options: options, completionHandler: completion) + } } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift index 332beb75f2a..b4f48016b25 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -7,34 +7,38 @@ import SafariServices typealias OpenInSafariVCResponse = (Result) -> Void class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { - - private let completion: OpenInSafariVCResponse - private let url: URL - let safari: SFSafariViewController - var didFinish: (() -> Void)? - - init(url: URL, completion: @escaping OpenInSafariVCResponse) { - self.url = url - self.completion = completion - self.safari = SFSafariViewController(url: url) - super.init() - self.safari.delegate = self - } - - func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) { - if didLoadSuccessfully { - completion(Result.success(true)) - } else { - completion(Result.failure(GeneralError(code: "Error", message: "Error while launching \(url)", details: nil))) - } - } - - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - controller.dismiss(animated: true, completion: nil) - didFinish?() - } - - func close() { - safariViewControllerDidFinish(safari) + + private let completion: OpenInSafariVCResponse + private let url: URL + let safari: SFSafariViewController + var didFinish: (() -> Void)? + + init(url: URL, completion: @escaping OpenInSafariVCResponse) { + self.url = url + self.completion = completion + self.safari = SFSafariViewController(url: url) + super.init() + self.safari.delegate = self + } + + func safariViewController( + _ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool + ) { + if didLoadSuccessfully { + completion(Result.success(true)) + } else { + completion( + Result.failure( + GeneralError(code: "Error", message: "Error while launching \(url)", details: nil))) } + } + + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + controller.dismiss(animated: true, completion: nil) + didFinish?() + } + + func close() { + safariViewControllerDidFinish(safari) + } } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index 67b7ac2e9ce..acffbf8186e 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -6,107 +6,111 @@ import Flutter public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { - public static func register(with registrar: FlutterPluginRegistrar) { - let plugin = URLLauncherPlugin(binaryMessenger: registrar.messenger()) - registrar.publish(plugin) - } + public static func register(with registrar: FlutterPluginRegistrar) { + let plugin = URLLauncherPlugin(binaryMessenger: registrar.messenger()) + registrar.publish(plugin) + } - private var currentSession: URLLaunchSession? - private let launcher: Launcher + private var currentSession: URLLaunchSession? + private let launcher: Launcher - init(launcher: Launcher = UIApplicationLauncher(), binaryMessenger: FlutterBinaryMessenger) { - self.launcher = launcher - super.init() - UrlLauncherApiSetup.setUp(binaryMessenger: binaryMessenger, api: self) - } + init(launcher: Launcher = UIApplicationLauncher(), binaryMessenger: FlutterBinaryMessenger) { + self.launcher = launcher + super.init() + UrlLauncherApiSetup.setUp(binaryMessenger: binaryMessenger, api: self) + } - func canLaunchUrl(url: String) -> Bool { - guard let url = URL(string: url) else { return false } - return launcher.canOpenURL(url) - } + func canLaunchUrl(url: String) -> Bool { + guard let url = URL(string: url) else { return false } + return launcher.canOpenURL(url) + } - func launchUrl(url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) { - guard let url = URL(string: url) else { - completion(Result.failure(invalidURLError(for: url))) - return - } - let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] - launcher.openURL(url, options: options) { success in - completion(Result.success(success)) - } + func launchUrl( + url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void + ) { + guard let url = URL(string: url) else { + completion(Result.failure(invalidURLError(for: url))) + return } - - func openUrlInSafariViewController(url: String, completion: @escaping (Result) -> Void) { - guard let url = URL(string: url) else { - completion(Result.failure(invalidURLError(for: url))) - return - } - - currentSession = URLLaunchSession(url: url, completion: completion) - guard let session = currentSession else { return } - - session.didFinish = { [weak self] in - self?.currentSession = nil - } - topViewController?.present(session.safari, animated: true, completion: nil) + let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] + launcher.openURL(url, options: options) { success in + completion(Result.success(success)) } - - func closeSafariViewController() { - currentSession?.close() + } + + func openUrlInSafariViewController( + url: String, completion: @escaping (Result) -> Void + ) { + guard let url = URL(string: url) else { + completion(Result.failure(invalidURLError(for: url))) + return } - - var topViewController: UIViewController? { - // TODO(stuartmorgan) Provide a non-deprecated codepath. See - // https://github.com/flutter/flutter/issues/104117 - UIApplication.shared.keyWindow?.rootViewController?.topViewController + + currentSession = URLLaunchSession(url: url, completion: completion) + guard let session = currentSession else { return } + + session.didFinish = { [weak self] in + self?.currentSession = nil } + topViewController?.present(session.safari, animated: true, completion: nil) + } - /** + func closeSafariViewController() { + currentSession?.close() + } + + var topViewController: UIViewController? { + // TODO(stuartmorgan) Provide a non-deprecated codepath. See + // https://github.com/flutter/flutter/issues/104117 + UIApplication.shared.keyWindow?.rootViewController?.topViewController + } + + /** * Creates an error for an invalid URL string. * * @param url The invalid URL string * @return The error to return */ - func invalidURLError(for url: String) -> Error { - GeneralError(code: "argument_error", message: "Unable to parse URL", details: "Provided URL: \(url)") - } + func invalidURLError(for url: String) -> Error { + GeneralError( + code: "argument_error", message: "Unable to parse URL", details: "Provided URL: \(url)") + } } -/** - * This method recursively iterate through the view hierarchy - * to return the top most view controller. - * - * It supports the following scenarios: - * - * - The view controller is presenting another view. - * - The view controller is a UINavigationController. - * - The view controller is a UITabBarController. - * - * @return The top most view controller. - */ +/// This method recursively iterate through the view hierarchy +/// to return the top most view controller. +/// +/// It supports the following scenarios: +/// +/// - The view controller is presenting another view. +/// - The view controller is a UINavigationController. +/// - The view controller is a UITabBarController. +/// +/// @return The top most view controller. extension UIViewController { - var topViewController: UIViewController { - if let navigationController = self as? UINavigationController { - return navigationController.viewControllers.last?.topViewController ?? navigationController.visibleViewController ?? navigationController - } - if let tabBarController = self as? UITabBarController { - return tabBarController.selectedViewController?.topViewController ?? tabBarController - } - if let presented = presentedViewController { - return presented.topViewController - } - return self + var topViewController: UIViewController { + if let navigationController = self as? UINavigationController { + return navigationController.viewControllers.last?.topViewController ?? navigationController + .visibleViewController ?? navigationController + } + if let tabBarController = self as? UITabBarController { + return tabBarController.selectedViewController?.topViewController ?? tabBarController } + if let presented = presentedViewController { + return presented.topViewController + } + return self + } } class GeneralError: Error { - let code: String - let message: String - let details: String? - - init(code: String, message: String, details: String? = nil) { - self.code = code - self.message = message - self.details = details - } + let code: String + let message: String + let details: String? + + init(code: String, message: String, details: String? = nil) { + self.code = code + self.message = message + self.details = details + } } From 1120baaf7eb215a9a813961381f9c9cfeee20cc0 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Tue, 29 Aug 2023 00:14:13 -0500 Subject: [PATCH 14/29] making test class final and private --- .../example/ios/RunnerTests/URLLauncherTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index b134182dfc3..0698007f70b 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import url_launcher_ios -class URLLauncherTests: XCTestCase { +final private class URLLauncherTests: XCTestCase { var plugin: URLLauncherPlugin! var launcher: FakeLauncher! @@ -116,7 +116,7 @@ class URLLauncherTests: XCTestCase { } -class FakeLauncher: Launcher { +final fileprivate class FakeLauncher: Launcher { var passedOptions: [UIApplication.OpenExternalURLOptionsKey: Any]? func canOpenURL(_ url: URL) -> Bool { @@ -132,7 +132,7 @@ class FakeLauncher: Launcher { } } -class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { +final fileprivate class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { func send(onChannel channel: String, message: Data?) {} func send( From 13cd002e450f08e3dc989b402a7544363d363978 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Tue, 12 Sep 2023 23:22:25 -0500 Subject: [PATCH 15/29] updated tests and formated pigeon file --- .../ios/RunnerTests/URLLauncherTests.swift | 124 +++++++++++------- .../ios/Classes/Launcher.swift | 2 +- .../ios/Classes/URLLaunchSession.swift | 2 +- .../ios/Classes/messages.g.swift | 47 ++++--- 4 files changed, 104 insertions(+), 71 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index a68a67cfbb5..523ede57f9f 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -9,49 +9,44 @@ import XCTest final class URLLauncherTests: XCTestCase { - private func createPlugin() -> FLTURLLauncherPlugin { + private func createPlugin() -> URLLauncherPlugin { let launcher = FakeLauncher() - return FLTURLLauncherPlugin(launcher: launcher) + let binaryMessenger = FakeFlutterBinaryMessenger() + return URLLauncherPlugin(launcher: launcher, binaryMessenger: binaryMessenger) } - private func createPlugin(launcher: FakeLauncher) -> FLTURLLauncherPlugin { - FLTURLLauncherPlugin(launcher: launcher) + private func createPlugin(launcher: FakeLauncher) -> URLLauncherPlugin { + let binaryMessenger = FakeFlutterBinaryMessenger() + return URLLauncherPlugin(launcher: launcher, binaryMessenger: binaryMessenger) } func testCanLaunchSuccess() { - var error: FlutterError? - let result = createPlugin().canLaunchURL("good://url", error: &error) + let result = createPlugin().canLaunchUrl(url: "good://url") - XCTAssertNotNil(result) - XCTAssertTrue(result?.boolValue ?? false) - XCTAssertNil(error) + XCTAssertTrue(result) } func testCanLaunchFailure() { - var error: FlutterError? - let result = createPlugin().canLaunchURL("bad://url", error: &error) + let result = createPlugin().canLaunchUrl(url: "bad://url") - XCTAssertNotNil(result) - XCTAssertFalse(result?.boolValue ?? true) + XCTAssertFalse(result) } func testCanLaunchFailureWithInvalidURL() { - var error: FlutterError? - let result = createPlugin().canLaunchURL("urls can't have spaces", error: &error) - - XCTAssertNil(result) - XCTAssertNotNil(error) - XCTAssertEqual(error?.code, "argument_error") - XCTAssertEqual(error?.message, "Unable to parse URL") - XCTAssertEqual(error?.details as? String, "Provided URL: urls can't have spaces") + let result = createPlugin().canLaunchUrl(url: "urls can't have spaces") + + XCTAssertFalse(result) } func testLaunchSuccess() { let expectation = XCTestExpectation(description: "completion called") - createPlugin().launchURL("good://url", universalLinksOnly: false) { result, error in - XCTAssertNotNil(result) - XCTAssertTrue(result?.boolValue ?? false) - XCTAssertNil(error) + createPlugin().launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } expectation.fulfill() } @@ -60,11 +55,13 @@ final class URLLauncherTests: XCTestCase { func testLaunchFailure() { let expectation = XCTestExpectation(description: "completion called") - - createPlugin().launchURL("bad://url", universalLinksOnly: false) { result, error in - XCTAssertNotNil(result) - XCTAssertFalse(result?.boolValue ?? true) - XCTAssertNil(error) + createPlugin().launchUrl(url: "bad://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertFalse(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } expectation.fulfill() } @@ -73,14 +70,16 @@ final class URLLauncherTests: XCTestCase { func testLaunchFailureWithInvalidURL() { let expectation = XCTestExpectation(description: "completion called") - - createPlugin().launchURL("urls can't have spaces", universalLinksOnly: false) { result, error in - XCTAssertNil(result) - XCTAssertNotNil(error) - XCTAssertEqual(error?.code, "argument_error") - XCTAssertEqual(error?.message, "Unable to parse URL") - XCTAssertEqual(error?.details as? String, "Provided URL: urls can't have spaces") - + createPlugin().launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in + switch result { + case .success(_): + XCTFail("Expected an error") + case .failure(let error): + let generalError = error as! GeneralError + XCTAssertEqual(generalError.code, "argument_error") + XCTAssertEqual(generalError.message, "Unable to parse URL") + XCTAssertEqual(generalError.details, "Provided URL: urls can't have spaces") + } expectation.fulfill() } @@ -92,8 +91,13 @@ final class URLLauncherTests: XCTestCase { let plugin = createPlugin(launcher: launcher) let expectation = XCTestExpectation(description: "completion called") - plugin.launchURL("good://url", universalLinksOnly: false) { result, error in - XCTAssertNil(error) + plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } expectation.fulfill() } @@ -106,9 +110,13 @@ final class URLLauncherTests: XCTestCase { let plugin = createPlugin(launcher: launcher) let expectation = XCTestExpectation(description: "completion called") - - plugin.launchURL("good://url", universalLinksOnly: true) { result, error in - XCTAssertNil(error) + plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in + switch result { + case .success(let success): + XCTAssertTrue(success) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } expectation.fulfill() } @@ -118,18 +126,34 @@ final class URLLauncherTests: XCTestCase { } -final private class FakeLauncher: NSObject, FULLauncher { +final private class FakeLauncher: NSObject, Launcher { var passedOptions: [UIApplication.OpenExternalURLOptionsKey: Any]? - func canOpen(_ url: URL) -> Bool { - return url.scheme == "good" + func canOpenURL(_ url: URL) -> Bool { + url.scheme == "good" } - func open( - _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any] = [:], - completionHandler: ((Bool) -> Void)? = nil + func openURL( + _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], + completionHandler completion: ((Bool) -> Void)? ) { self.passedOptions = options - completionHandler?(url.scheme == "good") + completion?(url.scheme == "good") } } + +final private class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { + func send(onChannel channel: String, message: Data?) {} + + func send( + onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil + ) {} + + func setMessageHandlerOnChannel( + _ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil + ) -> FlutterBinaryMessengerConnection { + 123 + } + + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} +} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift index 731ed2b0dab..f50950cddde 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift @@ -9,7 +9,7 @@ protocol Launcher { completionHandler completion: ((Bool) -> Void)?) } -class UIApplicationLauncher: Launcher { +final class UIApplicationLauncher: Launcher { func canOpenURL(_ url: URL) -> Bool { UIApplication.shared.canOpenURL(url) } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift index b4f48016b25..204f17490c5 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -6,7 +6,7 @@ import SafariServices typealias OpenInSafariVCResponse = (Result) -> Void -class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { +final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { private let completion: OpenInSafariVCResponse private let url: URL diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift index 79e665197dd..bfd22737d88 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -5,12 +5,13 @@ // See also: https://pub.dev/packages/pigeon import Foundation + #if os(iOS) -import Flutter + import Flutter #elseif os(macOS) -import FlutterMacOS + import FlutterMacOS #else -#error("Unsupported platform.") + #error("Unsupported platform.") #endif private func wrapResult(_ result: Any?) -> [Any?] { @@ -22,13 +23,13 @@ private func wrapError(_ error: Any) -> [Any?] { return [ flutterError.code, flutterError.message, - flutterError.details + flutterError.details, ] } return [ "\(error)", "\(type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)" + "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -42,10 +43,12 @@ protocol UrlLauncherApi { /// Returns true if the URL can definitely be launched. func canLaunchUrl(url: String) throws -> Bool /// Opens the URL externally, returning true if successful. - func launchUrl(url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) + func launchUrl( + url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - func openUrlInSafariViewController(url: String, completion: @escaping (Result) -> Void) + func openUrlInSafariViewController( + url: String, completion: @escaping (Result) -> Void) /// Closes the view controller opened by [openUrlInSafariViewController]. func closeSafariViewController() throws } @@ -56,7 +59,8 @@ class UrlLauncherApiSetup { /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { /// Returns true if the URL can definitely be launched. - let canLaunchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", binaryMessenger: binaryMessenger) + let canLaunchUrlChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", binaryMessenger: binaryMessenger) if let api = api { canLaunchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any] @@ -72,7 +76,8 @@ class UrlLauncherApiSetup { canLaunchUrlChannel.setMessageHandler(nil) } /// Opens the URL externally, returning true if successful. - let launchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.launchUrl", binaryMessenger: binaryMessenger) + let launchUrlChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.UrlLauncherApi.launchUrl", binaryMessenger: binaryMessenger) if let api = api { launchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any] @@ -80,10 +85,10 @@ class UrlLauncherApiSetup { let universalLinksOnlyArg = args[1] as! Bool api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -92,17 +97,19 @@ class UrlLauncherApiSetup { } /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController", binaryMessenger: binaryMessenger) + let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController", + binaryMessenger: binaryMessenger) if let api = api { openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in let args = message as! [Any] let urlArg = args[0] as! String api.openUrlInSafariViewController(url: urlArg) { result in switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -110,7 +117,9 @@ class UrlLauncherApiSetup { openUrlInSafariViewControllerChannel.setMessageHandler(nil) } /// Closes the view controller opened by [openUrlInSafariViewController]. - let closeSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController", binaryMessenger: binaryMessenger) + let closeSafariViewControllerChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController", + binaryMessenger: binaryMessenger) if let api = api { closeSafariViewControllerChannel.setMessageHandler { _, reply in do { From f8f5f0b88cbd903b3c6afcb1a2e03456f96b8334 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Wed, 13 Sep 2023 17:39:45 -0500 Subject: [PATCH 16/29] updates changelog --- packages/url_launcher/url_launcher_ios/CHANGELOG.md | 3 +++ packages/url_launcher/url_launcher_ios/pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index 2575bd86cb2..e8b9692e846 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,6 +1,9 @@ ## 6.1.5 * Migrates plugin from objc to swift + +## 6.1.5 + * Adds pub topics to package metadata. * Updates minimum supported SDK version to Flutter 3.7/Dart 2.19. diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index b68565faedd..ea7beb6eee2 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -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.1.5 +version: 6.1.6 environment: sdk: ">=2.19.0 <4.0.0" From 827204ad94bb708b79e62226c436f401ba3808e8 Mon Sep 17 00:00:00 2001 From: chrisdlangham Date: Wed, 13 Sep 2023 19:13:30 -0500 Subject: [PATCH 17/29] Update CHANGELOG.md --- packages/url_launcher/url_launcher_ios/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index e8b9692e846..91ba9020061 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,4 +1,4 @@ -## 6.1.5 +## 6.1.6 * Migrates plugin from objc to swift From 6b5e4cd8bfa8d0a9ba863f5098b06306ad2f3b35 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Thu, 14 Sep 2023 21:20:56 -0500 Subject: [PATCH 18/29] uses latest version of pigeon --- .../ios/Classes/messages.g.swift | 24 ++++++++++++------- .../url_launcher_ios/lib/src/messages.g.dart | 12 ++++++---- .../url_launcher_ios/pubspec.yaml | 2 +- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift index bfd22737d88..7ac756f969d 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// Autogenerated from Pigeon (v11.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -14,6 +14,10 @@ import Foundation #error("Unsupported platform.") #endif +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + private func wrapResult(_ result: Any?) -> [Any?] { return [result] } @@ -35,7 +39,7 @@ private func wrapError(_ error: Any) -> [Any?] { private func nilOrValue(_ value: Any?) -> T? { if value is NSNull { return nil } - return (value as Any) as! T? + return value as! T? } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. @@ -60,10 +64,11 @@ class UrlLauncherApiSetup { static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { /// Returns true if the URL can definitely be launched. let canLaunchUrlChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", binaryMessenger: binaryMessenger) + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", + binaryMessenger: binaryMessenger) if let api = api { canLaunchUrlChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let urlArg = args[0] as! String do { let result = try api.canLaunchUrl(url: urlArg) @@ -77,10 +82,11 @@ class UrlLauncherApiSetup { } /// Opens the URL externally, returning true if successful. let launchUrlChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.UrlLauncherApi.launchUrl", binaryMessenger: binaryMessenger) + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", + binaryMessenger: binaryMessenger) if let api = api { launchUrlChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let urlArg = args[0] as! String let universalLinksOnlyArg = args[1] as! Bool api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in @@ -98,11 +104,11 @@ class UrlLauncherApiSetup { /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController", + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", binaryMessenger: binaryMessenger) if let api = api { openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in - let args = message as! [Any] + let args = message as! [Any?] let urlArg = args[0] as! String api.openUrlInSafariViewController(url: urlArg) { result in switch result { @@ -118,7 +124,7 @@ class UrlLauncherApiSetup { } /// Closes the view controller opened by [openUrlInSafariViewController]. let closeSafariViewControllerChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController", + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController", binaryMessenger: binaryMessenger) if let api = api { closeSafariViewControllerChannel.setMessageHandler { _, reply in diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index 0c35d9a0c1e..fc24e5a8916 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.5), do not edit directly. +// Autogenerated from Pigeon (v11.0.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -24,7 +24,8 @@ class UrlLauncherApi { /// Returns true if the URL can definitely be launched. Future canLaunchUrl(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec, + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send([arg_url]) as List?; @@ -52,7 +53,7 @@ class UrlLauncherApi { /// Opens the URL externally, returning true if successful. Future launchUrl(String arg_url, bool arg_universalLinksOnly) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl', codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel .send([arg_url, arg_universalLinksOnly]) as List?; @@ -81,7 +82,7 @@ class UrlLauncherApi { /// when it has loaded successfully. Future openUrlInSafariViewController(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.openUrlInSafariViewController', + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController', codec, binaryMessenger: _binaryMessenger); final List? replyList = @@ -110,7 +111,8 @@ class UrlLauncherApi { /// Closes the view controller opened by [openUrlInSafariViewController]. Future closeSafariViewController() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.UrlLauncherApi.closeSafariViewController', codec, + 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController', + codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send(null) as List?; if (replyList == null) { diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index ea7beb6eee2..1d8e4de6f98 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pigeon: ^9.2.4 + pigeon: ^11.0.1 plugin_platform_interface: ^2.0.0 test: ^1.16.3 From 3651a9747bbaa64b208e4452dec254e026de408b Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Thu, 14 Sep 2023 21:44:48 -0500 Subject: [PATCH 19/29] updates change log --- packages/url_launcher/url_launcher_ios/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index 91ba9020061..6fb3424e63f 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,6 +1,6 @@ ## 6.1.6 -* Migrates plugin from objc to swift +* Migrates plugin from Objective-C to Swift. ## 6.1.5 From 257b7ccc89260d3c37c54a724cc0b578aa8c37e6 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Thu, 14 Sep 2023 21:46:23 -0500 Subject: [PATCH 20/29] moves setting up the pigeon api to the register function instead of the plugin's constructor --- .../ios/RunnerTests/URLLauncherTests.swift | 22 ++----------------- .../ios/Classes/URLLauncherPlugin.swift | 7 +++--- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index 523ede57f9f..96c9ef330bb 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -11,13 +11,11 @@ final class URLLauncherTests: XCTestCase { private func createPlugin() -> URLLauncherPlugin { let launcher = FakeLauncher() - let binaryMessenger = FakeFlutterBinaryMessenger() - return URLLauncherPlugin(launcher: launcher, binaryMessenger: binaryMessenger) + return URLLauncherPlugin(launcher: launcher) } private func createPlugin(launcher: FakeLauncher) -> URLLauncherPlugin { - let binaryMessenger = FakeFlutterBinaryMessenger() - return URLLauncherPlugin(launcher: launcher, binaryMessenger: binaryMessenger) + return URLLauncherPlugin(launcher: launcher) } func testCanLaunchSuccess() { @@ -141,19 +139,3 @@ final private class FakeLauncher: NSObject, Launcher { completion?(url.scheme == "good") } } - -final private class FakeFlutterBinaryMessenger: NSObject, FlutterBinaryMessenger { - func send(onChannel channel: String, message: Data?) {} - - func send( - onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil - ) {} - - func setMessageHandlerOnChannel( - _ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil - ) -> FlutterBinaryMessengerConnection { - 123 - } - - func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} -} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index acffbf8186e..1d587faa062 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -7,17 +7,16 @@ import Flutter public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { public static func register(with registrar: FlutterPluginRegistrar) { - let plugin = URLLauncherPlugin(binaryMessenger: registrar.messenger()) + let plugin = URLLauncherPlugin() + UrlLauncherApiSetup.setUp(binaryMessenger: registrar.messenger(), api: plugin) registrar.publish(plugin) } private var currentSession: URLLaunchSession? private let launcher: Launcher - init(launcher: Launcher = UIApplicationLauncher(), binaryMessenger: FlutterBinaryMessenger) { + init(launcher: Launcher = UIApplicationLauncher()) { self.launcher = launcher - super.init() - UrlLauncherApiSetup.setUp(binaryMessenger: binaryMessenger, api: self) } func canLaunchUrl(url: String) -> Bool { From 7241d2006ecd629ad5a853c5a08ab0fecbc827f6 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Fri, 6 Oct 2023 23:01:26 -0500 Subject: [PATCH 21/29] adds in missing throws keyword --- .../ios/RunnerTests/URLLauncherTests.swift | 30 +++++++++++++------ .../ios/Classes/URLLauncherPlugin.swift | 8 +++-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index cd8d50245d5..68fb85089dd 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -19,21 +19,33 @@ final class URLLauncherTests: XCTestCase { } func testCanLaunchSuccess() { - let result = createPlugin().canLaunchUrl(url: "good://url") - - XCTAssertTrue(result) + do { + let result = try createPlugin().canLaunchUrl(url: "good://url") + XCTAssertTrue(result) + } catch { + XCTFail("Unexpected error: \(error)") + } } func testCanLaunchFailure() { - let result = createPlugin().canLaunchUrl(url: "bad://url") - - XCTAssertFalse(result) + do { + let result = try createPlugin().canLaunchUrl(url: "bad://url") + XCTAssertFalse(result) + } catch { + XCTFail("Unexpected error: \(error)") + } } func testCanLaunchFailureWithInvalidURL() { - let result = createPlugin().canLaunchUrl(url: "urls can't have spaces") - - XCTAssertFalse(result) + do { + let result = try createPlugin().canLaunchUrl(url: "urls can't have spaces") + XCTAssertFalse(result) + } catch { + let generalError = error as? GeneralError + XCTAssertEqual(generalError?.code, "argument_error") + XCTAssertEqual(generalError?.message, "Unable to parse URL") + XCTAssertEqual(generalError?.details, "Provided URL: urls can't have spaces") + } } func testLaunchSuccess() { diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index 1d587faa062..c2a07434ddb 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -19,8 +19,10 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { self.launcher = launcher } - func canLaunchUrl(url: String) -> Bool { - guard let url = URL(string: url) else { return false } + func canLaunchUrl(url: String) throws -> Bool { + guard let url = URL(string: url) else { + throw invalidURLError(for: url) + } return launcher.canOpenURL(url) } @@ -54,7 +56,7 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { topViewController?.present(session.safari, animated: true, completion: nil) } - func closeSafariViewController() { + func closeSafariViewController() throws { currentSession?.close() } From 9d578f2d357ce18112fe160e8308b9b8f05f484d Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Fri, 6 Oct 2023 23:47:19 -0500 Subject: [PATCH 22/29] addresses feedback --- .../ios/RunnerTests/URLLauncherTests.swift | 12 +++---- .../ios/Classes/Launcher.swift | 4 +++ .../ios/Classes/URLLaunchSession.swift | 22 ++++++------ .../ios/Classes/URLLauncherPlugin.swift | 36 +++++++------------ 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index 68fb85089dd..25e87ef02ed 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -41,10 +41,10 @@ final class URLLauncherTests: XCTestCase { let result = try createPlugin().canLaunchUrl(url: "urls can't have spaces") XCTAssertFalse(result) } catch { - let generalError = error as? GeneralError + let generalError = error as? FlutterError XCTAssertEqual(generalError?.code, "argument_error") XCTAssertEqual(generalError?.message, "Unable to parse URL") - XCTAssertEqual(generalError?.details, "Provided URL: urls can't have spaces") + XCTAssertEqual(generalError?.details as? String, "Provided URL: urls can't have spaces") } } @@ -89,10 +89,10 @@ final class URLLauncherTests: XCTestCase { case .failure(let error): XCTAssertNotNil(error) - let generalError = error as! GeneralError - XCTAssertEqual(generalError.code, "argument_error") - XCTAssertEqual(generalError.message, "Unable to parse URL") - XCTAssertEqual(generalError.details, "Provided URL: urls can't have spaces") + let generalError = error as? FlutterError + XCTAssertEqual(generalError?.code, "argument_error") + XCTAssertEqual(generalError?.message, "Unable to parse URL") + XCTAssertEqual(generalError?.details as? String, "Provided URL: urls can't have spaces") } expectation.fulfill() } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift index f50950cddde..8fdc1bfb636 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/// Protocol for UIApplication methods relating to launching URLs. +/// +/// This protocol exists to allow injecting an alternate implementation for testing. protocol Launcher { func canOpenURL(_ url: URL) -> Bool func openURL( @@ -9,6 +12,7 @@ protocol Launcher { completionHandler completion: ((Bool) -> Void)?) } +/// Default implementation of Launcher, using UIApplication. final class UIApplicationLauncher: Launcher { func canOpenURL(_ url: URL) -> Bool { UIApplication.shared.canOpenURL(url) diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift index 204f17490c5..1fbfbef0f24 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -2,34 +2,36 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import Flutter import SafariServices -typealias OpenInSafariVCResponse = (Result) -> Void +typealias OpenInSafariCompletionHandler = (Result) -> Void final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { - private let completion: OpenInSafariVCResponse + private let completion: OpenInSafariCompletionHandler private let url: URL - let safari: SFSafariViewController + let safariViewController: SFSafariViewController var didFinish: (() -> Void)? - init(url: URL, completion: @escaping OpenInSafariVCResponse) { + init(url: URL, completion: @escaping OpenInSafariCompletionHandler) { self.url = url self.completion = completion - self.safari = SFSafariViewController(url: url) + self.safariViewController = SFSafariViewController(url: url) super.init() - self.safari.delegate = self + self.safariViewController.delegate = self } func safariViewController( _ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool ) { if didLoadSuccessfully { - completion(Result.success(true)) + completion(.success(true)) } else { completion( - Result.failure( - GeneralError(code: "Error", message: "Error while launching \(url)", details: nil))) + .failure( + FlutterError(code: "Error", message: "Error while launching \(url)", details: nil)) + ) } } @@ -39,6 +41,6 @@ final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { } func close() { - safariViewControllerDidFinish(safari) + safariViewControllerDidFinish(safariViewController) } } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index c2a07434ddb..d7ea5677800 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -15,6 +15,12 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { private var currentSession: URLLaunchSession? private let launcher: Launcher + var topViewController: UIViewController? { + // TODO(stuartmorgan) Provide a non-deprecated codepath. See + // https://github.com/flutter/flutter/issues/104117 + UIApplication.shared.keyWindow?.rootViewController?.topViewController + } + init(launcher: Launcher = UIApplicationLauncher()) { self.launcher = launcher } @@ -47,25 +53,19 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { return } - currentSession = URLLaunchSession(url: url, completion: completion) - guard let session = currentSession else { return } + let session = URLLaunchSession(url: url, completion: completion) + currentSession = session session.didFinish = { [weak self] in self?.currentSession = nil } - topViewController?.present(session.safari, animated: true, completion: nil) + topViewController?.present(session.safariViewController, animated: true, completion: nil) } func closeSafariViewController() throws { currentSession?.close() } - var topViewController: UIViewController? { - // TODO(stuartmorgan) Provide a non-deprecated codepath. See - // https://github.com/flutter/flutter/issues/104117 - UIApplication.shared.keyWindow?.rootViewController?.topViewController - } - /** * Creates an error for an invalid URL string. * @@ -73,13 +73,13 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { * @return The error to return */ func invalidURLError(for url: String) -> Error { - GeneralError( + FlutterError( code: "argument_error", message: "Unable to parse URL", details: "Provided URL: \(url)") } } -/// This method recursively iterate through the view hierarchy -/// to return the top most view controller. +/// This method recursively iterates through the view hierarchy +/// to return the top-most view controller. /// /// It supports the following scenarios: /// @@ -104,14 +104,4 @@ extension UIViewController { } } -class GeneralError: Error { - let code: String - let message: String - let details: String? - - init(code: String, message: String, details: String? = nil) { - self.code = code - self.message = message - self.details = details - } -} +extension FlutterError: Error {} From 657c011db07eda0c2a616e6326374b44b2a8d154 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Wed, 11 Oct 2023 00:07:32 -0500 Subject: [PATCH 23/29] changes pigeon api to not throw errors, and let the dart side throw errors --- .../ios/RunnerTests/URLLauncherTests.swift | 72 +++++----- .../ios/Classes/URLLaunchSession.swift | 29 +++- .../ios/Classes/URLLauncherPlugin.swift | 28 ++-- .../ios/Classes/messages.g.swift | 93 ++++++++++++- .../url_launcher_ios/lib/src/messages.g.dart | 88 +++++++++++- .../lib/url_launcher_ios.dart | 36 ++++- .../url_launcher_ios/pigeons/messages.dart | 39 +++++- .../url_launcher_ios/pubspec.yaml | 2 + .../test/url_launcher_ios_test.dart | 129 +++++++++--------- .../test/url_launcher_ios_test.mocks.dart | 111 +++++++++++++++ 10 files changed, 488 insertions(+), 139 deletions(-) create mode 100644 packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index 25e87ef02ed..27f5127db5a 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -19,32 +19,26 @@ final class URLLauncherTests: XCTestCase { } func testCanLaunchSuccess() { - do { - let result = try createPlugin().canLaunchUrl(url: "good://url") - XCTAssertTrue(result) - } catch { - XCTFail("Unexpected error: \(error)") - } + let result = createPlugin().canLaunchUrl(url: "good://url") + XCTAssertEqual(result.result, .success) } func testCanLaunchFailure() { - do { - let result = try createPlugin().canLaunchUrl(url: "bad://url") - XCTAssertFalse(result) - } catch { - XCTFail("Unexpected error: \(error)") - } + let result = createPlugin().canLaunchUrl(url: "bad://url") + XCTAssertEqual(result.result, .failure) } func testCanLaunchFailureWithInvalidURL() { - do { - let result = try createPlugin().canLaunchUrl(url: "urls can't have spaces") - XCTAssertFalse(result) - } catch { - let generalError = error as? FlutterError - XCTAssertEqual(generalError?.code, "argument_error") - XCTAssertEqual(generalError?.message, "Unable to parse URL") - XCTAssertEqual(generalError?.details as? String, "Provided URL: urls can't have spaces") + let result = createPlugin().canLaunchUrl(url: "urls can't have spaces") + if result.result == .failure { + // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't + // fail to parse URLs, so the test must allow for either outcome. + XCTAssertNil(result.errorMessage) + XCTAssertNil(result.errorDetails) + } else { + XCTAssertEqual(result.result, .invalidUrl) + XCTAssertEqual(result.errorMessage, "Unable to parse URL") + XCTAssertEqual(result.errorDetails, "Provided URL: urls can't have spaces") } } @@ -52,8 +46,8 @@ final class URLLauncherTests: XCTestCase { let expectation = XCTestExpectation(description: "completion called") createPlugin().launchUrl(url: "good://url", universalLinksOnly: false) { result in switch result { - case .success(let success): - XCTAssertTrue(success) + case .success(let details): + XCTAssertEqual(details.result, .success) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -67,8 +61,8 @@ final class URLLauncherTests: XCTestCase { let expectation = XCTestExpectation(description: "completion called") createPlugin().launchUrl(url: "bad://url", universalLinksOnly: false) { result in switch result { - case .success(let success): - XCTAssertFalse(success) + case .success(let details): + XCTAssertEqual(details.result, .failure) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -82,17 +76,19 @@ final class URLLauncherTests: XCTestCase { let expectation = XCTestExpectation(description: "completion called") createPlugin().launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in switch result { - case .success(_): - // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't - // fail to parse URLs, so the test must allow for either outcome. - XCTFail("Expected an error") + case .success(let details): + if details.result == .failure { + // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't + // fail to parse URLs, so the test must allow for either outcome. + XCTAssertNil(details.errorMessage) + XCTAssertNil(details.errorDetails) + } else { + XCTAssertEqual(details.result, .invalidUrl) + XCTAssertEqual(details.errorMessage, "Unable to parse URL") + XCTAssertEqual(details.errorDetails, "Provided URL: urls can't have spaces") + } case .failure(let error): - XCTAssertNotNil(error) - - let generalError = error as? FlutterError - XCTAssertEqual(generalError?.code, "argument_error") - XCTAssertEqual(generalError?.message, "Unable to parse URL") - XCTAssertEqual(generalError?.details as? String, "Provided URL: urls can't have spaces") + XCTFail("Unexpected error: \(error)") } expectation.fulfill() } @@ -107,8 +103,8 @@ final class URLLauncherTests: XCTestCase { let expectation = XCTestExpectation(description: "completion called") plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in switch result { - case .success(let success): - XCTAssertTrue(success) + case .success(let details): + XCTAssertEqual(details.result, .success) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -126,8 +122,8 @@ final class URLLauncherTests: XCTestCase { let expectation = XCTestExpectation(description: "completion called") plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in switch result { - case .success(let success): - XCTAssertTrue(success) + case .success(let details): + XCTAssertEqual(details.result, .success) case .failure(let error): XCTFail("Unexpected error: \(error)") } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift index 1fbfbef0f24..b28fee15abd 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -5,15 +5,25 @@ import Flutter import SafariServices -typealias OpenInSafariCompletionHandler = (Result) -> Void +typealias OpenInSafariCompletionHandler = (Result) -> Void +/// A session responsible for launching a URL in Safari and handling its events. final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { private let completion: OpenInSafariCompletionHandler private let url: URL + + /// The Safari view controller used for displaying the URL. let safariViewController: SFSafariViewController + + // A closure to be executed after the Safari view controller finishes. var didFinish: (() -> Void)? + /// Initializes a new URLLaunchSession with the provided URL and completion handler. + /// + /// - Parameters: + /// - url: The URL to be opened in Safari. + /// - completion: The completion handler to be called after attempting to open the URL. init(url: URL, completion: @escaping OpenInSafariCompletionHandler) { self.url = url self.completion = completion @@ -22,24 +32,33 @@ final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { self.safariViewController.delegate = self } + /// Called when the Safari view controller completes the initial load. + /// + /// - Parameters: + /// - controller: The Safari view controller. + /// - didLoadSuccessfully: Indicates if the initial load was successful. func safariViewController( _ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool ) { if didLoadSuccessfully { - completion(.success(true)) + completion(.success(LaunchResultDetails(result: .success))) } else { completion( - .failure( - FlutterError(code: "Error", message: "Error while launching \(url)", details: nil)) - ) + .success( + LaunchResultDetails(result: .failedToLoad, errorMessage: "Error while launching \(url)") + )) } } + /// Called when the user finishes using the Safari view controller. + /// + /// - Parameter controller: The Safari view controller. func safariViewControllerDidFinish(_ controller: SFSafariViewController) { controller.dismiss(animated: true, completion: nil) didFinish?() } + /// Closes the Safari view controller. func close() { safariViewControllerDidFinish(safariViewController) } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index d7ea5677800..ab76a406377 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -25,31 +25,34 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { self.launcher = launcher } - func canLaunchUrl(url: String) throws -> Bool { + func canLaunchUrl(url: String) -> LaunchResultDetails { guard let url = URL(string: url) else { - throw invalidURLError(for: url) + return invalidURLError(for: url) } - return launcher.canOpenURL(url) + let canOpen = launcher.canOpenURL(url) + return LaunchResultDetails(result: canOpen ? .success : .failure) } func launchUrl( - url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void + url: String, universalLinksOnly: Bool, + completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { - completion(Result.failure(invalidURLError(for: url))) + completion(.success(invalidURLError(for: url))) return } let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] launcher.openURL(url, options: options) { success in - completion(Result.success(success)) + let result = LaunchResultDetails(result: success ? .success : .failure) + completion(.success(result)) } } func openUrlInSafariViewController( - url: String, completion: @escaping (Result) -> Void + url: String, completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { - completion(Result.failure(invalidURLError(for: url))) + completion(.success(invalidURLError(for: url))) return } @@ -72,9 +75,10 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { * @param url The invalid URL string * @return The error to return */ - func invalidURLError(for url: String) -> Error { - FlutterError( - code: "argument_error", message: "Unable to parse URL", details: "Provided URL: \(url)") + func invalidURLError(for url: String) -> LaunchResultDetails { + LaunchResultDetails( + result: .invalidUrl, errorMessage: "Unable to parse URL", errorDetails: "Provided URL: \(url)" + ) } } @@ -103,5 +107,3 @@ extension UIViewController { return self } } - -extension FlutterError: Error {} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift index 7ac756f969d..3111302597e 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -42,17 +42,95 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +/// Possible outcomes of launching a URL. +enum LaunchResult: Int { + /// The URL was successfully launched. + case success = 0 + /// The URL could not be launched + case failure = 1 + /// The URL was not launched because it is not invalid URL + case invalidUrl = 2 + /// The URL did not load successfully in the SFSafariViewController. + case failedToLoad = 3 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct LaunchResultDetails { + /// The result of the launch attempt. + var result: LaunchResult + /// A system-provided error message, if any. + var errorMessage: String? = nil + /// A system-provided error details, if any. + var errorDetails: String? = nil + + static func fromList(_ list: [Any?]) -> LaunchResultDetails? { + let result = LaunchResult(rawValue: list[0] as! Int)! + let errorMessage: String? = nilOrValue(list[1]) + let errorDetails: String? = nilOrValue(list[2]) + + return LaunchResultDetails( + result: result, + errorMessage: errorMessage, + errorDetails: errorDetails + ) + } + func toList() -> [Any?] { + return [ + result.rawValue, + errorMessage, + errorDetails, + ] + } +} + +private class UrlLauncherApiCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return LaunchResultDetails.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class UrlLauncherApiCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? LaunchResultDetails { + super.writeByte(128) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class UrlLauncherApiCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return UrlLauncherApiCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return UrlLauncherApiCodecWriter(data: data) + } +} + +class UrlLauncherApiCodec: FlutterStandardMessageCodec { + static let shared = UrlLauncherApiCodec(readerWriter: UrlLauncherApiCodecReaderWriter()) +} + /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol UrlLauncherApi { /// Returns true if the URL can definitely be launched. - func canLaunchUrl(url: String) throws -> Bool + func canLaunchUrl(url: String) throws -> LaunchResultDetails /// Opens the URL externally, returning true if successful. func launchUrl( - url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) + url: String, universalLinksOnly: Bool, + completion: @escaping (Result) -> Void) /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. func openUrlInSafariViewController( - url: String, completion: @escaping (Result) -> Void) + url: String, completion: @escaping (Result) -> Void) /// Closes the view controller opened by [openUrlInSafariViewController]. func closeSafariViewController() throws } @@ -60,12 +138,13 @@ protocol UrlLauncherApi { /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class UrlLauncherApiSetup { /// The codec used by UrlLauncherApi. + static var codec: FlutterStandardMessageCodec { UrlLauncherApiCodec.shared } /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { /// Returns true if the URL can definitely be launched. let canLaunchUrlChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", - binaryMessenger: binaryMessenger) + binaryMessenger: binaryMessenger, codec: codec) if let api = api { canLaunchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -83,7 +162,7 @@ class UrlLauncherApiSetup { /// Opens the URL externally, returning true if successful. let launchUrlChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", - binaryMessenger: binaryMessenger) + binaryMessenger: binaryMessenger, codec: codec) if let api = api { launchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -105,7 +184,7 @@ class UrlLauncherApiSetup { /// when it has loaded successfully. let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", - binaryMessenger: binaryMessenger) + binaryMessenger: binaryMessenger, codec: codec) if let api = api { openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -125,7 +204,7 @@ class UrlLauncherApiSetup { /// Closes the view controller opened by [openUrlInSafariViewController]. let closeSafariViewControllerChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController", - binaryMessenger: binaryMessenger) + binaryMessenger: binaryMessenger, codec: codec) if let api = api { closeSafariViewControllerChannel.setMessageHandler { _, reply in do { diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index fc24e5a8916..3c48bcbd6e4 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -11,6 +11,78 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +/// Possible outcomes of launching a URL. +enum LaunchResult { + /// The URL was successfully launched. + success, + + /// The URL could not be launched + failure, + + /// The URL was not launched because it is not invalid URL + invalidUrl, + + /// The URL did not load successfully in the SFSafariViewController. + failedToLoad, +} + +class LaunchResultDetails { + LaunchResultDetails({ + required this.result, + this.errorMessage, + this.errorDetails, + }); + + /// The result of the launch attempt. + LaunchResult result; + + /// A system-provided error message, if any. + String? errorMessage; + + /// A system-provided error details, if any. + String? errorDetails; + + Object encode() { + return [ + result.index, + errorMessage, + errorDetails, + ]; + } + + static LaunchResultDetails decode(Object result) { + result as List; + return LaunchResultDetails( + result: LaunchResult.values[result[0]! as int], + errorMessage: result[1] as String?, + errorDetails: result[2] as String?, + ); + } +} + +class _UrlLauncherApiCodec extends StandardMessageCodec { + const _UrlLauncherApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is LaunchResultDetails) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return LaunchResultDetails.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + class UrlLauncherApi { /// Constructor for [UrlLauncherApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default @@ -19,10 +91,10 @@ class UrlLauncherApi { : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = StandardMessageCodec(); + static const MessageCodec codec = _UrlLauncherApiCodec(); /// Returns true if the URL can definitely be launched. - Future canLaunchUrl(String arg_url) async { + Future canLaunchUrl(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl', codec, @@ -46,12 +118,13 @@ class UrlLauncherApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as bool?)!; + return (replyList[0] as LaunchResultDetails?)!; } } /// Opens the URL externally, returning true if successful. - Future launchUrl(String arg_url, bool arg_universalLinksOnly) async { + Future launchUrl( + String arg_url, bool arg_universalLinksOnly) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl', codec, binaryMessenger: _binaryMessenger); @@ -74,13 +147,14 @@ class UrlLauncherApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as bool?)!; + return (replyList[0] as LaunchResultDetails?)!; } } /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - Future openUrlInSafariViewController(String arg_url) async { + Future openUrlInSafariViewController( + String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController', codec, @@ -104,7 +178,7 @@ class UrlLauncherApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as bool?)!; + return (replyList[0] as LaunchResultDetails?)!; } } diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart index 2f0e9f47b94..d2658f12eb1 100644 --- a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart +++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; @@ -26,8 +27,9 @@ class UrlLauncherIOS extends UrlLauncherPlatform { final LinkDelegate? linkDelegate = null; @override - Future canLaunch(String url) { - return _hostApi.canLaunchUrl(url); + Future canLaunch(String url) async { + final LaunchResultDetails result = await _hostApi.canLaunchUrl(url); + return _mapLaunchResults(results: result); } @override @@ -45,11 +47,39 @@ class UrlLauncherIOS extends UrlLauncherPlatform { required bool universalLinksOnly, required Map headers, String? webOnlyWindowName, - }) { + }) async { + final LaunchResultDetails result = + await _launchUrl(useSafariVC, url, universalLinksOnly); + return _mapLaunchResults(results: result); + } + + Future _launchUrl( + bool useSafariVC, String url, bool universalLinksOnly) { if (useSafariVC) { return _hostApi.openUrlInSafariViewController(url); } else { return _hostApi.launchUrl(url, universalLinksOnly); } } + + bool _mapLaunchResults({required final LaunchResultDetails results}) { + switch (results.result) { + case LaunchResult.success: + return true; + case LaunchResult.failure: + return false; + case LaunchResult.invalidUrl: + throw PlatformException( + code: 'invalidUrl', + message: results.errorMessage, + details: results.errorDetails, + ); + case LaunchResult.failedToLoad: + throw PlatformException( + code: 'failedToLoad', + message: results.errorMessage, + details: results.errorDetails, + ); + } + } } diff --git a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart index 7fdff617067..fd699c79fb3 100644 --- a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart +++ b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart @@ -9,22 +9,55 @@ import 'package:pigeon/pigeon.dart'; swiftOut: 'ios/Classes/messages.g.swift', copyrightHeader: 'pigeons/copyright.txt', )) + +/// Possible outcomes of launching a URL. +enum LaunchResult { + /// The URL was successfully launched. + success, + + /// The URL could not be launched + failure, + + /// The URL was not launched because it is not invalid URL + invalidUrl, + + /// The URL did not load successfully in the SFSafariViewController. + failedToLoad, +} + +class LaunchResultDetails { + LaunchResultDetails({ + required this.result, + this.errorMessage, + this.errorDetails, + }); + + /// The result of the launch attempt. + final LaunchResult result; + + /// A system-provided error message, if any. + final String? errorMessage; + + /// A system-provided error details, if any. + final String? errorDetails; +} + @HostApi() abstract class UrlLauncherApi { /// Returns true if the URL can definitely be launched. @ObjCSelector('canLaunchURL:') - bool canLaunchUrl(String url); + LaunchResultDetails canLaunchUrl(String url); /// Opens the URL externally, returning true if successful. @async @ObjCSelector('launchURL:universalLinksOnly:') - bool launchUrl(String url, bool universalLinksOnly); + LaunchResultDetails launchUrl(String url, bool universalLinksOnly); /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. @async @ObjCSelector('openSafariViewControllerWithURL:') - bool openUrlInSafariViewController(String url); + LaunchResultDetails openUrlInSafariViewController(String url); /// Closes the view controller opened by [openUrlInSafariViewController]. void closeSafariViewController(); diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 1d8e4de6f98..dd66c3a9cf1 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -22,8 +22,10 @@ dependencies: url_launcher_platform_interface: ^2.0.3 dev_dependencies: + build_runner: ^2.3.3 flutter_test: sdk: flutter + mockito: 5.4.2 pigeon: ^11.0.1 plugin_platform_interface: ^2.0.0 test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart index 9274173f90b..1be85d7f706 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -4,18 +4,25 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; import 'package:url_launcher_ios/src/messages.g.dart'; import 'package:url_launcher_ios/url_launcher_ios.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'url_launcher_ios_test.mocks.dart'; + +@GenerateMocks([UrlLauncherApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('UrlLauncherIOS', () { - late _FakeUrlLauncherApi api; + late MockUrlLauncherApi api; + late UrlLauncherIOS launcher; setUp(() { - api = _FakeUrlLauncherApi(); + api = MockUrlLauncherApi(); + launcher = UrlLauncherIOS(api: api); }); test('registers instance', () { @@ -24,24 +31,32 @@ void main() { }); test('canLaunch success', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.canLaunchUrl(any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.success), + ); expect(await launcher.canLaunch('http://example.com/'), true); }); test('canLaunch failure', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.canLaunchUrl(any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.failure), + ); expect(await launcher.canLaunch('unknown://scheme'), false); }); test('canLaunch invalid URL passes the PlatformException through', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.canLaunchUrl(any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.invalidUrl), + ); await expectLater(launcher.canLaunch('invalid://u r l'), throwsA(isA())); }); test('launch success', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.launchUrl(any, any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.success), + ); expect( await launcher.launch( 'http://example.com/', @@ -53,11 +68,15 @@ void main() { headers: const {}, ), true); - expect(api.passedUniversalLinksOnly, false); + + verify(api.launchUrl(any, false)).called(1); + verifyNever(api.openUrlInSafariViewController(any)); }); test('launch failure', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.launchUrl(any, any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.failure), + ); expect( await launcher.launch( 'unknown://scheme', @@ -69,11 +88,32 @@ void main() { headers: const {}, ), false); - expect(api.passedUniversalLinksOnly, false); + verify(api.launchUrl(any, false)).called(1); + verifyNever(api.openUrlInSafariViewController(any)); }); test('launch invalid URL passes the PlatformException through', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.launchUrl(any, any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.invalidUrl), + ); + await expectLater( + launcher.launch( + 'invalid://u r l', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ), + throwsA(isA())); + }); + + test('launch failed to load passes the PlatformException through', + () async { + when(api.launchUrl(any, any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.failedToLoad), + ); await expectLater( launcher.launch( 'invalid://u r l', @@ -88,7 +128,9 @@ void main() { }); test('launch force SafariVC', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.openUrlInSafariViewController(any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.success), + ); expect( await launcher.launch( 'http://example.com/', @@ -100,11 +142,14 @@ void main() { headers: const {}, ), true); - expect(api.usedSafariViewController, true); + verify(api.openUrlInSafariViewController(any)).called(1); + verifyNever(api.launchUrl(any, any)); }); test('launch universal links only', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.launchUrl(any, any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.success), + ); expect( await launcher.launch( 'http://example.com/', @@ -116,11 +161,14 @@ void main() { headers: const {}, ), true); - expect(api.passedUniversalLinksOnly, true); + verify(api.launchUrl(any, true)).called(1); + verifyNever(api.openUrlInSafariViewController(any)); }); test('launch force SafariVC to false', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + when(api.launchUrl(any, any)).thenAnswer( + (_) async => LaunchResultDetails(result: LaunchResult.success), + ); expect( await launcher.launch( 'http://example.com/', @@ -132,58 +180,13 @@ void main() { headers: const {}, ), true); - expect(api.usedSafariViewController, false); + verify(api.launchUrl(any, false)).called(1); + verifyNever(api.openUrlInSafariViewController(any)); }); test('closeWebView default behavior', () async { - final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); await launcher.closeWebView(); - expect(api.closed, true); + verify(api.closeSafariViewController()).called(1); }); }); } - -/// A fake implementation of the host API that reacts to specific schemes. -/// -/// See _isLaunchable for the behaviors. -class _FakeUrlLauncherApi implements UrlLauncherApi { - bool? passedUniversalLinksOnly; - bool? usedSafariViewController; - bool? closed; - - @override - Future canLaunchUrl(String url) async { - return _isLaunchable(url); - } - - @override - Future launchUrl(String url, bool universalLinksOnly) async { - passedUniversalLinksOnly = universalLinksOnly; - usedSafariViewController = false; - return _isLaunchable(url); - } - - @override - Future openUrlInSafariViewController(String url) async { - usedSafariViewController = true; - return _isLaunchable(url); - } - - @override - Future closeSafariViewController() async { - closed = true; - } - - bool _isLaunchable(String url) { - final String scheme = url.split(':')[0]; - switch (scheme) { - case 'http': - case 'https': - return true; - case 'invalid': - throw PlatformException(code: 'argument_error'); - default: - return false; - } - } -} diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart new file mode 100644 index 00000000000..1bd27eb7d54 --- /dev/null +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart @@ -0,0 +1,111 @@ +// Mocks generated by Mockito 5.4.2 from annotations +// in url_launcher_ios/test/url_launcher_ios_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:url_launcher_ios/src/messages.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeLaunchResultDetails_0 extends _i1.SmartFake + implements _i2.LaunchResultDetails { + _FakeLaunchResultDetails_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [UrlLauncherApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUrlLauncherApi extends _i1.Mock implements _i2.UrlLauncherApi { + MockUrlLauncherApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.LaunchResultDetails> canLaunchUrl(String? arg_url) => + (super.noSuchMethod( + Invocation.method( + #canLaunchUrl, + [arg_url], + ), + returnValue: _i3.Future<_i2.LaunchResultDetails>.value( + _FakeLaunchResultDetails_0( + this, + Invocation.method( + #canLaunchUrl, + [arg_url], + ), + )), + ) as _i3.Future<_i2.LaunchResultDetails>); + + @override + _i3.Future<_i2.LaunchResultDetails> launchUrl( + String? arg_url, + bool? arg_universalLinksOnly, + ) => + (super.noSuchMethod( + Invocation.method( + #launchUrl, + [ + arg_url, + arg_universalLinksOnly, + ], + ), + returnValue: _i3.Future<_i2.LaunchResultDetails>.value( + _FakeLaunchResultDetails_0( + this, + Invocation.method( + #launchUrl, + [ + arg_url, + arg_universalLinksOnly, + ], + ), + )), + ) as _i3.Future<_i2.LaunchResultDetails>); + + @override + _i3.Future<_i2.LaunchResultDetails> openUrlInSafariViewController( + String? arg_url) => + (super.noSuchMethod( + Invocation.method( + #openUrlInSafariViewController, + [arg_url], + ), + returnValue: _i3.Future<_i2.LaunchResultDetails>.value( + _FakeLaunchResultDetails_0( + this, + Invocation.method( + #openUrlInSafariViewController, + [arg_url], + ), + )), + ) as _i3.Future<_i2.LaunchResultDetails>); + + @override + _i3.Future closeSafariViewController() => (super.noSuchMethod( + Invocation.method( + #closeSafariViewController, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} From 4d4f1bb7938f7206d84525f28c1c9cb6c445cfa7 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Thu, 19 Oct 2023 00:11:09 -0500 Subject: [PATCH 24/29] addresses feedback --- .../ios/RunnerTests/URLLauncherTests.swift | 42 ++++----- .../ios/Classes/Launcher.swift | 9 +- .../ios/Classes/URLLaunchSession.swift | 9 +- .../ios/Classes/URLLauncherPlugin.swift | 33 +++---- .../ios/Classes/messages.g.swift | 90 +++---------------- .../url_launcher_ios/lib/src/messages.g.dart | 77 ++-------------- .../lib/url_launcher_ios.dart | 32 +++---- .../url_launcher_ios/pigeons/messages.dart | 28 +----- .../test/url_launcher_ios_test.dart | 29 +++--- .../test/url_launcher_ios_test.mocks.dart | 57 +++--------- 10 files changed, 100 insertions(+), 306 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index 27f5127db5a..4cdd35cc63e 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -20,26 +20,20 @@ final class URLLauncherTests: XCTestCase { func testCanLaunchSuccess() { let result = createPlugin().canLaunchUrl(url: "good://url") - XCTAssertEqual(result.result, .success) + XCTAssertEqual(result, .success) } func testCanLaunchFailure() { let result = createPlugin().canLaunchUrl(url: "bad://url") - XCTAssertEqual(result.result, .failure) + XCTAssertEqual(result, .failedToLoad) } func testCanLaunchFailureWithInvalidURL() { let result = createPlugin().canLaunchUrl(url: "urls can't have spaces") - if result.result == .failure { - // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't - // fail to parse URLs, so the test must allow for either outcome. - XCTAssertNil(result.errorMessage) - XCTAssertNil(result.errorDetails) - } else { - XCTAssertEqual(result.result, .invalidUrl) - XCTAssertEqual(result.errorMessage, "Unable to parse URL") - XCTAssertEqual(result.errorDetails, "Provided URL: urls can't have spaces") - } + + // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't + // fail to parse URLs, so the test must allow for either outcome. + XCTAssertTrue(result == .failedToLoad || result == .invalidUrl) } func testLaunchSuccess() { @@ -47,7 +41,7 @@ final class URLLauncherTests: XCTestCase { createPlugin().launchUrl(url: "good://url", universalLinksOnly: false) { result in switch result { case .success(let details): - XCTAssertEqual(details.result, .success) + XCTAssertEqual(details, .success) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -62,7 +56,7 @@ final class URLLauncherTests: XCTestCase { createPlugin().launchUrl(url: "bad://url", universalLinksOnly: false) { result in switch result { case .success(let details): - XCTAssertEqual(details.result, .failure) + XCTAssertEqual(details, .failedToLoad) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -77,16 +71,9 @@ final class URLLauncherTests: XCTestCase { createPlugin().launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in switch result { case .success(let details): - if details.result == .failure { - // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't - // fail to parse URLs, so the test must allow for either outcome. - XCTAssertNil(details.errorMessage) - XCTAssertNil(details.errorDetails) - } else { - XCTAssertEqual(details.result, .invalidUrl) - XCTAssertEqual(details.errorMessage, "Unable to parse URL") - XCTAssertEqual(details.errorDetails, "Provided URL: urls can't have spaces") - } + // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't + // fail to parse URLs, so the test must allow for either outcome. + XCTAssertTrue(details == .failedToLoad || details == .invalidUrl) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -104,7 +91,7 @@ final class URLLauncherTests: XCTestCase { plugin.launchUrl(url: "good://url", universalLinksOnly: false) { result in switch result { case .success(let details): - XCTAssertEqual(details.result, .success) + XCTAssertEqual(details, .success) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -123,7 +110,7 @@ final class URLLauncherTests: XCTestCase { plugin.launchUrl(url: "good://url", universalLinksOnly: true) { result in switch result { case .success(let details): - XCTAssertEqual(details.result, .success) + XCTAssertEqual(details, .success) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -144,7 +131,8 @@ final private class FakeLauncher: NSObject, Launcher { } func openURL( - _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], + _ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)? ) { self.passedOptions = options diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift index 8fdc1bfb636..d02c8fe2a1e 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift @@ -6,9 +6,13 @@ /// /// This protocol exists to allow injecting an alternate implementation for testing. protocol Launcher { + /// Returns a Boolean value that indicates whether an app is available to handle a URL scheme. func canOpenURL(_ url: URL) -> Bool + + /// Attempts to asynchronously open the resource at the specified URL. func openURL( - _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], + _ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) } @@ -19,7 +23,8 @@ final class UIApplicationLauncher: Launcher { } func openURL( - _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], + _ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)? ) { UIApplication.shared.open(url, options: options, completionHandler: completion) diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift index b28fee15abd..513fef0a3ab 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -5,7 +5,7 @@ import Flutter import SafariServices -typealias OpenInSafariCompletionHandler = (Result) -> Void +typealias OpenInSafariCompletionHandler = (Result) -> Void /// A session responsible for launching a URL in Safari and handling its events. final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { @@ -41,12 +41,9 @@ final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { _ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool ) { if didLoadSuccessfully { - completion(.success(LaunchResultDetails(result: .success))) + completion(.success(.success)) } else { - completion( - .success( - LaunchResultDetails(result: .failedToLoad, errorMessage: "Error while launching \(url)") - )) + completion(.success(.failedToLoad)) } } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index ab76a406377..19f58f1c723 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -15,7 +15,7 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { private var currentSession: URLLaunchSession? private let launcher: Launcher - var topViewController: UIViewController? { + private var topViewController: UIViewController? { // TODO(stuartmorgan) Provide a non-deprecated codepath. See // https://github.com/flutter/flutter/issues/104117 UIApplication.shared.keyWindow?.rootViewController?.topViewController @@ -25,34 +25,33 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { self.launcher = launcher } - func canLaunchUrl(url: String) -> LaunchResultDetails { + func canLaunchUrl(url: String) -> LaunchResult { guard let url = URL(string: url) else { - return invalidURLError(for: url) + return .failedToLoad } let canOpen = launcher.canOpenURL(url) - return LaunchResultDetails(result: canOpen ? .success : .failure) + return canOpen ? .success : .failedToLoad } func launchUrl( url: String, universalLinksOnly: Bool, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { - completion(.success(invalidURLError(for: url))) + completion(.success(.failedToLoad)) return } let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] - launcher.openURL(url, options: options) { success in - let result = LaunchResultDetails(result: success ? .success : .failure) - completion(.success(result)) + launcher.openURL(url, options: options) { result in + completion(.success(result ? .success : .failedToLoad)) } } func openUrlInSafariViewController( - url: String, completion: @escaping (Result) -> Void + url: String, completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { - completion(.success(invalidURLError(for: url))) + completion(.success(.failedToLoad)) return } @@ -68,18 +67,6 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { func closeSafariViewController() throws { currentSession?.close() } - - /** - * Creates an error for an invalid URL string. - * - * @param url The invalid URL string - * @return The error to return - */ - func invalidURLError(for url: String) -> LaunchResultDetails { - LaunchResultDetails( - result: .invalidUrl, errorMessage: "Unable to parse URL", errorDetails: "Provided URL: \(url)" - ) - } } /// This method recursively iterates through the view hierarchy diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift index 3111302597e..7fb90f9fa5c 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -47,90 +47,23 @@ enum LaunchResult: Int { /// The URL was successfully launched. case success = 0 /// The URL could not be launched - case failure = 1 + case failedToLoad = 1 /// The URL was not launched because it is not invalid URL case invalidUrl = 2 - /// The URL did not load successfully in the SFSafariViewController. - case failedToLoad = 3 -} - -/// Generated class from Pigeon that represents data sent in messages. -struct LaunchResultDetails { - /// The result of the launch attempt. - var result: LaunchResult - /// A system-provided error message, if any. - var errorMessage: String? = nil - /// A system-provided error details, if any. - var errorDetails: String? = nil - - static func fromList(_ list: [Any?]) -> LaunchResultDetails? { - let result = LaunchResult(rawValue: list[0] as! Int)! - let errorMessage: String? = nilOrValue(list[1]) - let errorDetails: String? = nilOrValue(list[2]) - - return LaunchResultDetails( - result: result, - errorMessage: errorMessage, - errorDetails: errorDetails - ) - } - func toList() -> [Any?] { - return [ - result.rawValue, - errorMessage, - errorDetails, - ] - } -} - -private class UrlLauncherApiCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 128: - return LaunchResultDetails.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class UrlLauncherApiCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? LaunchResultDetails { - super.writeByte(128) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class UrlLauncherApiCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return UrlLauncherApiCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return UrlLauncherApiCodecWriter(data: data) - } -} - -class UrlLauncherApiCodec: FlutterStandardMessageCodec { - static let shared = UrlLauncherApiCodec(readerWriter: UrlLauncherApiCodecReaderWriter()) } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol UrlLauncherApi { /// Returns true if the URL can definitely be launched. - func canLaunchUrl(url: String) throws -> LaunchResultDetails + func canLaunchUrl(url: String) throws -> LaunchResult /// Opens the URL externally, returning true if successful. func launchUrl( url: String, universalLinksOnly: Bool, - completion: @escaping (Result) -> Void) + completion: @escaping (Result) -> Void) /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. func openUrlInSafariViewController( - url: String, completion: @escaping (Result) -> Void) + url: String, completion: @escaping (Result) -> Void) /// Closes the view controller opened by [openUrlInSafariViewController]. func closeSafariViewController() throws } @@ -138,20 +71,19 @@ protocol UrlLauncherApi { /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class UrlLauncherApiSetup { /// The codec used by UrlLauncherApi. - static var codec: FlutterStandardMessageCodec { UrlLauncherApiCodec.shared } /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { /// Returns true if the URL can definitely be launched. let canLaunchUrlChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", - binaryMessenger: binaryMessenger, codec: codec) + binaryMessenger: binaryMessenger) if let api = api { canLaunchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] let urlArg = args[0] as! String do { let result = try api.canLaunchUrl(url: urlArg) - reply(wrapResult(result)) + reply(wrapResult(result.rawValue)) } catch { reply(wrapError(error)) } @@ -162,7 +94,7 @@ class UrlLauncherApiSetup { /// Opens the URL externally, returning true if successful. let launchUrlChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", - binaryMessenger: binaryMessenger, codec: codec) + binaryMessenger: binaryMessenger) if let api = api { launchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -171,7 +103,7 @@ class UrlLauncherApiSetup { api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in switch result { case .success(let res): - reply(wrapResult(res)) + reply(wrapResult(res.rawValue)) case .failure(let error): reply(wrapError(error)) } @@ -184,7 +116,7 @@ class UrlLauncherApiSetup { /// when it has loaded successfully. let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", - binaryMessenger: binaryMessenger, codec: codec) + binaryMessenger: binaryMessenger) if let api = api { openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -192,7 +124,7 @@ class UrlLauncherApiSetup { api.openUrlInSafariViewController(url: urlArg) { result in switch result { case .success(let res): - reply(wrapResult(res)) + reply(wrapResult(res.rawValue)) case .failure(let error): reply(wrapError(error)) } @@ -204,7 +136,7 @@ class UrlLauncherApiSetup { /// Closes the view controller opened by [openUrlInSafariViewController]. let closeSafariViewControllerChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController", - binaryMessenger: binaryMessenger, codec: codec) + binaryMessenger: binaryMessenger) if let api = api { closeSafariViewControllerChannel.setMessageHandler { _, reply in do { diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index 3c48bcbd6e4..0c9fc321988 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -17,70 +17,10 @@ enum LaunchResult { success, /// The URL could not be launched - failure, + failedToLoad, /// The URL was not launched because it is not invalid URL invalidUrl, - - /// The URL did not load successfully in the SFSafariViewController. - failedToLoad, -} - -class LaunchResultDetails { - LaunchResultDetails({ - required this.result, - this.errorMessage, - this.errorDetails, - }); - - /// The result of the launch attempt. - LaunchResult result; - - /// A system-provided error message, if any. - String? errorMessage; - - /// A system-provided error details, if any. - String? errorDetails; - - Object encode() { - return [ - result.index, - errorMessage, - errorDetails, - ]; - } - - static LaunchResultDetails decode(Object result) { - result as List; - return LaunchResultDetails( - result: LaunchResult.values[result[0]! as int], - errorMessage: result[1] as String?, - errorDetails: result[2] as String?, - ); - } -} - -class _UrlLauncherApiCodec extends StandardMessageCodec { - const _UrlLauncherApiCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is LaunchResultDetails) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 128: - return LaunchResultDetails.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } } class UrlLauncherApi { @@ -91,10 +31,10 @@ class UrlLauncherApi { : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; - static const MessageCodec codec = _UrlLauncherApiCodec(); + static const MessageCodec codec = StandardMessageCodec(); /// Returns true if the URL can definitely be launched. - Future canLaunchUrl(String arg_url) async { + Future canLaunchUrl(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl', codec, @@ -118,12 +58,12 @@ class UrlLauncherApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as LaunchResultDetails?)!; + return LaunchResult.values[replyList[0]! as int]; } } /// Opens the URL externally, returning true if successful. - Future launchUrl( + Future launchUrl( String arg_url, bool arg_universalLinksOnly) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl', codec, @@ -147,14 +87,13 @@ class UrlLauncherApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as LaunchResultDetails?)!; + return LaunchResult.values[replyList[0]! as int]; } } /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - Future openUrlInSafariViewController( - String arg_url) async { + Future openUrlInSafariViewController(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController', codec, @@ -178,7 +117,7 @@ class UrlLauncherApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as LaunchResultDetails?)!; + return LaunchResult.values[replyList[0]! as int]; } } diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart index d2658f12eb1..7324df2c14a 100644 --- a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart +++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart @@ -28,8 +28,8 @@ class UrlLauncherIOS extends UrlLauncherPlatform { @override Future canLaunch(String url) async { - final LaunchResultDetails result = await _hostApi.canLaunchUrl(url); - return _mapLaunchResults(results: result); + final LaunchResult result = await _hostApi.canLaunchUrl(url); + return _mapLaunchResults(results: result, url: url); } @override @@ -48,12 +48,12 @@ class UrlLauncherIOS extends UrlLauncherPlatform { required Map headers, String? webOnlyWindowName, }) async { - final LaunchResultDetails result = + final LaunchResult result = await _launchUrl(useSafariVC, url, universalLinksOnly); - return _mapLaunchResults(results: result); + return _mapLaunchResults(results: result, url: url); } - Future _launchUrl( + Future _launchUrl( bool useSafariVC, String url, bool universalLinksOnly) { if (useSafariVC) { return _hostApi.openUrlInSafariViewController(url); @@ -62,23 +62,23 @@ class UrlLauncherIOS extends UrlLauncherPlatform { } } - bool _mapLaunchResults({required final LaunchResultDetails results}) { - switch (results.result) { + bool _mapLaunchResults({ + required final LaunchResult results, + required String url, + }) { + // Replace this in https://github.com/flutter/flutter/issues/127665 + // This is temporary since FlutterError is not a NSError. + // The PlatformExceptions thrown here are for compatibility with the + // previous Objective-C implementation. + switch (results) { case LaunchResult.success: return true; - case LaunchResult.failure: + case LaunchResult.failedToLoad: return false; case LaunchResult.invalidUrl: throw PlatformException( code: 'invalidUrl', - message: results.errorMessage, - details: results.errorDetails, - ); - case LaunchResult.failedToLoad: - throw PlatformException( - code: 'failedToLoad', - message: results.errorMessage, - details: results.errorDetails, + message: 'Unable to parse URL $url', ); } } diff --git a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart index fd699c79fb3..049cc0a66f5 100644 --- a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart +++ b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart @@ -16,48 +16,28 @@ enum LaunchResult { success, /// The URL could not be launched - failure, + failedToLoad, /// The URL was not launched because it is not invalid URL invalidUrl, - - /// The URL did not load successfully in the SFSafariViewController. - failedToLoad, -} - -class LaunchResultDetails { - LaunchResultDetails({ - required this.result, - this.errorMessage, - this.errorDetails, - }); - - /// The result of the launch attempt. - final LaunchResult result; - - /// A system-provided error message, if any. - final String? errorMessage; - - /// A system-provided error details, if any. - final String? errorDetails; } @HostApi() abstract class UrlLauncherApi { /// Returns true if the URL can definitely be launched. @ObjCSelector('canLaunchURL:') - LaunchResultDetails canLaunchUrl(String url); + LaunchResult canLaunchUrl(String url); /// Opens the URL externally, returning true if successful. @async @ObjCSelector('launchURL:universalLinksOnly:') - LaunchResultDetails launchUrl(String url, bool universalLinksOnly); + LaunchResult launchUrl(String url, bool universalLinksOnly); /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. @async @ObjCSelector('openSafariViewControllerWithURL:') - LaunchResultDetails openUrlInSafariViewController(String url); + LaunchResult openUrlInSafariViewController(String url); /// Closes the view controller opened by [openUrlInSafariViewController]. void closeSafariViewController(); diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart index 1be85d7f706..6d091a36f45 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -32,14 +32,14 @@ void main() { test('canLaunch success', () async { when(api.canLaunchUrl(any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.success), + (_) async => LaunchResult.success, ); expect(await launcher.canLaunch('http://example.com/'), true); }); test('canLaunch failure', () async { when(api.canLaunchUrl(any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.failure), + (_) async => LaunchResult.failedToLoad, ); expect(await launcher.canLaunch('unknown://scheme'), false); }); @@ -47,7 +47,7 @@ void main() { test('canLaunch invalid URL passes the PlatformException through', () async { when(api.canLaunchUrl(any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.invalidUrl), + (_) async => LaunchResult.invalidUrl, ); await expectLater(launcher.canLaunch('invalid://u r l'), throwsA(isA())); @@ -55,7 +55,7 @@ void main() { test('launch success', () async { when(api.launchUrl(any, any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.success), + (_) async => LaunchResult.success, ); expect( await launcher.launch( @@ -75,7 +75,7 @@ void main() { test('launch failure', () async { when(api.launchUrl(any, any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.failure), + (_) async => LaunchResult.failedToLoad, ); expect( await launcher.launch( @@ -94,7 +94,7 @@ void main() { test('launch invalid URL passes the PlatformException through', () async { when(api.launchUrl(any, any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.invalidUrl), + (_) async => LaunchResult.invalidUrl, ); await expectLater( launcher.launch( @@ -109,13 +109,12 @@ void main() { throwsA(isA())); }); - test('launch failed to load passes the PlatformException through', - () async { + test('launch failed to load URL returns false', () async { when(api.launchUrl(any, any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.failedToLoad), + (_) async => LaunchResult.failedToLoad, ); - await expectLater( - launcher.launch( + expect( + await launcher.launch( 'invalid://u r l', useSafariVC: false, useWebView: false, @@ -124,12 +123,12 @@ void main() { universalLinksOnly: false, headers: const {}, ), - throwsA(isA())); + false); }); test('launch force SafariVC', () async { when(api.openUrlInSafariViewController(any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.success), + (_) async => LaunchResult.success, ); expect( await launcher.launch( @@ -148,7 +147,7 @@ void main() { test('launch universal links only', () async { when(api.launchUrl(any, any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.success), + (_) async => LaunchResult.success, ); expect( await launcher.launch( @@ -167,7 +166,7 @@ void main() { test('launch force SafariVC to false', () async { when(api.launchUrl(any, any)).thenAnswer( - (_) async => LaunchResultDetails(result: LaunchResult.success), + (_) async => LaunchResult.success, ); expect( await launcher.launch( diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart index 1bd27eb7d54..534fefb1693 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart @@ -19,17 +19,6 @@ import 'package:url_launcher_ios/src/messages.g.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeLaunchResultDetails_0 extends _i1.SmartFake - implements _i2.LaunchResultDetails { - _FakeLaunchResultDetails_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [UrlLauncherApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -39,24 +28,18 @@ class MockUrlLauncherApi extends _i1.Mock implements _i2.UrlLauncherApi { } @override - _i3.Future<_i2.LaunchResultDetails> canLaunchUrl(String? arg_url) => + _i3.Future<_i2.LaunchResult> canLaunchUrl(String? arg_url) => (super.noSuchMethod( Invocation.method( #canLaunchUrl, [arg_url], ), - returnValue: _i3.Future<_i2.LaunchResultDetails>.value( - _FakeLaunchResultDetails_0( - this, - Invocation.method( - #canLaunchUrl, - [arg_url], - ), - )), - ) as _i3.Future<_i2.LaunchResultDetails>); + returnValue: + _i3.Future<_i2.LaunchResult>.value(_i2.LaunchResult.success), + ) as _i3.Future<_i2.LaunchResult>); @override - _i3.Future<_i2.LaunchResultDetails> launchUrl( + _i3.Future<_i2.LaunchResult> launchUrl( String? arg_url, bool? arg_universalLinksOnly, ) => @@ -68,36 +51,20 @@ class MockUrlLauncherApi extends _i1.Mock implements _i2.UrlLauncherApi { arg_universalLinksOnly, ], ), - returnValue: _i3.Future<_i2.LaunchResultDetails>.value( - _FakeLaunchResultDetails_0( - this, - Invocation.method( - #launchUrl, - [ - arg_url, - arg_universalLinksOnly, - ], - ), - )), - ) as _i3.Future<_i2.LaunchResultDetails>); + returnValue: + _i3.Future<_i2.LaunchResult>.value(_i2.LaunchResult.success), + ) as _i3.Future<_i2.LaunchResult>); @override - _i3.Future<_i2.LaunchResultDetails> openUrlInSafariViewController( - String? arg_url) => + _i3.Future<_i2.LaunchResult> openUrlInSafariViewController(String? arg_url) => (super.noSuchMethod( Invocation.method( #openUrlInSafariViewController, [arg_url], ), - returnValue: _i3.Future<_i2.LaunchResultDetails>.value( - _FakeLaunchResultDetails_0( - this, - Invocation.method( - #openUrlInSafariViewController, - [arg_url], - ), - )), - ) as _i3.Future<_i2.LaunchResultDetails>); + returnValue: + _i3.Future<_i2.LaunchResult>.value(_i2.LaunchResult.success), + ) as _i3.Future<_i2.LaunchResult>); @override _i3.Future closeSafariViewController() => (super.noSuchMethod( From 7b6272f7577e7eb0cd540684b21cacebd292fa01 Mon Sep 17 00:00:00 2001 From: Chris Langham Date: Wed, 25 Oct 2023 23:21:02 -0500 Subject: [PATCH 25/29] addressing feedback --- .../ios/Classes/URLLaunchSession.swift | 3 +- .../ios/Classes/URLLauncherPlugin.swift | 8 +-- .../ios/Classes/messages.g.swift | 54 ++++++++----------- .../url_launcher_ios/lib/src/messages.g.dart | 4 +- .../lib/url_launcher_ios.dart | 7 ++- .../url_launcher_ios/pigeons/messages.dart | 4 +- .../test/url_launcher_ios_test.dart | 2 +- 7 files changed, 36 insertions(+), 46 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift index 513fef0a3ab..18538ef42f2 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -38,7 +38,8 @@ final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { /// - controller: The Safari view controller. /// - didLoadSuccessfully: Indicates if the initial load was successful. func safariViewController( - _ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool + _ controller: SFSafariViewController, + didCompleteInitialLoad didLoadSuccessfully: Bool ) { if didLoadSuccessfully { completion(.success(.success)) diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index 19f58f1c723..31299963680 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -34,7 +34,8 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { } func launchUrl( - url: String, universalLinksOnly: Bool, + url: String, + universalLinksOnly: Bool, completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { @@ -48,7 +49,8 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { } func openUrlInSafariViewController( - url: String, completion: @escaping (Result) -> Void + url: String, + completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { completion(.success(.failedToLoad)) @@ -64,7 +66,7 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { topViewController?.present(session.safariViewController, animated: true, completion: nil) } - func closeSafariViewController() throws { + func closeSafariViewController() { currentSession?.close() } } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift index 7fb90f9fa5c..ac0d89b2d89 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -5,13 +5,12 @@ // See also: https://pub.dev/packages/pigeon import Foundation - #if os(iOS) - import Flutter +import Flutter #elseif os(macOS) - import FlutterMacOS +import FlutterMacOS #else - #error("Unsupported platform.") +#error("Unsupported platform.") #endif private func isNullish(_ value: Any?) -> Bool { @@ -27,13 +26,13 @@ private func wrapError(_ error: Any) -> [Any?] { return [ flutterError.code, flutterError.message, - flutterError.details, + flutterError.details ] } return [ "\(error)", "\(type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", + "Stacktrace: \(Thread.callStackSymbols)" ] } @@ -46,9 +45,9 @@ private func nilOrValue(_ value: Any?) -> T? { enum LaunchResult: Int { /// The URL was successfully launched. case success = 0 - /// The URL could not be launched + /// The URL could not be launched. case failedToLoad = 1 - /// The URL was not launched because it is not invalid URL + /// The URL was not launched because the URL is invalid. case invalidUrl = 2 } @@ -57,13 +56,10 @@ protocol UrlLauncherApi { /// Returns true if the URL can definitely be launched. func canLaunchUrl(url: String) throws -> LaunchResult /// Opens the URL externally, returning true if successful. - func launchUrl( - url: String, universalLinksOnly: Bool, - completion: @escaping (Result) -> Void) + func launchUrl(url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - func openUrlInSafariViewController( - url: String, completion: @escaping (Result) -> Void) + func openUrlInSafariViewController(url: String, completion: @escaping (Result) -> Void) /// Closes the view controller opened by [openUrlInSafariViewController]. func closeSafariViewController() throws } @@ -74,9 +70,7 @@ class UrlLauncherApiSetup { /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { /// Returns true if the URL can definitely be launched. - let canLaunchUrlChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", - binaryMessenger: binaryMessenger) + let canLaunchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", binaryMessenger: binaryMessenger) if let api = api { canLaunchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -92,9 +86,7 @@ class UrlLauncherApiSetup { canLaunchUrlChannel.setMessageHandler(nil) } /// Opens the URL externally, returning true if successful. - let launchUrlChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", - binaryMessenger: binaryMessenger) + let launchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", binaryMessenger: binaryMessenger) if let api = api { launchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -102,10 +94,10 @@ class UrlLauncherApiSetup { let universalLinksOnlyArg = args[1] as! Bool api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in switch result { - case .success(let res): - reply(wrapResult(res.rawValue)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res.rawValue)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -114,19 +106,17 @@ class UrlLauncherApiSetup { } /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", - binaryMessenger: binaryMessenger) + let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", binaryMessenger: binaryMessenger) if let api = api { openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in let args = message as! [Any?] let urlArg = args[0] as! String api.openUrlInSafariViewController(url: urlArg) { result in switch result { - case .success(let res): - reply(wrapResult(res.rawValue)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res.rawValue)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -134,9 +124,7 @@ class UrlLauncherApiSetup { openUrlInSafariViewControllerChannel.setMessageHandler(nil) } /// Closes the view controller opened by [openUrlInSafariViewController]. - let closeSafariViewControllerChannel = FlutterBasicMessageChannel( - name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController", - binaryMessenger: binaryMessenger) + let closeSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController", binaryMessenger: binaryMessenger) if let api = api { closeSafariViewControllerChannel.setMessageHandler { _, reply in do { diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index 0c9fc321988..e860faf63b1 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -16,10 +16,10 @@ enum LaunchResult { /// The URL was successfully launched. success, - /// The URL could not be launched + /// The URL could not be launched. failedToLoad, - /// The URL was not launched because it is not invalid URL + /// The URL was not launched because the URL is invalid. invalidUrl, } diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart index 7324df2c14a..c721f9bd0e0 100644 --- a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart +++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart @@ -66,16 +66,15 @@ class UrlLauncherIOS extends UrlLauncherPlatform { required final LaunchResult results, required String url, }) { - // Replace this in https://github.com/flutter/flutter/issues/127665 - // This is temporary since FlutterError is not a NSError. - // The PlatformExceptions thrown here are for compatibility with the - // previous Objective-C implementation. switch (results) { case LaunchResult.success: return true; case LaunchResult.failedToLoad: return false; case LaunchResult.invalidUrl: + // TODO(stuartmorgan): Remove this as part of standardizing error handling. See + // flutter/flutter#127665 + // The PlatformExceptions thrown here is for compatibility with the previous implementation. throw PlatformException( code: 'invalidUrl', message: 'Unable to parse URL $url', diff --git a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart index 049cc0a66f5..5b2e66ee267 100644 --- a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart +++ b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart @@ -15,10 +15,10 @@ enum LaunchResult { /// The URL was successfully launched. success, - /// The URL could not be launched + /// The URL could not be launched. failedToLoad, - /// The URL was not launched because it is not invalid URL + /// The URL was not launched because the URL is invalid. invalidUrl, } diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart index 6d091a36f45..493bb948553 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -115,7 +115,7 @@ void main() { ); expect( await launcher.launch( - 'invalid://u r l', + 'unknown://scheme', useSafariVC: false, useWebView: false, enableJavaScript: false, From e7e011a2dbcdf722490823a4469890f938d27fa3 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 26 Oct 2023 10:33:30 -0400 Subject: [PATCH 26/29] Replace default launcher implementation with conformance extension --- .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../ios/RunnerTests/URLLauncherTests.swift | 2 +- .../ios/Classes/Launcher.swift | 18 +++--------------- .../ios/Classes/URLLauncherPlugin.swift | 4 ++-- 5 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index dc5aefce899..c10bff136dc 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -269,7 +269,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ad0ebfab1b8..fa4e0bbd319 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Void)? diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift index d02c8fe2a1e..f97db9db9c5 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/Launcher.swift @@ -10,23 +10,11 @@ protocol Launcher { func canOpenURL(_ url: URL) -> Bool /// Attempts to asynchronously open the resource at the specified URL. - func openURL( + func open( _ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler completion: ((Bool) -> Void)?) } -/// Default implementation of Launcher, using UIApplication. -final class UIApplicationLauncher: Launcher { - func canOpenURL(_ url: URL) -> Bool { - UIApplication.shared.canOpenURL(url) - } - - func openURL( - _ url: URL, - options: [UIApplication.OpenExternalURLOptionsKey: Any], - completionHandler completion: ((Bool) -> Void)? - ) { - UIApplication.shared.open(url, options: options, completionHandler: completion) - } -} +/// Launcher is intentionally a direct passthroguh to UIApplication. +extension UIApplication: Launcher {} diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index 31299963680..4b48fe4d387 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -21,7 +21,7 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { UIApplication.shared.keyWindow?.rootViewController?.topViewController } - init(launcher: Launcher = UIApplicationLauncher()) { + init(launcher: Launcher = UIApplication.shared) { self.launcher = launcher } @@ -43,7 +43,7 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { return } let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] - launcher.openURL(url, options: options) { result in + launcher.open(url, options: options) { result in completion(.success(result ? .success : .failedToLoad)) } } From d955ef8d75ea6151f294e51fc5e57a4cccc4931c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 26 Oct 2023 10:36:54 -0400 Subject: [PATCH 27/29] swift-format --- .../ios/Classes/URLLauncherPlugin.swift | 3 +- .../ios/Classes/messages.g.swift | 50 ++++++++++++------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index 4b48fe4d387..4ca8aa7858e 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -84,7 +84,8 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { extension UIViewController { var topViewController: UIViewController { if let navigationController = self as? UINavigationController { - return navigationController.viewControllers.last?.topViewController ?? navigationController + return navigationController.viewControllers.last?.topViewController + ?? navigationController .visibleViewController ?? navigationController } if let tabBarController = self as? UITabBarController { diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift index ac0d89b2d89..76b58747985 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -5,12 +5,13 @@ // See also: https://pub.dev/packages/pigeon import Foundation + #if os(iOS) -import Flutter + import Flutter #elseif os(macOS) -import FlutterMacOS + import FlutterMacOS #else -#error("Unsupported platform.") + #error("Unsupported platform.") #endif private func isNullish(_ value: Any?) -> Bool { @@ -26,13 +27,13 @@ private func wrapError(_ error: Any) -> [Any?] { return [ flutterError.code, flutterError.message, - flutterError.details + flutterError.details, ] } return [ "\(error)", "\(type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)" + "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -56,10 +57,13 @@ protocol UrlLauncherApi { /// Returns true if the URL can definitely be launched. func canLaunchUrl(url: String) throws -> LaunchResult /// Opens the URL externally, returning true if successful. - func launchUrl(url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) + func launchUrl( + url: String, universalLinksOnly: Bool, + completion: @escaping (Result) -> Void) /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - func openUrlInSafariViewController(url: String, completion: @escaping (Result) -> Void) + func openUrlInSafariViewController( + url: String, completion: @escaping (Result) -> Void) /// Closes the view controller opened by [openUrlInSafariViewController]. func closeSafariViewController() throws } @@ -70,7 +74,9 @@ class UrlLauncherApiSetup { /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { /// Returns true if the URL can definitely be launched. - let canLaunchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", binaryMessenger: binaryMessenger) + let canLaunchUrlChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", + binaryMessenger: binaryMessenger) if let api = api { canLaunchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -86,7 +92,9 @@ class UrlLauncherApiSetup { canLaunchUrlChannel.setMessageHandler(nil) } /// Opens the URL externally, returning true if successful. - let launchUrlChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", binaryMessenger: binaryMessenger) + let launchUrlChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", + binaryMessenger: binaryMessenger) if let api = api { launchUrlChannel.setMessageHandler { message, reply in let args = message as! [Any?] @@ -94,10 +102,10 @@ class UrlLauncherApiSetup { let universalLinksOnlyArg = args[1] as! Bool api.launchUrl(url: urlArg, universalLinksOnly: universalLinksOnlyArg) { result in switch result { - case .success(let res): - reply(wrapResult(res.rawValue)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res.rawValue)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -106,17 +114,19 @@ class UrlLauncherApiSetup { } /// Opens the URL in an in-app SFSafariViewController, returning true /// when it has loaded successfully. - let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", binaryMessenger: binaryMessenger) + let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", + binaryMessenger: binaryMessenger) if let api = api { openUrlInSafariViewControllerChannel.setMessageHandler { message, reply in let args = message as! [Any?] let urlArg = args[0] as! String api.openUrlInSafariViewController(url: urlArg) { result in switch result { - case .success(let res): - reply(wrapResult(res.rawValue)) - case .failure(let error): - reply(wrapError(error)) + case .success(let res): + reply(wrapResult(res.rawValue)) + case .failure(let error): + reply(wrapError(error)) } } } @@ -124,7 +134,9 @@ class UrlLauncherApiSetup { openUrlInSafariViewControllerChannel.setMessageHandler(nil) } /// Closes the view controller opened by [openUrlInSafariViewController]. - let closeSafariViewControllerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController", binaryMessenger: binaryMessenger) + let closeSafariViewControllerChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.closeSafariViewController", + binaryMessenger: binaryMessenger) if let api = api { closeSafariViewControllerChannel.setMessageHandler { _, reply in do { From 1297683501be744be4dbc8d1f5473be986b83ccd Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 26 Oct 2023 11:13:30 -0400 Subject: [PATCH 28/29] Rework return enum to have different versions --- .../ios/RunnerTests/URLLauncherTests.swift | 8 +-- .../ios/Classes/URLLaunchSession.swift | 2 +- .../ios/Classes/URLLauncherPlugin.swift | 12 ++-- .../ios/Classes/messages.g.swift | 34 ++++++---- .../url_launcher_ios/lib/src/messages.g.dart | 30 ++++++--- .../lib/url_launcher_ios.dart | 67 +++++++++++++------ .../url_launcher_ios/pigeons/messages.dart | 28 +++++--- .../test/url_launcher_ios_test.dart | 43 ++++++++---- .../test/url_launcher_ios_test.mocks.dart | 7 +- 9 files changed, 155 insertions(+), 76 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index 59cbd972266..aeb1c4c9da3 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -25,7 +25,7 @@ final class URLLauncherTests: XCTestCase { func testCanLaunchFailure() { let result = createPlugin().canLaunchUrl(url: "bad://url") - XCTAssertEqual(result, .failedToLoad) + XCTAssertEqual(result, .failure) } func testCanLaunchFailureWithInvalidURL() { @@ -33,7 +33,7 @@ final class URLLauncherTests: XCTestCase { // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't // fail to parse URLs, so the test must allow for either outcome. - XCTAssertTrue(result == .failedToLoad || result == .invalidUrl) + XCTAssertTrue(result == .failure || result == .invalidUrl) } func testLaunchSuccess() { @@ -56,7 +56,7 @@ final class URLLauncherTests: XCTestCase { createPlugin().launchUrl(url: "bad://url", universalLinksOnly: false) { result in switch result { case .success(let details): - XCTAssertEqual(details, .failedToLoad) + XCTAssertEqual(details, .failure) case .failure(let error): XCTFail("Unexpected error: \(error)") } @@ -73,7 +73,7 @@ final class URLLauncherTests: XCTestCase { case .success(let details): // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't // fail to parse URLs, so the test must allow for either outcome. - XCTAssertTrue(details == .failedToLoad || details == .invalidUrl) + XCTAssertTrue(details == .failure || details == .invalidUrl) case .failure(let error): XCTFail("Unexpected error: \(error)") } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift index 18538ef42f2..b0761e57f08 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLaunchSession.swift @@ -5,7 +5,7 @@ import Flutter import SafariServices -typealias OpenInSafariCompletionHandler = (Result) -> Void +typealias OpenInSafariCompletionHandler = (Result) -> Void /// A session responsible for launching a URL in Safari and handling its events. final class URLLaunchSession: NSObject, SFSafariViewControllerDelegate { diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift index 4ca8aa7858e..18800319218 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/URLLauncherPlugin.swift @@ -27,10 +27,10 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { func canLaunchUrl(url: String) -> LaunchResult { guard let url = URL(string: url) else { - return .failedToLoad + return .invalidUrl } let canOpen = launcher.canOpenURL(url) - return canOpen ? .success : .failedToLoad + return canOpen ? .success : .failure } func launchUrl( @@ -39,21 +39,21 @@ public final class URLLauncherPlugin: NSObject, FlutterPlugin, UrlLauncherApi { completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { - completion(.success(.failedToLoad)) + completion(.success(.invalidUrl)) return } let options = [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: universalLinksOnly] launcher.open(url, options: options) { result in - completion(.success(result ? .success : .failedToLoad)) + completion(.success(result ? .success : .failure)) } } func openUrlInSafariViewController( url: String, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { guard let url = URL(string: url) else { - completion(.success(.failedToLoad)) + completion(.success(.invalidUrl)) return } diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift index 76b58747985..c3b0b8a65df 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/messages.g.swift @@ -44,26 +44,36 @@ private func nilOrValue(_ value: Any?) -> T? { /// Possible outcomes of launching a URL. enum LaunchResult: Int { - /// The URL was successfully launched. + /// The URL was successfully launched (or could be, for `canLaunchUrl`). case success = 0 - /// The URL could not be launched. + /// There was no handler available for the URL. + case failure = 1 + /// The URL could not be launched because it is invalid. + case invalidUrl = 2 +} + +/// Possible outcomes of handling a URL within the application. +enum InAppLoadResult: Int { + /// The URL was successfully loaded. + case success = 0 + /// The URL did not load successfully. case failedToLoad = 1 - /// The URL was not launched because the URL is invalid. + /// The URL could not be launched because it is invalid. case invalidUrl = 2 } /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol UrlLauncherApi { - /// Returns true if the URL can definitely be launched. + /// Checks whether a URL can be loaded. func canLaunchUrl(url: String) throws -> LaunchResult - /// Opens the URL externally, returning true if successful. + /// Opens the URL externally, returning the status of launching it. func launchUrl( url: String, universalLinksOnly: Bool, completion: @escaping (Result) -> Void) - /// Opens the URL in an in-app SFSafariViewController, returning true - /// when it has loaded successfully. + /// Opens the URL in an in-app SFSafariViewController, returning the results + /// of loading it. func openUrlInSafariViewController( - url: String, completion: @escaping (Result) -> Void) + url: String, completion: @escaping (Result) -> Void) /// Closes the view controller opened by [openUrlInSafariViewController]. func closeSafariViewController() throws } @@ -73,7 +83,7 @@ class UrlLauncherApiSetup { /// The codec used by UrlLauncherApi. /// Sets up an instance of `UrlLauncherApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UrlLauncherApi?) { - /// Returns true if the URL can definitely be launched. + /// Checks whether a URL can be loaded. let canLaunchUrlChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl", binaryMessenger: binaryMessenger) @@ -91,7 +101,7 @@ class UrlLauncherApiSetup { } else { canLaunchUrlChannel.setMessageHandler(nil) } - /// Opens the URL externally, returning true if successful. + /// Opens the URL externally, returning the status of launching it. let launchUrlChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.launchUrl", binaryMessenger: binaryMessenger) @@ -112,8 +122,8 @@ class UrlLauncherApiSetup { } else { launchUrlChannel.setMessageHandler(nil) } - /// Opens the URL in an in-app SFSafariViewController, returning true - /// when it has loaded successfully. + /// Opens the URL in an in-app SFSafariViewController, returning the results + /// of loading it. let openUrlInSafariViewControllerChannel = FlutterBasicMessageChannel( name: "dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController", binaryMessenger: binaryMessenger) diff --git a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart index e860faf63b1..a7e9a8c6e5a 100644 --- a/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart +++ b/packages/url_launcher/url_launcher_ios/lib/src/messages.g.dart @@ -13,13 +13,25 @@ import 'package:flutter/services.dart'; /// Possible outcomes of launching a URL. enum LaunchResult { - /// The URL was successfully launched. + /// The URL was successfully launched (or could be, for `canLaunchUrl`). success, - /// The URL could not be launched. + /// There was no handler available for the URL. + failure, + + /// The URL could not be launched because it is invalid. + invalidUrl, +} + +/// Possible outcomes of handling a URL within the application. +enum InAppLoadResult { + /// The URL was successfully loaded. + success, + + /// The URL did not load successfully. failedToLoad, - /// The URL was not launched because the URL is invalid. + /// The URL could not be launched because it is invalid. invalidUrl, } @@ -33,7 +45,7 @@ class UrlLauncherApi { static const MessageCodec codec = StandardMessageCodec(); - /// Returns true if the URL can definitely be launched. + /// Checks whether a URL can be loaded. Future canLaunchUrl(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.canLaunchUrl', @@ -62,7 +74,7 @@ class UrlLauncherApi { } } - /// Opens the URL externally, returning true if successful. + /// Opens the URL externally, returning the status of launching it. Future launchUrl( String arg_url, bool arg_universalLinksOnly) async { final BasicMessageChannel channel = BasicMessageChannel( @@ -91,9 +103,9 @@ class UrlLauncherApi { } } - /// Opens the URL in an in-app SFSafariViewController, returning true - /// when it has loaded successfully. - Future openUrlInSafariViewController(String arg_url) async { + /// Opens the URL in an in-app SFSafariViewController, returning the results + /// of loading it. + Future openUrlInSafariViewController(String arg_url) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.url_launcher_ios.UrlLauncherApi.openUrlInSafariViewController', codec, @@ -117,7 +129,7 @@ class UrlLauncherApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return LaunchResult.values[replyList[0]! as int]; + return InAppLoadResult.values[replyList[0]! as int]; } } diff --git a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart index 2ec778277c1..9d1ebc9c236 100644 --- a/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart +++ b/packages/url_launcher/url_launcher_ios/lib/url_launcher_ios.dart @@ -29,7 +29,7 @@ class UrlLauncherIOS extends UrlLauncherPlatform { @override Future canLaunch(String url) async { final LaunchResult result = await _hostApi.canLaunchUrl(url); - return _mapLaunchResults(results: result, url: url); + return _mapLaunchResult(result); } @override @@ -91,14 +91,14 @@ class UrlLauncherIOS extends UrlLauncherPlatform { break; } - final LaunchResult result; if (inApp) { - result = await _hostApi.openUrlInSafariViewController(url); + return _mapInAppLoadResult( + await _hostApi.openUrlInSafariViewController(url), + url: url); } else { - result = await _hostApi.launchUrl(url, - options.mode == PreferredLaunchMode.externalNonBrowserApplication); + return _mapLaunchResult(await _hostApi.launchUrl(url, + options.mode == PreferredLaunchMode.externalNonBrowserApplication)); } - return _mapLaunchResults(results: result, url: url); } @override @@ -125,24 +125,51 @@ class UrlLauncherIOS extends UrlLauncherPlatform { mode == PreferredLaunchMode.inAppBrowserView; } - bool _mapLaunchResults({ - required final LaunchResult results, - required String url, - }) { - switch (results) { + bool _mapLaunchResult(LaunchResult result) { + switch (result) { case LaunchResult.success: return true; - case LaunchResult.failedToLoad: + case LaunchResult.failure: return false; case LaunchResult.invalidUrl: - // TODO(stuartmorgan): Remove this as part of standardizing error - // handling. See https://github.com/flutter/flutter/issues/127665 - // The PlatformExceptions thrown here is for compatibility with the - // previous implementation. - throw PlatformException( - code: 'invalidUrl', - message: 'Unable to parse URL $url', - ); + throw _invalidUrlException(); } } + + bool _mapInAppLoadResult(InAppLoadResult result, {required String url}) { + switch (result) { + case InAppLoadResult.success: + return true; + case InAppLoadResult.failedToLoad: + throw _failedSafariViewControllerLoadException(url); + case InAppLoadResult.invalidUrl: + throw _invalidUrlException(); + } + } + + // TODO(stuartmorgan): Remove this as part of standardizing error handling. + // See https://github.com/flutter/flutter/issues/127665 + // + // This PlatformException (including the exact string details, since those + // are a defacto part of the API) is for compatibility with the previous + // native implementation. + PlatformException _invalidUrlException() { + throw PlatformException( + code: 'argument_error', + message: 'Unable to parse URL', + ); + } + + // TODO(stuartmorgan): Remove this as part of standardizing error handling. + // See https://github.com/flutter/flutter/issues/127665 + // + // This PlatformException (including the exact string details, since those + // are a defacto part of the API) is for compatibility with the previous + // native implementation. + PlatformException _failedSafariViewControllerLoadException(String url) { + throw PlatformException( + code: 'Error', + message: 'Error while launching $url', + ); + } } diff --git a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart index 5b2e66ee267..f5dc1052b32 100644 --- a/packages/url_launcher/url_launcher_ios/pigeons/messages.dart +++ b/packages/url_launcher/url_launcher_ios/pigeons/messages.dart @@ -12,32 +12,44 @@ import 'package:pigeon/pigeon.dart'; /// Possible outcomes of launching a URL. enum LaunchResult { - /// The URL was successfully launched. + /// The URL was successfully launched (or could be, for `canLaunchUrl`). success, - /// The URL could not be launched. + /// There was no handler available for the URL. + failure, + + /// The URL could not be launched because it is invalid. + invalidUrl, +} + +/// Possible outcomes of handling a URL within the application. +enum InAppLoadResult { + /// The URL was successfully loaded. + success, + + /// The URL did not load successfully. failedToLoad, - /// The URL was not launched because the URL is invalid. + /// The URL could not be launched because it is invalid. invalidUrl, } @HostApi() abstract class UrlLauncherApi { - /// Returns true if the URL can definitely be launched. + /// Checks whether a URL can be loaded. @ObjCSelector('canLaunchURL:') LaunchResult canLaunchUrl(String url); - /// Opens the URL externally, returning true if successful. + /// Opens the URL externally, returning the status of launching it. @async @ObjCSelector('launchURL:universalLinksOnly:') LaunchResult launchUrl(String url, bool universalLinksOnly); - /// Opens the URL in an in-app SFSafariViewController, returning true - /// when it has loaded successfully. + /// Opens the URL in an in-app SFSafariViewController, returning the results + /// of loading it. @async @ObjCSelector('openSafariViewControllerWithURL:') - LaunchResult openUrlInSafariViewController(String url); + InAppLoadResult openUrlInSafariViewController(String url); /// Closes the view controller opened by [openUrlInSafariViewController]. void closeSafariViewController(); diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart index b1069a28479..195db630294 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -38,7 +38,7 @@ void main() { test('handles failure', () async { when(api.canLaunchUrl(_webUrl)) - .thenAnswer((_) async => LaunchResult.failedToLoad); + .thenAnswer((_) async => LaunchResult.failure); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect(await launcher.canLaunch(_webUrl), false); }); @@ -48,7 +48,9 @@ void main() { .thenAnswer((_) async => LaunchResult.invalidUrl); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); await expectLater( - launcher.canLaunch(_webUrl), throwsA(isA())); + launcher.canLaunch(_webUrl), + throwsA(isA().having( + (PlatformException e) => e.code, 'code', 'argument_error'))); }); }); @@ -73,7 +75,7 @@ void main() { test('handles failure', () async { when(api.launchUrl(_webUrl, any)) - .thenAnswer((_) async => LaunchResult.failedToLoad); + .thenAnswer((_) async => LaunchResult.failure); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect( await launcher.launch( @@ -103,12 +105,13 @@ void main() { universalLinksOnly: false, headers: const {}, ), - throwsA(isA())); + throwsA(isA().having( + (PlatformException e) => e.code, 'code', 'argument_error'))); }); test('force SafariVC is handled', () async { when(api.openUrlInSafariViewController(_webUrl)) - .thenAnswer((_) async => LaunchResult.success); + .thenAnswer((_) async => InAppLoadResult.success); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); expect( await launcher.launch( @@ -189,14 +192,15 @@ void main() { _webUrl, const LaunchOptions(mode: PreferredLaunchMode.externalApplication), ), - throwsA(isA())); + throwsA(isA().having( + (PlatformException e) => e.code, 'code', 'argument_error'))); }); }); group('launch with Safari view controller', () { test('calls through with inAppWebView', () async { when(api.openUrlInSafariViewController(_webUrl)) - .thenAnswer((_) async => LaunchResult.success); + .thenAnswer((_) async => InAppLoadResult.success); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); final bool launched = await launcher.launchUrl( _webUrl, const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)); @@ -206,7 +210,7 @@ void main() { test('calls through with inAppBrowserView', () async { when(api.openUrlInSafariViewController(_webUrl)) - .thenAnswer((_) async => LaunchResult.success); + .thenAnswer((_) async => InAppLoadResult.success); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); final bool launched = await launcher.launchUrl(_webUrl, const LaunchOptions(mode: PreferredLaunchMode.inAppBrowserView)); @@ -216,12 +220,24 @@ void main() { test('throws PlatformException for invalid URL', () async { when(api.openUrlInSafariViewController(_webUrl)) - .thenAnswer((_) async => LaunchResult.invalidUrl); + .thenAnswer((_) async => InAppLoadResult.invalidUrl); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); await expectLater( launcher.launchUrl(_webUrl, const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)), - throwsA(isA())); + throwsA(isA().having( + (PlatformException e) => e.code, 'code', 'argument_error'))); + }); + + test('throws PlatformException for load failure', () async { + when(api.openUrlInSafariViewController(_webUrl)) + .thenAnswer((_) async => InAppLoadResult.failedToLoad); + final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); + await expectLater( + launcher.launchUrl(_webUrl, + const LaunchOptions(mode: PreferredLaunchMode.inAppWebView)), + throwsA(isA() + .having((PlatformException e) => e.code, 'code', 'Error'))); }); }); @@ -248,7 +264,8 @@ void main() { _webUrl, const LaunchOptions( mode: PreferredLaunchMode.externalNonBrowserApplication)), - throwsA(isA())); + throwsA(isA().having( + (PlatformException e) => e.code, 'code', 'argument_error'))); }); }); @@ -256,7 +273,7 @@ void main() { test('uses Safari view controller for http', () async { const String httpUrl = 'http://example.com/'; when(api.openUrlInSafariViewController(httpUrl)) - .thenAnswer((_) async => LaunchResult.success); + .thenAnswer((_) async => InAppLoadResult.success); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); final bool launched = await launcher.launchUrl(httpUrl, const LaunchOptions()); @@ -267,7 +284,7 @@ void main() { test('uses Safari view controller for https', () async { const String httpsUrl = 'https://example.com/'; when(api.openUrlInSafariViewController(httpsUrl)) - .thenAnswer((_) async => LaunchResult.success); + .thenAnswer((_) async => InAppLoadResult.success); final UrlLauncherIOS launcher = UrlLauncherIOS(api: api); final bool launched = await launcher.launchUrl(httpsUrl, const LaunchOptions()); diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart index 534fefb1693..e9eccab1621 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.mocks.dart @@ -56,15 +56,16 @@ class MockUrlLauncherApi extends _i1.Mock implements _i2.UrlLauncherApi { ) as _i3.Future<_i2.LaunchResult>); @override - _i3.Future<_i2.LaunchResult> openUrlInSafariViewController(String? arg_url) => + _i3.Future<_i2.InAppLoadResult> openUrlInSafariViewController( + String? arg_url) => (super.noSuchMethod( Invocation.method( #openUrlInSafariViewController, [arg_url], ), returnValue: - _i3.Future<_i2.LaunchResult>.value(_i2.LaunchResult.success), - ) as _i3.Future<_i2.LaunchResult>); + _i3.Future<_i2.InAppLoadResult>.value(_i2.InAppLoadResult.success), + ) as _i3.Future<_i2.InAppLoadResult>); @override _i3.Future closeSafariViewController() => (super.noSuchMethod( From 43966a0a112dff134c8172c1a6597a00bece9340 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 26 Oct 2023 12:03:46 -0400 Subject: [PATCH 29/29] Improve invalid URL testing --- .../ios/RunnerTests/URLLauncherTests.swift | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift index aeb1c4c9da3..966a3d63e6d 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift +++ b/packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.swift @@ -7,6 +7,12 @@ import XCTest @testable import url_launcher_ios +// Tests whether NSURL parsing is strict. When linking against the iOS 17 SDK or later, +// NSURL uses a more lenient parser which will not return nil. +private func urlParsingIsStrict() -> Bool { + return URL(string: "b a d U R L") == nil +} + final class URLLauncherTests: XCTestCase { private func createPlugin() -> URLLauncherPlugin { @@ -31,9 +37,11 @@ final class URLLauncherTests: XCTestCase { func testCanLaunchFailureWithInvalidURL() { let result = createPlugin().canLaunchUrl(url: "urls can't have spaces") - // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't - // fail to parse URLs, so the test must allow for either outcome. - XCTAssertTrue(result == .failure || result == .invalidUrl) + if urlParsingIsStrict() { + XCTAssertEqual(result, .invalidUrl) + } else { + XCTAssertEqual(result, .failure) + } } func testLaunchSuccess() { @@ -71,9 +79,11 @@ final class URLLauncherTests: XCTestCase { createPlugin().launchUrl(url: "urls can't have spaces", universalLinksOnly: false) { result in switch result { case .success(let details): - // When linking against the iOS 17 SDK or later, NSURL uses a lenient parser, and won't - // fail to parse URLs, so the test must allow for either outcome. - XCTAssertTrue(details == .failure || details == .invalidUrl) + if urlParsingIsStrict() { + XCTAssertEqual(details, .invalidUrl) + } else { + XCTAssertEqual(details, .failure) + } case .failure(let error): XCTFail("Unexpected error: \(error)") }