Skip to content

Commit

Permalink
[url_launcher] Convert iOS to Pigeon (#3481)
Browse files Browse the repository at this point in the history
[url_launcher] Convert iOS to Pigeon
  • Loading branch information
stuartmorgan committed Mar 18, 2023
1 parent 3d078b5 commit 3b3a09d
Show file tree
Hide file tree
Showing 15 changed files with 755 additions and 243 deletions.
4 changes: 4 additions & 0 deletions packages/url_launcher/url_launcher_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 6.1.3

* Switches to Pigeon for internal implementation.

## 6.1.2

* Clarifies explanation of endorsement in README.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,156 @@
// 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 <FULLauncher>
@property(copy, nonatomic) NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *passedOptions;
@end

@implementation FULFakeLauncher
- (BOOL)canOpenURL:(NSURL *)url {
return [url.scheme isEqualToString:@"good"];
}

- (void)openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *)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)testPlugin {
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init];
XCTAssertNotNil(plugin);
- (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
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ dev_dependencies:
sdk: flutter
integration_test:
sdk: flutter
mockito: 5.3.2
plugin_platform_interface: ^2.0.0

flutter:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@

#import <Flutter/Flutter.h>

@interface FLTURLLauncherPlugin : NSObject <FlutterPlugin>
#import "messages.g.h"

@interface FLTURLLauncherPlugin : NSObject <FlutterPlugin, FULUrlLauncherApi>
@end
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
#import <SafariServices/SafariServices.h>

#import "FLTURLLauncherPlugin.h"
#import "FLTURLLauncherPlugin_Test.h"
#import "FULLauncher.h"
#import "messages.g.h"

typedef void (^OpenInSafariVCResponse)(NSNumber *_Nullable, FlutterError *_Nullable);

@interface FLTURLLaunchSession : NSObject <SFSafariViewControllerDelegate>

@property(copy, nonatomic) FlutterResult flutterResult;
@property(copy, nonatomic) OpenInSafariVCResponse completion;
@property(strong, nonatomic) NSURL *url;
@property(strong, nonatomic) SFSafariViewController *safari;
@property(nonatomic, copy) void (^didFinish)(void);
Expand All @@ -17,11 +22,11 @@ @interface FLTURLLaunchSession : NSObject <SFSafariViewControllerDelegate>

@implementation FLTURLLaunchSession

- (instancetype)initWithUrl:url withFlutterResult:result {
- (instancetype)initWithURL:url completion:completion {
self = [super init];
if (self) {
self.url = url;
self.flutterResult = result;
self.completion = completion;
self.safari = [[SFSafariViewController alloc] initWithURL:url];
self.safari.delegate = self;
}
Expand All @@ -31,12 +36,13 @@ - (instancetype)initWithUrl:url withFlutterResult:result {
- (void)safariViewController:(SFSafariViewController *)controller
didCompleteInitialLoad:(BOOL)didLoadSuccessfully {
if (didLoadSuccessfully) {
self.flutterResult(@YES);
self.completion(@YES, nil);
} else {
self.flutterResult([FlutterError
errorWithCode:@"Error"
message:[NSString stringWithFormat:@"Error while launching %@", self.url]
details:nil]);
self.completion(
nil, [FlutterError
errorWithCode:@"Error"
message:[NSString stringWithFormat:@"Error while launching %@", self.url]
details:nil]);
}
}

Expand All @@ -51,64 +57,86 @@ - (void)close {

@end

#pragma mark -

/// Default implementation of FULLancher, using UIApplication.
@interface FULUIApplicationLauncher : NSObject <FULLauncher>
@end

@implementation FULUIApplicationLauncher
- (BOOL)canOpenURL:(nonnull NSURL *)url {
return [[UIApplication sharedApplication] canOpenURL:url];
}

- (void)openURL:(nonnull NSURL *)url
options:(nonnull NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *)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<FULLauncher> *launcher;

@end

@implementation FLTURLLauncherPlugin

+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel =
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/url_launcher_ios"
binaryMessenger:registrar.messenger];
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init];
[registrar addMethodCallDelegate:plugin channel:channel];
}

- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
NSString *url = call.arguments[@"url"];
if ([@"canLaunch" isEqualToString:call.method]) {
result(@([self canLaunchURL:url]));
} else if ([@"launch" isEqualToString:call.method]) {
NSNumber *useSafariVC = call.arguments[@"useSafariVC"];
if (useSafariVC.boolValue) {
[self launchURLInVC:url result:result];
} else {
[self launchURL:url call:call result:result];
}
} else if ([@"closeWebView" isEqualToString:call.method]) {
[self closeWebViewWithResult:result];
} else {
result(FlutterMethodNotImplemented);
FULUrlLauncherApiSetup(registrar.messenger, plugin);
}

- (instancetype)init {
return [self initWithLauncher:[[FULUIApplicationLauncher alloc] init]];
}

- (instancetype)initWithLauncher:(NSObject<FULLauncher> *)launcher {
if (self = [super init]) {
_launcher = launcher;
}
return self;
}

- (BOOL)canLaunchURL:(NSString *)urlString {
- (nullable NSNumber *)canLaunchURL:(NSString *)urlString
error:(FlutterError *_Nullable *_Nonnull)error {
NSURL *url = [NSURL URLWithString:urlString];
UIApplication *application = [UIApplication sharedApplication];
return [application canOpenURL:url];
if (!url) {
*error = [self invalidURLErrorForURLString:urlString];
return nil;
}
return @([self.launcher canOpenURL:url]);
}

- (void)launchURL:(NSString *)urlString
call:(FlutterMethodCall *)call
result:(FlutterResult)result {
universalLinksOnly:(NSNumber *)universalLinksOnly
completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion {
NSURL *url = [NSURL URLWithString:urlString];
UIApplication *application = [UIApplication sharedApplication];

NSNumber *universalLinksOnly = call.arguments[@"universalLinksOnly"] ?: @0;
if (!url) {
completion(nil, [self invalidURLErrorForURLString:urlString]);
return;
}
NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly};
[application openURL:url
options:options
completionHandler:^(BOOL success) {
result(@(success));
}];
[self.launcher openURL:url
options:options
completionHandler:^(BOOL success) {
completion(@(success), nil);
}];
}

- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result {
- (void)openSafariViewControllerWithURL:(NSString *)urlString
completion:(OpenInSafariVCResponse)completion {
NSURL *url = [NSURL URLWithString:urlString];
self.currentSession = [[FLTURLLaunchSession alloc] initWithUrl:url withFlutterResult:result];
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;
Expand All @@ -118,11 +146,8 @@ - (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result {
completion:nil];
}

- (void)closeWebViewWithResult:(FlutterResult)result {
if (self.currentSession != nil) {
[self.currentSession close];
}
result(nil);
- (void)closeSafariViewControllerWithError:(FlutterError *_Nullable *_Nonnull)error {
[self.currentSession close];
}

- (UIViewController *)topViewController {
Expand Down Expand Up @@ -162,4 +187,16 @@ - (UIViewController *)topViewControllerFromViewController:(UIViewController *)vi
}
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
Loading

0 comments on commit 3b3a09d

Please sign in to comment.