[url_launcher] Convert Windows to Pigeon (#6991)

* Initial definition matching current API

* Rename, autoformat

* Update native implementation and unit tests

* Update Dart; remove unnecessary Pigeon test API

* Version bump

* autoformat

* Adjust mock API setup

* Improve comment
diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
index 07a5ef3..abb3ab1 100644
--- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md
+++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 3.0.3
 
+* Converts internal implentation to Pigeon.
 * Updates minimum Flutter version to 3.0.
 
 ## 3.0.2
diff --git a/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart
new file mode 100644
index 0000000..a1d46c1
--- /dev/null
+++ b/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart
@@ -0,0 +1,71 @@
+// 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 (v5.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
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+class UrlLauncherApi {
+  /// Constructor for [UrlLauncherApi].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  UrlLauncherApi({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = StandardMessageCodec();
+
+  Future<bool> canLaunchUrl(String arg_url) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList =
+        await channel.send(<Object?>[arg_url]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else if (replyList[0] == null) {
+      throw PlatformException(
+        code: 'null-error',
+        message: 'Host platform returned null value for non-null return value.',
+      );
+    } else {
+      return (replyList[0] as bool?)!;
+    }
+  }
+
+  Future<void> launchUrl(String arg_url) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec,
+        binaryMessenger: _binaryMessenger);
+    final List<Object?>? replyList =
+        await channel.send(<Object?>[arg_url]) as List<Object?>?;
+    if (replyList == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyList.length > 1) {
+      throw PlatformException(
+        code: replyList[0]! as String,
+        message: replyList[1] as String?,
+        details: replyList[2],
+      );
+    } else {
+      return;
+    }
+  }
+}
diff --git a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart
index b0ee8cb..41c403e 100644
--- a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart
+++ b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart
@@ -2,17 +2,21 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-
-import 'package:flutter/services.dart';
+import 'package:flutter/foundation.dart';
 import 'package:url_launcher_platform_interface/link.dart';
 import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
 
-const MethodChannel _channel =
-    MethodChannel('plugins.flutter.io/url_launcher_windows');
+import 'src/messages.g.dart';
 
 /// An implementation of [UrlLauncherPlatform] for Windows.
 class UrlLauncherWindows extends UrlLauncherPlatform {
+  /// Creates a new plugin implementation instance.
+  UrlLauncherWindows({
+    @visibleForTesting UrlLauncherApi? api,
+  }) : _hostApi = api ?? UrlLauncherApi();
+
+  final UrlLauncherApi _hostApi;
+
   /// Registers this class as the default instance of [UrlLauncherPlatform].
   static void registerWith() {
     UrlLauncherPlatform.instance = UrlLauncherWindows();
@@ -23,10 +27,7 @@
 
   @override
   Future<bool> canLaunch(String url) {
-    return _channel.invokeMethod<bool>(
-      'canLaunch',
-      <String, Object>{'url': url},
-    ).then((bool? value) => value ?? false);
+    return _hostApi.canLaunchUrl(url);
   }
 
   @override
@@ -39,16 +40,9 @@
     required bool universalLinksOnly,
     required Map<String, String> headers,
     String? webOnlyWindowName,
-  }) {
-    return _channel.invokeMethod<bool>(
-      'launch',
-      <String, Object>{
-        'url': url,
-        'enableJavaScript': enableJavaScript,
-        'enableDomStorage': enableDomStorage,
-        'universalLinksOnly': universalLinksOnly,
-        'headers': headers,
-      },
-    ).then((bool? value) => value ?? false);
+  }) async {
+    await _hostApi.launchUrl(url);
+    // Failure is handled via a PlatformException from `launchUrl`.
+    return true;
   }
 }
diff --git a/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt b/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt
new file mode 100644
index 0000000..1236b63
--- /dev/null
+++ b/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt
@@ -0,0 +1,3 @@
+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.
diff --git a/packages/url_launcher/url_launcher_windows/pigeons/messages.dart b/packages/url_launcher/url_launcher_windows/pigeons/messages.dart
new file mode 100644
index 0000000..9607cdf
--- /dev/null
+++ b/packages/url_launcher/url_launcher_windows/pigeons/messages.dart
@@ -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.
+
+import 'package:pigeon/pigeon.dart';
+
+@ConfigurePigeon(PigeonOptions(
+  dartOut: 'lib/src/messages.g.dart',
+  cppOptions: CppOptions(namespace: 'url_launcher_windows'),
+  cppHeaderOut: 'windows/messages.g.h',
+  cppSourceOut: 'windows/messages.g.cpp',
+  copyrightHeader: 'pigeons/copyright.txt',
+))
+@HostApi(dartHostTestHandler: 'TestUrlLauncherApi')
+abstract class UrlLauncherApi {
+  bool canLaunchUrl(String url);
+  void launchUrl(String url);
+}
diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml
index 63ca778..de4f5ed 100644
--- a/packages/url_launcher/url_launcher_windows/pubspec.yaml
+++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Windows implementation of the url_launcher plugin.
 repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_windows
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22
-version: 3.0.2
+version: 3.0.3
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -24,4 +24,5 @@
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  pigeon: ^5.0.1
   test: ^1.16.3
diff --git a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart
index 8b55b29..7f48f64 100644
--- a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart
+++ b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart
@@ -5,140 +5,101 @@
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
+import 'package:url_launcher_windows/src/messages.g.dart';
 import 'package:url_launcher_windows/url_launcher_windows.dart';
 
 void main() {
-  TestWidgetsFlutterBinding.ensureInitialized();
+  late _FakeUrlLauncherApi api;
+  late UrlLauncherWindows plugin;
 
-  group('$UrlLauncherWindows', () {
-    const MethodChannel channel =
-        MethodChannel('plugins.flutter.io/url_launcher_windows');
-    final List<MethodCall> log = <MethodCall>[];
-    channel.setMockMethodCallHandler((MethodCall methodCall) async {
-      log.add(methodCall);
+  setUp(() {
+    api = _FakeUrlLauncherApi();
+    plugin = UrlLauncherWindows(api: api);
+  });
 
-      // Return null explicitly instead of relying on the implicit null
-      // returned by the method channel if no return statement is specified.
-      return null;
+  test('registers instance', () {
+    UrlLauncherWindows.registerWith();
+    expect(UrlLauncherPlatform.instance, isA<UrlLauncherWindows>());
+  });
+
+  group('canLaunch', () {
+    test('handles true', () async {
+      api.canLaunch = true;
+
+      final bool result = await plugin.canLaunch('http://example.com/');
+
+      expect(result, isTrue);
+      expect(api.argument, 'http://example.com/');
     });
 
-    test('registers instance', () {
-      UrlLauncherWindows.registerWith();
-      expect(UrlLauncherPlatform.instance, isA<UrlLauncherWindows>());
-    });
+    test('handles false', () async {
+      api.canLaunch = false;
 
-    tearDown(() {
-      log.clear();
-    });
+      final bool result = await plugin.canLaunch('http://example.com/');
 
-    test('canLaunch', () async {
-      final UrlLauncherWindows launcher = UrlLauncherWindows();
-      await launcher.canLaunch('http://example.com/');
-      expect(
-        log,
-        <Matcher>[
-          isMethodCall('canLaunch', arguments: <String, Object>{
-            'url': 'http://example.com/',
-          })
-        ],
-      );
-    });
-
-    test('canLaunch should return false if platform returns null', () async {
-      final UrlLauncherWindows launcher = UrlLauncherWindows();
-      final bool canLaunch = await launcher.canLaunch('http://example.com/');
-
-      expect(canLaunch, false);
-    });
-
-    test('launch', () async {
-      final UrlLauncherWindows launcher = UrlLauncherWindows();
-      await launcher.launch(
-        'http://example.com/',
-        useSafariVC: true,
-        useWebView: false,
-        enableJavaScript: false,
-        enableDomStorage: false,
-        universalLinksOnly: false,
-        headers: const <String, String>{},
-      );
-      expect(
-        log,
-        <Matcher>[
-          isMethodCall('launch', arguments: <String, Object>{
-            'url': 'http://example.com/',
-            'enableJavaScript': false,
-            'enableDomStorage': false,
-            'universalLinksOnly': false,
-            'headers': <String, String>{},
-          })
-        ],
-      );
-    });
-
-    test('launch with headers', () async {
-      final UrlLauncherWindows launcher = UrlLauncherWindows();
-      await launcher.launch(
-        'http://example.com/',
-        useSafariVC: true,
-        useWebView: false,
-        enableJavaScript: false,
-        enableDomStorage: false,
-        universalLinksOnly: false,
-        headers: const <String, String>{'key': 'value'},
-      );
-      expect(
-        log,
-        <Matcher>[
-          isMethodCall('launch', arguments: <String, Object>{
-            'url': 'http://example.com/',
-            'enableJavaScript': false,
-            'enableDomStorage': false,
-            'universalLinksOnly': false,
-            'headers': <String, String>{'key': 'value'},
-          })
-        ],
-      );
-    });
-
-    test('launch universal links only', () async {
-      final UrlLauncherWindows launcher = UrlLauncherWindows();
-      await launcher.launch(
-        'http://example.com/',
-        useSafariVC: false,
-        useWebView: false,
-        enableJavaScript: false,
-        enableDomStorage: false,
-        universalLinksOnly: true,
-        headers: const <String, String>{},
-      );
-      expect(
-        log,
-        <Matcher>[
-          isMethodCall('launch', arguments: <String, Object>{
-            'url': 'http://example.com/',
-            'enableJavaScript': false,
-            'enableDomStorage': false,
-            'universalLinksOnly': true,
-            'headers': <String, String>{},
-          })
-        ],
-      );
-    });
-
-    test('launch should return false if platform returns null', () async {
-      final UrlLauncherWindows launcher = UrlLauncherWindows();
-      final bool launched = await launcher.launch(
-        'http://example.com/',
-        useSafariVC: true,
-        useWebView: false,
-        enableJavaScript: false,
-        enableDomStorage: false,
-        universalLinksOnly: false,
-        headers: const <String, String>{},
-      );
-
-      expect(launched, false);
+      expect(result, isFalse);
+      expect(api.argument, 'http://example.com/');
     });
   });
+
+  group('launch', () {
+    test('handles success', () async {
+      api.canLaunch = true;
+
+      expect(
+          plugin.launch(
+            'http://example.com/',
+            useSafariVC: true,
+            useWebView: false,
+            enableJavaScript: false,
+            enableDomStorage: false,
+            universalLinksOnly: false,
+            headers: const <String, String>{},
+          ),
+          completes);
+      expect(api.argument, 'http://example.com/');
+    });
+
+    test('handles failure', () async {
+      api.canLaunch = false;
+
+      await expectLater(
+          plugin.launch(
+            'http://example.com/',
+            useSafariVC: true,
+            useWebView: false,
+            enableJavaScript: false,
+            enableDomStorage: false,
+            universalLinksOnly: false,
+            headers: const <String, String>{},
+          ),
+          throwsA(isA<PlatformException>()));
+      expect(api.argument, 'http://example.com/');
+    });
+  });
+}
+
+class _FakeUrlLauncherApi implements UrlLauncherApi {
+  /// The argument that was passed to an API call.
+  String? argument;
+
+  /// Controls the behavior of the fake implementations.
+  ///
+  /// - [canLaunchUrl] returns this value.
+  /// - [launchUrl] throws if this is false.
+  bool canLaunch = false;
+
+  @override
+  Future<bool> canLaunchUrl(String url) async {
+    argument = url;
+    return canLaunch;
+  }
+
+  @override
+  Future<void> launchUrl(String url) async {
+    argument = url;
+    if (!canLaunch) {
+      throw PlatformException(code: 'Failed');
+    }
+  }
 }
diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
index a4185ac..a34bcb3 100644
--- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
+++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt
@@ -5,6 +5,8 @@
 set(PLUGIN_NAME "${PROJECT_NAME}_plugin")
 
 list(APPEND PLUGIN_SOURCES
+  "messages.g.cpp"
+  "messages.g.h"
   "system_apis.cpp"
   "system_apis.h"
   "url_launcher_plugin.cpp"
diff --git a/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp b/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp
new file mode 100644
index 0000000..eb1cf79
--- /dev/null
+++ b/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp
@@ -0,0 +1,113 @@
+// 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 (v5.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+#undef _HAS_EXCEPTIONS
+
+#include "messages.g.h"
+
+#include <flutter/basic_message_channel.h>
+#include <flutter/binary_messenger.h>
+#include <flutter/encodable_value.h>
+#include <flutter/standard_message_codec.h>
+
+#include <map>
+#include <optional>
+#include <string>
+
+namespace url_launcher_windows {
+
+/// The codec used by UrlLauncherApi.
+const flutter::StandardMessageCodec& UrlLauncherApi::GetCodec() {
+  return flutter::StandardMessageCodec::GetInstance(
+      &flutter::StandardCodecSerializer::GetInstance());
+}
+
+// Sets up an instance of `UrlLauncherApi` to handle messages through the
+// `binary_messenger`.
+void UrlLauncherApi::SetUp(flutter::BinaryMessenger* binary_messenger,
+                           UrlLauncherApi* api) {
+  {
+    auto channel =
+        std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>(
+            binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl",
+            &GetCodec());
+    if (api != nullptr) {
+      channel->SetMessageHandler(
+          [api](const flutter::EncodableValue& message,
+                const flutter::MessageReply<flutter::EncodableValue>& reply) {
+            try {
+              const auto& args = std::get<flutter::EncodableList>(message);
+              const auto& encodable_url_arg = args.at(0);
+              if (encodable_url_arg.IsNull()) {
+                reply(WrapError("url_arg unexpectedly null."));
+                return;
+              }
+              const auto& url_arg = std::get<std::string>(encodable_url_arg);
+              ErrorOr<bool> output = api->CanLaunchUrl(url_arg);
+              if (output.has_error()) {
+                reply(WrapError(output.error()));
+                return;
+              }
+              flutter::EncodableList wrapped;
+              wrapped.push_back(
+                  flutter::EncodableValue(std::move(output).TakeValue()));
+              reply(flutter::EncodableValue(std::move(wrapped)));
+            } catch (const std::exception& exception) {
+              reply(WrapError(exception.what()));
+            }
+          });
+    } else {
+      channel->SetMessageHandler(nullptr);
+    }
+  }
+  {
+    auto channel =
+        std::make_unique<flutter::BasicMessageChannel<flutter::EncodableValue>>(
+            binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.launchUrl",
+            &GetCodec());
+    if (api != nullptr) {
+      channel->SetMessageHandler(
+          [api](const flutter::EncodableValue& message,
+                const flutter::MessageReply<flutter::EncodableValue>& reply) {
+            try {
+              const auto& args = std::get<flutter::EncodableList>(message);
+              const auto& encodable_url_arg = args.at(0);
+              if (encodable_url_arg.IsNull()) {
+                reply(WrapError("url_arg unexpectedly null."));
+                return;
+              }
+              const auto& url_arg = std::get<std::string>(encodable_url_arg);
+              std::optional<FlutterError> output = api->LaunchUrl(url_arg);
+              if (output.has_value()) {
+                reply(WrapError(output.value()));
+                return;
+              }
+              flutter::EncodableList wrapped;
+              wrapped.push_back(flutter::EncodableValue());
+              reply(flutter::EncodableValue(std::move(wrapped)));
+            } catch (const std::exception& exception) {
+              reply(WrapError(exception.what()));
+            }
+          });
+    } else {
+      channel->SetMessageHandler(nullptr);
+    }
+  }
+}
+
+flutter::EncodableValue UrlLauncherApi::WrapError(
+    std::string_view error_message) {
+  return flutter::EncodableValue(flutter::EncodableList{
+      flutter::EncodableValue(std::string(error_message)),
+      flutter::EncodableValue("Error"), flutter::EncodableValue()});
+}
+flutter::EncodableValue UrlLauncherApi::WrapError(const FlutterError& error) {
+  return flutter::EncodableValue(flutter::EncodableList{
+      flutter::EncodableValue(error.message()),
+      flutter::EncodableValue(error.code()), error.details()});
+}
+
+}  // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/messages.g.h b/packages/url_launcher/url_launcher_windows/windows/messages.g.h
new file mode 100644
index 0000000..cb8e95f
--- /dev/null
+++ b/packages/url_launcher/url_launcher_windows/windows/messages.g.h
@@ -0,0 +1,86 @@
+// 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 (v5.0.1), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+#ifndef PIGEON_H_
+#define PIGEON_H_
+#include <flutter/basic_message_channel.h>
+#include <flutter/binary_messenger.h>
+#include <flutter/encodable_value.h>
+#include <flutter/standard_message_codec.h>
+
+#include <map>
+#include <optional>
+#include <string>
+
+namespace url_launcher_windows {
+
+// Generated class from Pigeon.
+
+class FlutterError {
+ public:
+  explicit FlutterError(const std::string& code) : code_(code) {}
+  explicit FlutterError(const std::string& code, const std::string& message)
+      : code_(code), message_(message) {}
+  explicit FlutterError(const std::string& code, const std::string& message,
+                        const flutter::EncodableValue& details)
+      : code_(code), message_(message), details_(details) {}
+
+  const std::string& code() const { return code_; }
+  const std::string& message() const { return message_; }
+  const flutter::EncodableValue& details() const { return details_; }
+
+ private:
+  std::string code_;
+  std::string message_;
+  flutter::EncodableValue details_;
+};
+
+template <class T>
+class ErrorOr {
+ public:
+  ErrorOr(const T& rhs) { new (&v_) T(rhs); }
+  ErrorOr(const T&& rhs) { v_ = std::move(rhs); }
+  ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(rhs); }
+  ErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); }
+
+  bool has_error() const { return std::holds_alternative<FlutterError>(v_); }
+  const T& value() const { return std::get<T>(v_); };
+  const FlutterError& error() const { return std::get<FlutterError>(v_); };
+
+ private:
+  friend class UrlLauncherApi;
+  ErrorOr() = default;
+  T TakeValue() && { return std::get<T>(std::move(v_)); }
+
+  std::variant<T, FlutterError> v_;
+};
+
+// Generated interface from Pigeon that represents a handler of messages from
+// Flutter.
+class UrlLauncherApi {
+ public:
+  UrlLauncherApi(const UrlLauncherApi&) = delete;
+  UrlLauncherApi& operator=(const UrlLauncherApi&) = delete;
+  virtual ~UrlLauncherApi(){};
+  virtual ErrorOr<bool> CanLaunchUrl(const std::string& url) = 0;
+  virtual std::optional<FlutterError> LaunchUrl(const std::string& url) = 0;
+
+  // The codec used by UrlLauncherApi.
+  static const flutter::StandardMessageCodec& GetCodec();
+  // Sets up an instance of `UrlLauncherApi` to handle messages through the
+  // `binary_messenger`.
+  static void SetUp(flutter::BinaryMessenger* binary_messenger,
+                    UrlLauncherApi* api);
+  static flutter::EncodableValue WrapError(std::string_view error_message);
+  static flutter::EncodableValue WrapError(const FlutterError& error);
+
+ protected:
+  UrlLauncherApi() = default;
+};
+
+}  // namespace url_launcher_windows
+
+#endif  // PIGEON_H_
diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp
index abd690b..cde95ee 100644
--- a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp
@@ -5,7 +5,7 @@
 
 #include <windows.h>
 
-namespace url_launcher_plugin {
+namespace url_launcher_windows {
 
 SystemApis::SystemApis() {}
 
@@ -35,4 +35,4 @@
                          show_flags);
 }
 
-}  // namespace url_launcher_plugin
+}  // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.h b/packages/url_launcher/url_launcher_windows/windows/system_apis.h
index 7b56704..c56c410 100644
--- a/packages/url_launcher/url_launcher_windows/windows/system_apis.h
+++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.h
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 #include <windows.h>
 
-namespace url_launcher_plugin {
+namespace url_launcher_windows {
 
 // An interface wrapping system APIs used by the plugin, for mocking.
 class SystemApis {
@@ -53,4 +53,4 @@
                                   int show_flags);
 };
 
-}  // namespace url_launcher_plugin
+}  // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
index 191d51a..9dd2be5 100644
--- a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp
@@ -9,11 +9,13 @@
 #include <windows.h>
 
 #include <memory>
+#include <optional>
 #include <string>
 
+#include "messages.g.h"
 #include "url_launcher_plugin.h"
 
-namespace url_launcher_plugin {
+namespace url_launcher_windows {
 namespace test {
 
 namespace {
@@ -42,30 +44,10 @@
               (override));
 };
 
-class MockMethodResult : public flutter::MethodResult<> {
- public:
-  MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result),
-              (override));
-  MOCK_METHOD(void, ErrorInternal,
-              (const std::string& error_code, const std::string& error_message,
-               const EncodableValue* details),
-              (override));
-  MOCK_METHOD(void, NotImplementedInternal, (), (override));
-};
-
-std::unique_ptr<EncodableValue> CreateArgumentsWithUrl(const std::string& url) {
-  EncodableMap args = {
-      {EncodableValue("url"), EncodableValue(url)},
-  };
-  return std::make_unique<EncodableValue>(args);
-}
-
 }  // namespace
 
 TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) {
   std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
-  std::unique_ptr<MockMethodResult> result =
-      std::make_unique<MockMethodResult>();
 
   // Return success values from the registery commands.
   HKEY fake_key = reinterpret_cast<HKEY>(1);
@@ -73,20 +55,16 @@
       .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS)));
   EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_SUCCESS));
   EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS));
-  // Expect a success response.
-  EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true))));
 
   UrlLauncherPlugin plugin(std::move(system));
-  plugin.HandleMethodCall(
-      flutter::MethodCall("canLaunch",
-                          CreateArgumentsWithUrl("https://some.url.com")),
-      std::move(result));
+  ErrorOr<bool> result = plugin.CanLaunchUrl("https://some.url.com");
+
+  ASSERT_FALSE(result.has_error());
+  EXPECT_TRUE(result.value());
 }
 
 TEST(UrlLauncherPlugin, CanLaunchQueryFailure) {
   std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
-  std::unique_ptr<MockMethodResult> result =
-      std::make_unique<MockMethodResult>();
 
   // Return success values from the registery commands, except for the query,
   // to simulate a scheme that is in the registry, but has no URL handler.
@@ -95,68 +73,52 @@
       .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS)));
   EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_FILE_NOT_FOUND));
   EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS));
-  // Expect a success response.
-  EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false))));
 
   UrlLauncherPlugin plugin(std::move(system));
-  plugin.HandleMethodCall(
-      flutter::MethodCall("canLaunch",
-                          CreateArgumentsWithUrl("https://some.url.com")),
-      std::move(result));
+  ErrorOr<bool> result = plugin.CanLaunchUrl("https://some.url.com");
+
+  ASSERT_FALSE(result.has_error());
+  EXPECT_FALSE(result.value());
 }
 
 TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) {
   std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
-  std::unique_ptr<MockMethodResult> result =
-      std::make_unique<MockMethodResult>();
 
   // Return failure for opening.
   EXPECT_CALL(*system, RegOpenKeyExW).WillOnce(Return(ERROR_BAD_PATHNAME));
-  // Expect a success response.
-  EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false))));
 
   UrlLauncherPlugin plugin(std::move(system));
-  plugin.HandleMethodCall(
-      flutter::MethodCall("canLaunch",
-                          CreateArgumentsWithUrl("https://some.url.com")),
-      std::move(result));
+  ErrorOr<bool> result = plugin.CanLaunchUrl("https://some.url.com");
+
+  ASSERT_FALSE(result.has_error());
+  EXPECT_FALSE(result.value());
 }
 
 TEST(UrlLauncherPlugin, LaunchSuccess) {
   std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
-  std::unique_ptr<MockMethodResult> result =
-      std::make_unique<MockMethodResult>();
 
   // Return a success value (>32) from launching.
   EXPECT_CALL(*system, ShellExecuteW)
       .WillOnce(Return(reinterpret_cast<HINSTANCE>(33)));
-  // Expect a success response.
-  EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true))));
 
   UrlLauncherPlugin plugin(std::move(system));
-  plugin.HandleMethodCall(
-      flutter::MethodCall("launch",
-                          CreateArgumentsWithUrl("https://some.url.com")),
-      std::move(result));
+  std::optional<FlutterError> error = plugin.LaunchUrl("https://some.url.com");
+
+  EXPECT_FALSE(error.has_value());
 }
 
 TEST(UrlLauncherPlugin, LaunchReportsFailure) {
   std::unique_ptr<MockSystemApis> system = std::make_unique<MockSystemApis>();
-  std::unique_ptr<MockMethodResult> result =
-      std::make_unique<MockMethodResult>();
 
   // Return a faile value (<=32) from launching.
   EXPECT_CALL(*system, ShellExecuteW)
       .WillOnce(Return(reinterpret_cast<HINSTANCE>(32)));
-  // Expect an error response.
-  EXPECT_CALL(*result, ErrorInternal);
 
   UrlLauncherPlugin plugin(std::move(system));
-  plugin.HandleMethodCall(
-      flutter::MethodCall("launch",
-                          CreateArgumentsWithUrl("https://some.url.com")),
-      std::move(result));
+  std::optional<FlutterError> error = plugin.LaunchUrl("https://some.url.com");
+
+  EXPECT_TRUE(error.has_value());
 }
 
 }  // namespace test
-}  // namespace url_launcher_plugin
+}  // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
index d5f2012..1dfee16 100644
--- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp
@@ -13,7 +13,9 @@
 #include <sstream>
 #include <string>
 
-namespace url_launcher_plugin {
+#include "messages.g.h"
+
+namespace url_launcher_windows {
 
 namespace {
 
@@ -62,18 +64,9 @@
 // static
 void UrlLauncherPlugin::RegisterWithRegistrar(
     flutter::PluginRegistrar* registrar) {
-  auto channel = std::make_unique<flutter::MethodChannel<>>(
-      registrar->messenger(), "plugins.flutter.io/url_launcher_windows",
-      &flutter::StandardMethodCodec::GetInstance());
-
   std::unique_ptr<UrlLauncherPlugin> plugin =
       std::make_unique<UrlLauncherPlugin>();
-
-  channel->SetMethodCallHandler(
-      [plugin_pointer = plugin.get()](const auto& call, auto result) {
-        plugin_pointer->HandleMethodCall(call, std::move(result));
-      });
-
+  UrlLauncherApi::SetUp(registrar->messenger(), plugin.get());
   registrar->AddPlugin(std::move(plugin));
 }
 
@@ -85,37 +78,7 @@
 
 UrlLauncherPlugin::~UrlLauncherPlugin() = default;
 
-void UrlLauncherPlugin::HandleMethodCall(
-    const flutter::MethodCall<>& method_call,
-    std::unique_ptr<flutter::MethodResult<>> result) {
-  if (method_call.method_name().compare("launch") == 0) {
-    std::string url = GetUrlArgument(method_call);
-    if (url.empty()) {
-      result->Error("argument_error", "No URL provided");
-      return;
-    }
-
-    std::optional<std::string> error = LaunchUrl(url);
-    if (error) {
-      result->Error("open_error", error.value());
-      return;
-    }
-    result->Success(EncodableValue(true));
-  } else if (method_call.method_name().compare("canLaunch") == 0) {
-    std::string url = GetUrlArgument(method_call);
-    if (url.empty()) {
-      result->Error("argument_error", "No URL provided");
-      return;
-    }
-
-    bool can_launch = CanLaunchUrl(url);
-    result->Success(EncodableValue(can_launch));
-  } else {
-    result->NotImplemented();
-  }
-}
-
-bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) {
+ErrorOr<bool> UrlLauncherPlugin::CanLaunchUrl(const std::string& url) {
   size_t separator_location = url.find(":");
   if (separator_location == std::string::npos) {
     return false;
@@ -134,7 +97,7 @@
   return has_handler;
 }
 
-std::optional<std::string> UrlLauncherPlugin::LaunchUrl(
+std::optional<FlutterError> UrlLauncherPlugin::LaunchUrl(
     const std::string& url) {
   std::wstring url_wide = Utf16FromUtf8(url);
 
@@ -147,9 +110,9 @@
     std::ostringstream error_message;
     error_message << "Failed to open " << url << ": ShellExecute error code "
                   << status;
-    return std::optional<std::string>(error_message.str());
+    return FlutterError("open_error", error_message.str());
   }
   return std::nullopt;
 }
 
-}  // namespace url_launcher_plugin
+}  // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h
index 45e70e5..e51cde6 100644
--- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h
+++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h
@@ -10,11 +10,12 @@
 #include <sstream>
 #include <string>
 
+#include "messages.g.h"
 #include "system_apis.h"
 
-namespace url_launcher_plugin {
+namespace url_launcher_windows {
 
-class UrlLauncherPlugin : public flutter::Plugin {
+class UrlLauncherPlugin : public flutter::Plugin, public UrlLauncherApi {
  public:
   static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar);
 
@@ -31,18 +32,12 @@
   UrlLauncherPlugin(const UrlLauncherPlugin&) = delete;
   UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete;
 
-  // Called when a method is called on the plugin channel.
-  void HandleMethodCall(const flutter::MethodCall<>& method_call,
-                        std::unique_ptr<flutter::MethodResult<>> result);
+  // UrlLauncherApi:
+  ErrorOr<bool> CanLaunchUrl(const std::string& url) override;
+  std::optional<FlutterError> LaunchUrl(const std::string& url) override;
 
  private:
-  // Returns whether or not the given URL has a registered handler.
-  bool CanLaunchUrl(const std::string& url);
-
-  // Attempts to launch the given URL. On failure, returns an error string.
-  std::optional<std::string> LaunchUrl(const std::string& url);
-
   std::unique_ptr<SystemApis> system_apis_;
 };
 
-}  // namespace url_launcher_plugin
+}  // namespace url_launcher_windows
diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp
index 05de586..7267093 100644
--- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp
+++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp
@@ -9,7 +9,7 @@
 
 void UrlLauncherWindowsRegisterWithRegistrar(
     FlutterDesktopPluginRegistrarRef registrar) {
-  url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar(
+  url_launcher_windows::UrlLauncherPlugin::RegisterWithRegistrar(
       flutter::PluginRegistrarManager::GetInstance()
           ->GetRegistrar<flutter::PluginRegistrarWindows>(registrar));
 }