Skip to content

[url_launcher] Convert iOS to Pigeon #3481

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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