Skip to content

Commit 3b3a09d

Browse files
[url_launcher] Convert iOS to Pigeon (flutter#3481)
[url_launcher] Convert iOS to Pigeon
1 parent 3d078b5 commit 3b3a09d

File tree

15 files changed

+755
-243
lines changed

15 files changed

+755
-243
lines changed

packages/url_launcher/url_launcher_ios/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 6.1.3
2+
3+
* Switches to Pigeon for internal implementation.
4+
15
## 6.1.2
26

37
* Clarifies explanation of endorsement in README.

packages/url_launcher/url_launcher_ios/example/ios/RunnerTests/URLLauncherTests.m

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,156 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
@import Flutter;
56
@import url_launcher_ios;
67
@import XCTest;
78

9+
@interface FULFakeLauncher : NSObject <FULLauncher>
10+
@property(copy, nonatomic) NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *passedOptions;
11+
@end
12+
13+
@implementation FULFakeLauncher
14+
- (BOOL)canOpenURL:(NSURL *)url {
15+
return [url.scheme isEqualToString:@"good"];
16+
}
17+
18+
- (void)openURL:(NSURL *)url
19+
options:(NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *)options
20+
completionHandler:(void (^__nullable)(BOOL success))completion {
21+
self.passedOptions = options;
22+
completion([url.scheme isEqualToString:@"good"]);
23+
}
24+
@end
25+
26+
#pragma mark -
27+
828
@interface URLLauncherTests : XCTestCase
929
@end
1030

1131
@implementation URLLauncherTests
1232

13-
- (void)testPlugin {
14-
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init];
15-
XCTAssertNotNil(plugin);
33+
- (void)testCanLaunchSuccess {
34+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
35+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
36+
37+
FlutterError *error;
38+
NSNumber *result = [plugin canLaunchURL:@"good://url" error:&error];
39+
40+
XCTAssertTrue(result.boolValue);
41+
XCTAssertNil(error);
42+
}
43+
44+
- (void)testCanLaunchFailure {
45+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
46+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
47+
48+
FlutterError *error;
49+
NSNumber *result = [plugin canLaunchURL:@"bad://url" error:&error];
50+
51+
XCTAssertNotNil(result);
52+
XCTAssertFalse(result.boolValue);
53+
XCTAssertNil(error);
54+
}
55+
56+
- (void)testCanLaunchInvalidURL {
57+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
58+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
59+
60+
FlutterError *error;
61+
NSNumber *result = [plugin canLaunchURL:@"urls can't have spaces" error:&error];
62+
63+
XCTAssertNil(result);
64+
XCTAssertEqualObjects(error.code, @"argument_error");
65+
XCTAssertEqualObjects(error.message, @"Unable to parse URL");
66+
XCTAssertEqualObjects(error.details, @"Provided URL: urls can't have spaces");
67+
}
68+
69+
- (void)testLaunchSuccess {
70+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
71+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
72+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
73+
74+
[plugin launchURL:@"good://url"
75+
universalLinksOnly:@NO
76+
completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) {
77+
XCTAssertTrue(result.boolValue);
78+
XCTAssertNil(error);
79+
[resultExpectation fulfill];
80+
}];
81+
82+
[self waitForExpectationsWithTimeout:5 handler:nil];
83+
}
84+
85+
- (void)testLaunchFailure {
86+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
87+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
88+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
89+
90+
[plugin launchURL:@"bad://url"
91+
universalLinksOnly:@NO
92+
completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) {
93+
XCTAssertNotNil(result);
94+
XCTAssertFalse(result.boolValue);
95+
XCTAssertNil(error);
96+
[resultExpectation fulfill];
97+
}];
98+
99+
[self waitForExpectationsWithTimeout:5 handler:nil];
100+
}
101+
102+
- (void)testLaunchInvalidURL {
103+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
104+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
105+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
106+
107+
[plugin launchURL:@"urls can't have spaces"
108+
universalLinksOnly:@NO
109+
completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) {
110+
XCTAssertNil(result);
111+
XCTAssertNotNil(error);
112+
XCTAssertEqualObjects(error.code, @"argument_error");
113+
XCTAssertEqualObjects(error.message, @"Unable to parse URL");
114+
XCTAssertEqualObjects(error.details, @"Provided URL: urls can't have spaces");
115+
[resultExpectation fulfill];
116+
}];
117+
118+
[self waitForExpectationsWithTimeout:5 handler:nil];
119+
}
120+
121+
- (void)testLaunchWithoutUniversalLinks {
122+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
123+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
124+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
125+
126+
FlutterError *error;
127+
[plugin launchURL:@"good://url"
128+
universalLinksOnly:@NO
129+
completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) {
130+
[resultExpectation fulfill];
131+
}];
132+
133+
[self waitForExpectationsWithTimeout:5 handler:nil];
134+
XCTAssertNil(error);
135+
XCTAssertFalse(
136+
((NSNumber *)launcher.passedOptions[UIApplicationOpenURLOptionUniversalLinksOnly]).boolValue);
137+
}
138+
139+
- (void)testLaunchWithUniversalLinks {
140+
FULFakeLauncher *launcher = [[FULFakeLauncher alloc] init];
141+
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] initWithLauncher:launcher];
142+
XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"];
143+
144+
FlutterError *error;
145+
[plugin launchURL:@"good://url"
146+
universalLinksOnly:@YES
147+
completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) {
148+
[resultExpectation fulfill];
149+
}];
150+
151+
[self waitForExpectationsWithTimeout:5 handler:nil];
152+
XCTAssertNil(error);
153+
XCTAssertTrue(
154+
((NSNumber *)launcher.passedOptions[UIApplicationOpenURLOptionUniversalLinksOnly]).boolValue);
16155
}
17156

18157
@end

packages/url_launcher/url_launcher_ios/example/pubspec.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ dev_dependencies:
2525
sdk: flutter
2626
integration_test:
2727
sdk: flutter
28-
mockito: 5.3.2
2928
plugin_platform_interface: ^2.0.0
3029

3130
flutter:

packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,7 @@
44

55
#import <Flutter/Flutter.h>
66

7-
@interface FLTURLLauncherPlugin : NSObject <FlutterPlugin>
7+
#import "messages.g.h"
8+
9+
@interface FLTURLLauncherPlugin : NSObject <FlutterPlugin, FULUrlLauncherApi>
810
@end

packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m

Lines changed: 86 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
#import <SafariServices/SafariServices.h>
66

77
#import "FLTURLLauncherPlugin.h"
8+
#import "FLTURLLauncherPlugin_Test.h"
9+
#import "FULLauncher.h"
10+
#import "messages.g.h"
11+
12+
typedef void (^OpenInSafariVCResponse)(NSNumber *_Nullable, FlutterError *_Nullable);
813

914
@interface FLTURLLaunchSession : NSObject <SFSafariViewControllerDelegate>
1015

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

1823
@implementation FLTURLLaunchSession
1924

20-
- (instancetype)initWithUrl:url withFlutterResult:result {
25+
- (instancetype)initWithURL:url completion:completion {
2126
self = [super init];
2227
if (self) {
2328
self.url = url;
24-
self.flutterResult = result;
29+
self.completion = completion;
2530
self.safari = [[SFSafariViewController alloc] initWithURL:url];
2631
self.safari.delegate = self;
2732
}
@@ -31,12 +36,13 @@ - (instancetype)initWithUrl:url withFlutterResult:result {
3136
- (void)safariViewController:(SFSafariViewController *)controller
3237
didCompleteInitialLoad:(BOOL)didLoadSuccessfully {
3338
if (didLoadSuccessfully) {
34-
self.flutterResult(@YES);
39+
self.completion(@YES, nil);
3540
} else {
36-
self.flutterResult([FlutterError
37-
errorWithCode:@"Error"
38-
message:[NSString stringWithFormat:@"Error while launching %@", self.url]
39-
details:nil]);
41+
self.completion(
42+
nil, [FlutterError
43+
errorWithCode:@"Error"
44+
message:[NSString stringWithFormat:@"Error while launching %@", self.url]
45+
details:nil]);
4046
}
4147
}
4248

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

5258
@end
5359

60+
#pragma mark -
61+
62+
/// Default implementation of FULLancher, using UIApplication.
63+
@interface FULUIApplicationLauncher : NSObject <FULLauncher>
64+
@end
65+
66+
@implementation FULUIApplicationLauncher
67+
- (BOOL)canOpenURL:(nonnull NSURL *)url {
68+
return [[UIApplication sharedApplication] canOpenURL:url];
69+
}
70+
71+
- (void)openURL:(nonnull NSURL *)url
72+
options:(nonnull NSDictionary<UIApplicationOpenExternalURLOptionsKey, id> *)options
73+
completionHandler:(void (^_Nullable)(BOOL))completion {
74+
[[UIApplication sharedApplication] openURL:url options:options completionHandler:completion];
75+
}
76+
77+
@end
78+
79+
#pragma mark -
80+
5481
@interface FLTURLLauncherPlugin ()
5582

5683
@property(strong, nonatomic) FLTURLLaunchSession *currentSession;
84+
@property(strong, nonatomic) NSObject<FULLauncher> *launcher;
5785

5886
@end
5987

6088
@implementation FLTURLLauncherPlugin
6189

6290
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
63-
FlutterMethodChannel *channel =
64-
[FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/url_launcher_ios"
65-
binaryMessenger:registrar.messenger];
6691
FLTURLLauncherPlugin *plugin = [[FLTURLLauncherPlugin alloc] init];
67-
[registrar addMethodCallDelegate:plugin channel:channel];
68-
}
69-
70-
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
71-
NSString *url = call.arguments[@"url"];
72-
if ([@"canLaunch" isEqualToString:call.method]) {
73-
result(@([self canLaunchURL:url]));
74-
} else if ([@"launch" isEqualToString:call.method]) {
75-
NSNumber *useSafariVC = call.arguments[@"useSafariVC"];
76-
if (useSafariVC.boolValue) {
77-
[self launchURLInVC:url result:result];
78-
} else {
79-
[self launchURL:url call:call result:result];
80-
}
81-
} else if ([@"closeWebView" isEqualToString:call.method]) {
82-
[self closeWebViewWithResult:result];
83-
} else {
84-
result(FlutterMethodNotImplemented);
92+
FULUrlLauncherApiSetup(registrar.messenger, plugin);
93+
}
94+
95+
- (instancetype)init {
96+
return [self initWithLauncher:[[FULUIApplicationLauncher alloc] init]];
97+
}
98+
99+
- (instancetype)initWithLauncher:(NSObject<FULLauncher> *)launcher {
100+
if (self = [super init]) {
101+
_launcher = launcher;
85102
}
103+
return self;
86104
}
87105

88-
- (BOOL)canLaunchURL:(NSString *)urlString {
106+
- (nullable NSNumber *)canLaunchURL:(NSString *)urlString
107+
error:(FlutterError *_Nullable *_Nonnull)error {
89108
NSURL *url = [NSURL URLWithString:urlString];
90-
UIApplication *application = [UIApplication sharedApplication];
91-
return [application canOpenURL:url];
109+
if (!url) {
110+
*error = [self invalidURLErrorForURLString:urlString];
111+
return nil;
112+
}
113+
return @([self.launcher canOpenURL:url]);
92114
}
93115

94116
- (void)launchURL:(NSString *)urlString
95-
call:(FlutterMethodCall *)call
96-
result:(FlutterResult)result {
117+
universalLinksOnly:(NSNumber *)universalLinksOnly
118+
completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion {
97119
NSURL *url = [NSURL URLWithString:urlString];
98-
UIApplication *application = [UIApplication sharedApplication];
99-
100-
NSNumber *universalLinksOnly = call.arguments[@"universalLinksOnly"] ?: @0;
120+
if (!url) {
121+
completion(nil, [self invalidURLErrorForURLString:urlString]);
122+
return;
123+
}
101124
NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly};
102-
[application openURL:url
103-
options:options
104-
completionHandler:^(BOOL success) {
105-
result(@(success));
106-
}];
125+
[self.launcher openURL:url
126+
options:options
127+
completionHandler:^(BOOL success) {
128+
completion(@(success), nil);
129+
}];
107130
}
108131

109-
- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result {
132+
- (void)openSafariViewControllerWithURL:(NSString *)urlString
133+
completion:(OpenInSafariVCResponse)completion {
110134
NSURL *url = [NSURL URLWithString:urlString];
111-
self.currentSession = [[FLTURLLaunchSession alloc] initWithUrl:url withFlutterResult:result];
135+
if (!url) {
136+
completion(nil, [self invalidURLErrorForURLString:urlString]);
137+
return;
138+
}
139+
self.currentSession = [[FLTURLLaunchSession alloc] initWithURL:url completion:completion];
112140
__weak typeof(self) weakSelf = self;
113141
self.currentSession.didFinish = ^(void) {
114142
weakSelf.currentSession = nil;
@@ -118,11 +146,8 @@ - (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result {
118146
completion:nil];
119147
}
120148

121-
- (void)closeWebViewWithResult:(FlutterResult)result {
122-
if (self.currentSession != nil) {
123-
[self.currentSession close];
124-
}
125-
result(nil);
149+
- (void)closeSafariViewControllerWithError:(FlutterError *_Nullable *_Nonnull)error {
150+
[self.currentSession close];
126151
}
127152

128153
- (UIViewController *)topViewController {
@@ -162,4 +187,16 @@ - (UIViewController *)topViewControllerFromViewController:(UIViewController *)vi
162187
}
163188
return viewController;
164189
}
190+
191+
/**
192+
* Creates an error for an invalid URL string.
193+
*
194+
* @param url The invalid URL string
195+
* @return The error to return
196+
*/
197+
- (FlutterError *)invalidURLErrorForURLString:(NSString *)url {
198+
return [FlutterError errorWithCode:@"argument_error"
199+
message:@"Unable to parse URL"
200+
details:[NSString stringWithFormat:@"Provided URL: %@", url]];
201+
}
165202
@end

0 commit comments

Comments
 (0)