Skip to content

Commit aad90e7

Browse files
[url_launcher] Convert macOS to Pigeon (flutter#3686)
[url_launcher] Convert macOS to Pigeon
1 parent 1ddd2d9 commit aad90e7

File tree

10 files changed

+524
-278
lines changed

10 files changed

+524
-278
lines changed

packages/url_launcher/url_launcher_macos/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.0.5
2+
3+
* Converts method channel to Pigeon.
4+
15
## 3.0.4
26

37
* Clarifies explanation of endorsement in README.

packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift

Lines changed: 23 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import FlutterMacOS
66
import XCTest
7-
import url_launcher_macos
7+
8+
@testable import url_launcher_macos
89

910
/// A stub to simulate the system Url handler.
1011
class StubWorkspace: SystemURLHandler {
@@ -23,132 +24,51 @@ class StubWorkspace: SystemURLHandler {
2324
class RunnerTests: XCTestCase {
2425

2526
func testCanLaunchSuccessReturnsTrue() throws {
26-
let expectation = XCTestExpectation(description: "Check if the URL can be launched")
2727
let plugin = UrlLauncherPlugin()
2828

29-
let call = FlutterMethodCall(
30-
methodName: "canLaunch",
31-
arguments: ["url": "https://flutter.dev"])
32-
33-
plugin.handle(
34-
call,
35-
result: { (result: Any?) -> Void in
36-
XCTAssertEqual(result as? Bool, true)
37-
expectation.fulfill()
38-
})
39-
40-
wait(for: [expectation], timeout: 10.0)
29+
let result = try plugin.canLaunch(url: "https://flutter.dev")
30+
XCTAssertNil(result.error)
31+
XCTAssertTrue(result.value)
4132
}
4233

4334
func testCanLaunchNoAppIsAbleToOpenUrlReturnsFalse() throws {
44-
let expectation = XCTestExpectation(description: "Check if the URL can be launched")
4535
let plugin = UrlLauncherPlugin()
4636

47-
let call = FlutterMethodCall(
48-
methodName: "canLaunch",
49-
arguments: ["url": "example://flutter.dev"])
50-
51-
plugin.handle(
52-
call,
53-
result: { (result: Any?) -> Void in
54-
XCTAssertEqual(result as? Bool, false)
55-
expectation.fulfill()
56-
})
57-
58-
wait(for: [expectation], timeout: 10.0)
37+
let result = try plugin.canLaunch(url: "example://flutter.dev")
38+
XCTAssertNil(result.error)
39+
XCTAssertFalse(result.value)
5940
}
6041

61-
func testCanLaunchInvalidUrlReturnsFalse() throws {
62-
let expectation = XCTestExpectation(description: "Check if the URL can be launched")
42+
func testCanLaunchInvalidUrlReturnsError() throws {
6343
let plugin = UrlLauncherPlugin()
6444

65-
let call = FlutterMethodCall(
66-
methodName: "canLaunch",
67-
arguments: ["url": "brokenUrl"])
68-
69-
plugin.handle(
70-
call,
71-
result: { (result: Any?) -> Void in
72-
XCTAssertEqual(result as? Bool, false)
73-
expectation.fulfill()
74-
})
75-
76-
wait(for: [expectation], timeout: 10.0)
77-
}
78-
79-
func testCanLaunchMissingArgumentReturnsFlutterError() throws {
80-
let expectation = XCTestExpectation(description: "Check if the URL can be launched")
81-
let plugin = UrlLauncherPlugin()
82-
83-
let call = FlutterMethodCall(
84-
methodName: "canLaunch",
85-
arguments: [])
86-
87-
plugin.handle(
88-
call,
89-
result: { (result: Any?) -> Void in
90-
XCTAssertTrue(result is FlutterError)
91-
expectation.fulfill()
92-
})
93-
94-
wait(for: [expectation], timeout: 10.0)
45+
let result = try plugin.canLaunch(url: "invalid url")
46+
XCTAssertEqual(result.error, .invalidUrl)
9547
}
9648

9749
func testLaunchSuccessReturnsTrue() throws {
98-
let expectation = XCTestExpectation(description: "Try to open the URL")
9950
let workspace = StubWorkspace()
100-
let pluginWithStubWorkspace = UrlLauncherPlugin(workspace)
101-
102-
let call = FlutterMethodCall(
103-
methodName: "launch",
104-
arguments: ["url": "https://flutter.dev"])
51+
let plugin = UrlLauncherPlugin(workspace)
10552

106-
pluginWithStubWorkspace.handle(
107-
call,
108-
result: { (result: Any?) -> Void in
109-
XCTAssertEqual(result as? Bool, true)
110-
expectation.fulfill()
111-
})
112-
113-
wait(for: [expectation], timeout: 10.0)
53+
let result = try plugin.launch(url: "https://flutter.dev")
54+
XCTAssertNil(result.error)
55+
XCTAssertTrue(result.value)
11456
}
11557

11658
func testLaunchNoAppIsAbleToOpenUrlReturnsFalse() throws {
117-
let expectation = XCTestExpectation(description: "Try to open the URL")
11859
let workspace = StubWorkspace()
11960
workspace.isSuccessful = false
120-
let pluginWithStubWorkspace = UrlLauncherPlugin(workspace)
121-
122-
let call = FlutterMethodCall(
123-
methodName: "launch",
124-
arguments: ["url": "schemethatdoesnotexist://flutter.dev"])
61+
let plugin = UrlLauncherPlugin(workspace)
12562

126-
pluginWithStubWorkspace.handle(
127-
call,
128-
result: { (result: Any?) -> Void in
129-
XCTAssertEqual(result as? Bool, false)
130-
expectation.fulfill()
131-
})
132-
133-
wait(for: [expectation], timeout: 10.0)
63+
let result = try plugin.launch(url: "schemethatdoesnotexist://flutter.dev")
64+
XCTAssertNil(result.error)
65+
XCTAssertFalse(result.value)
13466
}
13567

136-
func testLaunchMissingArgumentReturnsFlutterError() throws {
137-
let expectation = XCTestExpectation(description: "Try to open the URL")
138-
let workspace = StubWorkspace()
139-
let pluginWithStubWorkspace = UrlLauncherPlugin(workspace)
140-
141-
let call = FlutterMethodCall(
142-
methodName: "launch",
143-
arguments: [])
144-
145-
pluginWithStubWorkspace.handle(
146-
call,
147-
result: { (result: Any?) -> Void in
148-
XCTAssertTrue(result is FlutterError)
149-
expectation.fulfill()
150-
})
68+
func testLaunchInvalidUrlReturnsError() throws {
69+
let plugin = UrlLauncherPlugin()
15170

152-
wait(for: [expectation], timeout: 10.0)
71+
let result = try plugin.launch(url: "invalid url")
72+
XCTAssertEqual(result.error, .invalidUrl)
15373
}
15474
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
// Autogenerated from Pigeon (v9.2.4), do not edit directly.
5+
// See also: https://pub.dev/packages/pigeon
6+
// 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
7+
8+
import 'dart:async';
9+
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
10+
11+
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
12+
import 'package:flutter/services.dart';
13+
14+
/// Possible error conditions for [UrlLauncherApi] calls.
15+
enum UrlLauncherError {
16+
/// The URL could not be parsed as an NSURL.
17+
invalidUrl,
18+
}
19+
20+
/// Possible results for a [UrlLauncherApi] call with a boolean outcome.
21+
class UrlLauncherBoolResult {
22+
UrlLauncherBoolResult({
23+
required this.value,
24+
this.error,
25+
});
26+
27+
bool value;
28+
29+
UrlLauncherError? error;
30+
31+
Object encode() {
32+
return <Object?>[
33+
value,
34+
error?.index,
35+
];
36+
}
37+
38+
static UrlLauncherBoolResult decode(Object result) {
39+
result as List<Object?>;
40+
return UrlLauncherBoolResult(
41+
value: result[0]! as bool,
42+
error:
43+
result[1] != null ? UrlLauncherError.values[result[1]! as int] : null,
44+
);
45+
}
46+
}
47+
48+
class _UrlLauncherApiCodec extends StandardMessageCodec {
49+
const _UrlLauncherApiCodec();
50+
@override
51+
void writeValue(WriteBuffer buffer, Object? value) {
52+
if (value is UrlLauncherBoolResult) {
53+
buffer.putUint8(128);
54+
writeValue(buffer, value.encode());
55+
} else {
56+
super.writeValue(buffer, value);
57+
}
58+
}
59+
60+
@override
61+
Object? readValueOfType(int type, ReadBuffer buffer) {
62+
switch (type) {
63+
case 128:
64+
return UrlLauncherBoolResult.decode(readValue(buffer)!);
65+
default:
66+
return super.readValueOfType(type, buffer);
67+
}
68+
}
69+
}
70+
71+
class UrlLauncherApi {
72+
/// Constructor for [UrlLauncherApi]. The [binaryMessenger] named argument is
73+
/// available for dependency injection. If it is left null, the default
74+
/// BinaryMessenger will be used which routes to the host platform.
75+
UrlLauncherApi({BinaryMessenger? binaryMessenger})
76+
: _binaryMessenger = binaryMessenger;
77+
final BinaryMessenger? _binaryMessenger;
78+
79+
static const MessageCodec<Object?> codec = _UrlLauncherApiCodec();
80+
81+
/// Returns a true result if the URL can definitely be launched.
82+
Future<UrlLauncherBoolResult> canLaunchUrl(String arg_url) async {
83+
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
84+
'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec,
85+
binaryMessenger: _binaryMessenger);
86+
final List<Object?>? replyList =
87+
await channel.send(<Object?>[arg_url]) as List<Object?>?;
88+
if (replyList == null) {
89+
throw PlatformException(
90+
code: 'channel-error',
91+
message: 'Unable to establish connection on channel.',
92+
);
93+
} else if (replyList.length > 1) {
94+
throw PlatformException(
95+
code: replyList[0]! as String,
96+
message: replyList[1] as String?,
97+
details: replyList[2],
98+
);
99+
} else if (replyList[0] == null) {
100+
throw PlatformException(
101+
code: 'null-error',
102+
message: 'Host platform returned null value for non-null return value.',
103+
);
104+
} else {
105+
return (replyList[0] as UrlLauncherBoolResult?)!;
106+
}
107+
}
108+
109+
/// Opens the URL externally, returning a true result if successful.
110+
Future<UrlLauncherBoolResult> launchUrl(String arg_url) async {
111+
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
112+
'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec,
113+
binaryMessenger: _binaryMessenger);
114+
final List<Object?>? replyList =
115+
await channel.send(<Object?>[arg_url]) as List<Object?>?;
116+
if (replyList == null) {
117+
throw PlatformException(
118+
code: 'channel-error',
119+
message: 'Unable to establish connection on channel.',
120+
);
121+
} else if (replyList.length > 1) {
122+
throw PlatformException(
123+
code: replyList[0]! as String,
124+
message: replyList[1] as String?,
125+
details: replyList[2],
126+
);
127+
} else if (replyList[0] == null) {
128+
throw PlatformException(
129+
code: 'null-error',
130+
message: 'Host platform returned null value for non-null return value.',
131+
);
132+
} else {
133+
return (replyList[0] as UrlLauncherBoolResult?)!;
134+
}
135+
}
136+
}

packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
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 'dart:async';
6-
5+
import 'package:flutter/foundation.dart' show visibleForTesting;
76
import 'package:flutter/services.dart';
87
import 'package:url_launcher_platform_interface/link.dart';
98
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
109

11-
const MethodChannel _channel =
12-
MethodChannel('plugins.flutter.io/url_launcher_macos');
10+
import 'src/messages.g.dart';
1311

1412
/// An implementation of [UrlLauncherPlatform] for macOS.
1513
class UrlLauncherMacOS extends UrlLauncherPlatform {
14+
/// Creates a new plugin implementation instance.
15+
UrlLauncherMacOS({
16+
@visibleForTesting UrlLauncherApi? api,
17+
}) : _hostApi = api ?? UrlLauncherApi();
18+
19+
final UrlLauncherApi _hostApi;
20+
1621
/// Registers this class as the default instance of [UrlLauncherPlatform].
1722
static void registerWith() {
1823
UrlLauncherPlatform.instance = UrlLauncherMacOS();
@@ -22,11 +27,14 @@ class UrlLauncherMacOS extends UrlLauncherPlatform {
2227
final LinkDelegate? linkDelegate = null;
2328

2429
@override
25-
Future<bool> canLaunch(String url) {
26-
return _channel.invokeMethod<bool>(
27-
'canLaunch',
28-
<String, Object>{'url': url},
29-
).then((bool? value) => value ?? false);
30+
Future<bool> canLaunch(String url) async {
31+
final UrlLauncherBoolResult result = await _hostApi.canLaunchUrl(url);
32+
switch (result.error) {
33+
case UrlLauncherError.invalidUrl:
34+
throw _getInvalidUrlException(url);
35+
case null:
36+
}
37+
return result.value;
3038
}
3139

3240
@override
@@ -39,16 +47,24 @@ class UrlLauncherMacOS extends UrlLauncherPlatform {
3947
required bool universalLinksOnly,
4048
required Map<String, String> headers,
4149
String? webOnlyWindowName,
42-
}) {
43-
return _channel.invokeMethod<bool>(
44-
'launch',
45-
<String, Object>{
46-
'url': url,
47-
'enableJavaScript': enableJavaScript,
48-
'enableDomStorage': enableDomStorage,
49-
'universalLinksOnly': universalLinksOnly,
50-
'headers': headers,
51-
},
52-
).then((bool? value) => value ?? false);
50+
}) async {
51+
final UrlLauncherBoolResult result = await _hostApi.launchUrl(url);
52+
switch (result.error) {
53+
case UrlLauncherError.invalidUrl:
54+
throw _getInvalidUrlException(url);
55+
case null:
56+
}
57+
return result.value;
58+
}
59+
60+
Exception _getInvalidUrlException(String url) {
61+
// TODO(stuartmorgan): Make this an actual ArgumentError. This should be
62+
// coordinated across all platforms as a breaking change to have them all
63+
// return the same thing; currently it throws a PlatformException to
64+
// preserve existing behavior.
65+
return PlatformException(
66+
code: 'argument_error',
67+
message: 'Unable to parse URL',
68+
details: 'Provided URL: $url');
5369
}
5470
}

0 commit comments

Comments
 (0)