[webview_flutter] Implementation of the webview_flutter_platform_interface package (#4302)

diff --git a/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS b/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS
new file mode 100644
index 0000000..78f9e5a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS
@@ -0,0 +1,67 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
+The Chromium Authors
+German Saprykin <saprykin.h@gmail.com>
+Benjamin Sauer <sauer.benjamin@gmail.com>
+larsenthomasj@gmail.com
+Ali Bitek <alibitek@protonmail.ch>
+Pol Batlló <pol.batllo@gmail.com>
+Anatoly Pulyaevskiy
+Hayden Flinner <haydenflinner@gmail.com>
+Stefano Rodriguez <hlsroddy@gmail.com>
+Salvatore Giordano <salvatoregiordanoo@gmail.com>
+Brian Armstrong <brian@flutter.institute>
+Paul DeMarco <paulmdemarco@gmail.com>
+Fabricio Nogueira <feufeu@gmail.com>
+Simon Lightfoot <simon@devangels.london>
+Ashton Thomas <ashton@acrinta.com>
+Thomas Danner <thmsdnnr@gmail.com>
+Diego Velásquez <diego.velasquez.lopez@gmail.com>
+Hajime Nakamura <nkmrhj@gmail.com>
+Tuyển Vũ Xuân <netsoft1985@gmail.com>
+Miguel Ruivo <miguel@miguelruivo.com>
+Sarthak Verma <sarthak@artiosys.com>
+Mike Diarmid <mike@invertase.io>
+Invertase <oss@invertase.io>
+Elliot Hesp <elliot@invertase.io>
+Vince Varga <vince.varga@smaho.com>
+Aawaz Gyawali <awazgyawali@gmail.com>
+EUI Limited <ian.evans3@admiralgroup.co.uk>
+Katarina Sheremet <katarina@sheremet.ch>
+Thomas Stockx <thomas@stockxit.com>
+Sarbagya Dhaubanjar <sarbagyastha@gmail.com>
+Ozkan Eksi <ozeksi@gmail.com>
+Rishab Nayak <rishab@bu.edu>
+ko2ic <ko2ic.dev@gmail.com>
+Jonathan Younger <jonathan@daikini.com>
+Jose Sanchez <josesm82@gmail.com>
+Debkanchan Samadder <debu.samadder@gmail.com>
+Audrius Karosevicius <audrius.karosevicius@gmail.com>
+Lukasz Piliszczuk <lukasz@intheloup.io>
+SoundReply Solutions GmbH <ch@soundreply.com>
+Rafal Wachol <rwachol@gmail.com>
+Pau Picas <pau.picas@gmail.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Alexandru Tuca <salexandru.tuca@outlook.com>
+Christian Weder <chrstian.weder@yapeal.ch>
+Rhodes Davis Jr. <rody.davis.jr@gmail.com>
+Luigi Agosti <luigi@tengio.com>
+Quentin Le Guennec <quentin@tengio.com>
+Koushik Ravikumar <koushik@tengio.com>
+Nissim Dsilva <nissim@tengio.com>
+Giancarlo Rocha <giancarloiff@gmail.com>
+Ryo Miyake <ryo@miyake.id>
+Théo Champion <contact.theochampion@gmail.com>
+Kazuki Yamaguchi <y.kazuki0614n@gmail.com>
+Eitan Schwartz <eshvartz@gmail.com>
+Chris Rutkowski <chrisrutkowski89@gmail.com>
+Juan Alvarez <juan.alvarez@resideo.com>
+Aleksandr Yurkovskiy <sanekyy@gmail.com>
+Anton Borries <mail@antonborri.es>
+Alex Li <google@alexv525.com>
+Rahul Raj <64.rahulraj@gmail.com>
+Maurits van Beusekom <maurits@baseflow.com>
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md
new file mode 100644
index 0000000..9e217a0
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+* Extracted platform interface from `webview_flutter`.
\ No newline at end of file
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/LICENSE b/packages/webview_flutter/webview_flutter_platform_interface/LICENSE
new file mode 100644
index 0000000..c6823b8
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2013 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/README.md b/packages/webview_flutter/webview_flutter_platform_interface/README.md
new file mode 100644
index 0000000..31e57ab
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/README.md
@@ -0,0 +1,23 @@
+# webview_flutter_platform_interface
+
+A common platform interface for the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin.
+
+This interface allows platform-specific implementations of the `webview_flutter`
+plugin, as well as the plugin itself, to ensure they are supporting the
+same interface.
+
+# Usage
+
+To implement a new platform-specific implementation of `webview_flutter`, extend
+[`WebviewPlatform`](lib/src/platform_interface/webview_platform.dart) with an implementation that performs the
+platform-specific behavior, and when you register your plugin, set the default
+`WebviewPlatform` by calling
+`WebviewPlatform.setInstance(MyPlatformWebview())`.
+
+# Note on breaking changes
+
+Strongly prefer non-breaking changes (such as adding a method to the interface)
+over breaking changes for this package.
+
+See https://flutter.dev/go/platform-interface-breaking-changes for a discussion
+on why a less-clean interface is preferable to a breaking change.
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart
new file mode 100644
index 0000000..b467daf
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart
@@ -0,0 +1,223 @@
+// 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 'dart:async';
+
+import 'package:flutter/services.dart';
+
+import '../platform_interface/javascript_channel_registry.dart';
+import '../platform_interface/platform_interface.dart';
+import '../types/types.dart';
+
+/// A [WebViewPlatformController] that uses a method channel to control the webview.
+class MethodChannelWebViewPlatform implements WebViewPlatformController {
+  /// Constructs an instance that will listen for webviews broadcasting to the
+  /// given [id], using the given [WebViewPlatformCallbacksHandler].
+  MethodChannelWebViewPlatform(
+    int id,
+    this._platformCallbacksHandler,
+    this._javascriptChannelRegistry,
+  )   : assert(_platformCallbacksHandler != null),
+        _channel = MethodChannel('plugins.flutter.io/webview_$id') {
+    _channel.setMethodCallHandler(_onMethodCall);
+  }
+
+  final JavascriptChannelRegistry _javascriptChannelRegistry;
+
+  final WebViewPlatformCallbacksHandler _platformCallbacksHandler;
+
+  final MethodChannel _channel;
+
+  static const MethodChannel _cookieManagerChannel =
+      MethodChannel('plugins.flutter.io/cookie_manager');
+
+  Future<bool?> _onMethodCall(MethodCall call) async {
+    switch (call.method) {
+      case 'javascriptChannelMessage':
+        final String channel = call.arguments['channel']!;
+        final String message = call.arguments['message']!;
+        _javascriptChannelRegistry.onJavascriptChannelMessage(channel, message);
+        return true;
+      case 'navigationRequest':
+        return await _platformCallbacksHandler.onNavigationRequest(
+          url: call.arguments['url']!,
+          isForMainFrame: call.arguments['isForMainFrame']!,
+        );
+      case 'onPageFinished':
+        _platformCallbacksHandler.onPageFinished(call.arguments['url']!);
+        return null;
+      case 'onProgress':
+        _platformCallbacksHandler.onProgress(call.arguments['progress']);
+        return null;
+      case 'onPageStarted':
+        _platformCallbacksHandler.onPageStarted(call.arguments['url']!);
+        return null;
+      case 'onWebResourceError':
+        _platformCallbacksHandler.onWebResourceError(
+          WebResourceError(
+            errorCode: call.arguments['errorCode']!,
+            description: call.arguments['description']!,
+            // iOS doesn't support `failingUrl`.
+            failingUrl: call.arguments['failingUrl'],
+            domain: call.arguments['domain'],
+            errorType: call.arguments['errorType'] == null
+                ? null
+                : WebResourceErrorType.values.firstWhere(
+                    (WebResourceErrorType type) {
+                      return type.toString() ==
+                          '$WebResourceErrorType.${call.arguments['errorType']}';
+                    },
+                  ),
+          ),
+        );
+        return null;
+    }
+
+    throw MissingPluginException(
+      '${call.method} was invoked but has no handler',
+    );
+  }
+
+  @override
+  Future<void> loadUrl(
+    String url,
+    Map<String, String>? headers,
+  ) async {
+    assert(url != null);
+    return _channel.invokeMethod<void>('loadUrl', <String, dynamic>{
+      'url': url,
+      'headers': headers,
+    });
+  }
+
+  @override
+  Future<String?> currentUrl() => _channel.invokeMethod<String>('currentUrl');
+
+  @override
+  Future<bool> canGoBack() =>
+      _channel.invokeMethod<bool>("canGoBack").then((result) => result!);
+
+  @override
+  Future<bool> canGoForward() =>
+      _channel.invokeMethod<bool>("canGoForward").then((result) => result!);
+
+  @override
+  Future<void> goBack() => _channel.invokeMethod<void>("goBack");
+
+  @override
+  Future<void> goForward() => _channel.invokeMethod<void>("goForward");
+
+  @override
+  Future<void> reload() => _channel.invokeMethod<void>("reload");
+
+  @override
+  Future<void> clearCache() => _channel.invokeMethod<void>("clearCache");
+
+  @override
+  Future<void> updateSettings(WebSettings settings) async {
+    final Map<String, dynamic> updatesMap = _webSettingsToMap(settings);
+    if (updatesMap.isNotEmpty) {
+      await _channel.invokeMethod<void>('updateSettings', updatesMap);
+    }
+  }
+
+  @override
+  Future<String> evaluateJavascript(String javascriptString) {
+    return _channel
+        .invokeMethod<String>('evaluateJavascript', javascriptString)
+        .then((result) => result!);
+  }
+
+  @override
+  Future<void> addJavascriptChannels(Set<String> javascriptChannelNames) {
+    return _channel.invokeMethod<void>(
+        'addJavascriptChannels', javascriptChannelNames.toList());
+  }
+
+  @override
+  Future<void> removeJavascriptChannels(Set<String> javascriptChannelNames) {
+    return _channel.invokeMethod<void>(
+        'removeJavascriptChannels', javascriptChannelNames.toList());
+  }
+
+  @override
+  Future<String?> getTitle() => _channel.invokeMethod<String>("getTitle");
+
+  @override
+  Future<void> scrollTo(int x, int y) {
+    return _channel.invokeMethod<void>('scrollTo', <String, int>{
+      'x': x,
+      'y': y,
+    });
+  }
+
+  @override
+  Future<void> scrollBy(int x, int y) {
+    return _channel.invokeMethod<void>('scrollBy', <String, int>{
+      'x': x,
+      'y': y,
+    });
+  }
+
+  @override
+  Future<int> getScrollX() =>
+      _channel.invokeMethod<int>("getScrollX").then((result) => result!);
+
+  @override
+  Future<int> getScrollY() =>
+      _channel.invokeMethod<int>("getScrollY").then((result) => result!);
+
+  /// Method channel implementation for [WebViewPlatform.clearCookies].
+  static Future<bool> clearCookies() {
+    return _cookieManagerChannel
+        .invokeMethod<bool>('clearCookies')
+        .then<bool>((dynamic result) => result!);
+  }
+
+  static Map<String, dynamic> _webSettingsToMap(WebSettings? settings) {
+    final Map<String, dynamic> map = <String, dynamic>{};
+    void _addIfNonNull(String key, dynamic value) {
+      if (value == null) {
+        return;
+      }
+      map[key] = value;
+    }
+
+    void _addSettingIfPresent<T>(String key, WebSetting<T> setting) {
+      if (!setting.isPresent) {
+        return;
+      }
+      map[key] = setting.value;
+    }
+
+    _addIfNonNull('jsMode', settings!.javascriptMode?.index);
+    _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate);
+    _addIfNonNull('hasProgressTracking', settings.hasProgressTracking);
+    _addIfNonNull('debuggingEnabled', settings.debuggingEnabled);
+    _addIfNonNull(
+        'gestureNavigationEnabled', settings.gestureNavigationEnabled);
+    _addIfNonNull(
+        'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback);
+    _addSettingIfPresent('userAgent', settings.userAgent);
+    return map;
+  }
+
+  /// Converts a [CreationParams] object to a map as expected by `platform_views` channel.
+  ///
+  /// This is used for the `creationParams` argument of the platform views created by
+  /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder].
+  static Map<String, dynamic> creationParamsToMap(
+    CreationParams creationParams, {
+    bool usesHybridComposition = false,
+  }) {
+    return <String, dynamic>{
+      'initialUrl': creationParams.initialUrl,
+      'settings': _webSettingsToMap(creationParams.webSettings),
+      'javascriptChannelNames': creationParams.javascriptChannelNames.toList(),
+      'userAgent': creationParams.userAgent,
+      'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index,
+      'usesHybridComposition': usesHybridComposition,
+    };
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart
new file mode 100644
index 0000000..142d8eb
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart
@@ -0,0 +1,42 @@
+// 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 '../types/javascript_channel.dart';
+import '../types/javascript_message.dart';
+
+/// Utility class for managing named JavaScript channels and forwarding incoming
+/// messages on the correct channel.
+class JavascriptChannelRegistry {
+  /// Constructs a [JavascriptChannelRegistry] initializing it with the given
+  /// set of [JavascriptChannel]s.
+  JavascriptChannelRegistry(Set<JavascriptChannel>? channels) {
+    updateJavascriptChannelsFromSet(channels);
+  }
+
+  /// Maps a channel name to a channel.
+  final Map<String, JavascriptChannel> channels = <String, JavascriptChannel>{};
+
+  /// Invoked when a JavaScript channel message is received.
+  void onJavascriptChannelMessage(String channel, String message) {
+    final JavascriptChannel? javascriptChannel = channels[channel];
+
+    if (javascriptChannel == null) {
+      throw ArgumentError('No channel registered with name $channel.');
+    }
+
+    javascriptChannel.onMessageReceived(JavascriptMessage(message));
+  }
+
+  /// Updates the set of [JavascriptChannel]s with the new set.
+  void updateJavascriptChannelsFromSet(Set<JavascriptChannel>? channels) {
+    this.channels.clear();
+    if (channels == null) {
+      return;
+    }
+
+    for (final JavascriptChannel channel in channels) {
+      this.channels[channel.name] = channel;
+    }
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart
new file mode 100644
index 0000000..43f967f
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart
@@ -0,0 +1,8 @@
+// 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.
+
+export 'javascript_channel_registry.dart';
+export 'webview_platform.dart';
+export 'webview_platform_callbacks_handler.dart';
+export 'webview_platform_controller.dart';
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart
new file mode 100644
index 0000000..4732f54
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart
@@ -0,0 +1,66 @@
+// 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:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/widgets.dart';
+import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart';
+
+import '../types/types.dart';
+import 'webview_platform_callbacks_handler.dart';
+import 'webview_platform_controller.dart';
+
+/// Signature for callbacks reporting that a [WebViewPlatformController] was created.
+///
+/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build].
+typedef WebViewPlatformCreatedCallback = void Function(
+    WebViewPlatformController? webViewPlatformController);
+
+/// Interface for a platform implementation of a WebView.
+///
+/// [WebView.platform] controls the builder that is used by [WebView].
+/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations
+/// for Android and iOS respectively.
+abstract class WebViewPlatform {
+  /// Builds a new WebView.
+  ///
+  /// Returns a Widget tree that embeds the created webview.
+  ///
+  /// `creationParams` are the initial parameters used to setup the webview.
+  ///
+  /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created
+  /// [WebViewPlatformController].
+  ///
+  /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController]
+  /// implementation is created with the [WebViewPlatformController] instance as a parameter.
+  ///
+  /// `gestureRecognizers` specifies which gestures should be consumed by the web view.
+  /// It is possible for other gesture recognizers to be competing with the web view on pointer
+  /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle
+  /// vertical drags. The web view will claim gestures that are recognized by any of the
+  /// recognizers on this list.
+  /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that
+  /// were not claimed by any other gesture recognizer.
+  ///
+  /// `webViewPlatformHandler` must not be null.
+  Widget build({
+    required BuildContext context,
+    // TODO(amirh): convert this to be the actual parameters.
+    // I'm starting without it as the PR is starting to become pretty big.
+    // I'll followup with the conversion PR.
+    required CreationParams creationParams,
+    required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler,
+    required JavascriptChannelRegistry javascriptChannelRegistry,
+    WebViewPlatformCreatedCallback? onWebViewPlatformCreated,
+    Set<Factory<OneSequenceGestureRecognizer>>? gestureRecognizers,
+  });
+
+  /// Clears all cookies for all [WebView] instances.
+  ///
+  /// Returns true if cookies were present before clearing, else false.
+  Future<bool> clearCookies() {
+    throw UnimplementedError(
+        "WebView clearCookies is not implemented on the current platform");
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart
new file mode 100644
index 0000000..44dae2e
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart
@@ -0,0 +1,32 @@
+// 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 'dart:async';
+
+import '../types/types.dart';
+
+/// Interface for callbacks made by [WebViewPlatformController].
+///
+/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController].
+/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview.
+abstract class WebViewPlatformCallbacksHandler {
+  /// Invoked by [WebViewPlatformController] when a navigation request is pending.
+  ///
+  /// If true is returned the navigation is allowed, otherwise it is blocked.
+  FutureOr<bool> onNavigationRequest(
+      {required String url, required bool isForMainFrame});
+
+  /// Invoked by [WebViewPlatformController] when a page has started loading.
+  void onPageStarted(String url);
+
+  /// Invoked by [WebViewPlatformController] when a page has finished loading.
+  void onPageFinished(String url);
+
+  /// Invoked by [WebViewPlatformController] when a page is loading.
+  /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`.
+  void onProgress(int progress);
+
+  /// Report web resource loading error to the host application.
+  void onWebResourceError(WebResourceError error);
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart
new file mode 100644
index 0000000..319ca7e
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart
@@ -0,0 +1,177 @@
+// 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 '../types/types.dart';
+import 'webview_platform_callbacks_handler.dart';
+
+/// Interface for talking to the webview's platform implementation.
+///
+/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is
+/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated].
+///
+/// Platform implementations that live in a separate package should extend this class rather than
+/// implement it as webview_flutter does not consider newly added methods to be breaking changes.
+/// Extending this class (using `extends`) ensures that the subclass will get the default
+/// implementation, while platform implementations that `implements` this interface will be broken
+/// by newly added [WebViewPlatformController] methods.
+abstract class WebViewPlatformController {
+  /// Creates a new WebViewPlatform.
+  ///
+  /// Callbacks made by the WebView will be delegated to `handler`.
+  ///
+  /// The `handler` parameter must not be null.
+  WebViewPlatformController(WebViewPlatformCallbacksHandler handler);
+
+  /// Loads the specified URL.
+  ///
+  /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will
+  /// be added as key value pairs of HTTP headers for the request.
+  ///
+  /// `url` must not be null.
+  ///
+  /// Throws an ArgumentError if `url` is not a valid URL string.
+  Future<void> loadUrl(
+    String url,
+    Map<String, String>? headers,
+  ) {
+    throw UnimplementedError(
+        "WebView loadUrl is not implemented on the current platform");
+  }
+
+  /// Updates the webview settings.
+  ///
+  /// Any non null field in `settings` will be set as the new setting value.
+  /// All null fields in `settings` are ignored.
+  Future<void> updateSettings(WebSettings setting) {
+    throw UnimplementedError(
+        "WebView updateSettings is not implemented on the current platform");
+  }
+
+  /// Accessor to the current URL that the WebView is displaying.
+  ///
+  /// If no URL was ever loaded, returns `null`.
+  Future<String?> currentUrl() {
+    throw UnimplementedError(
+        "WebView currentUrl is not implemented on the current platform");
+  }
+
+  /// Checks whether there's a back history item.
+  Future<bool> canGoBack() {
+    throw UnimplementedError(
+        "WebView canGoBack is not implemented on the current platform");
+  }
+
+  /// Checks whether there's a forward history item.
+  Future<bool> canGoForward() {
+    throw UnimplementedError(
+        "WebView canGoForward is not implemented on the current platform");
+  }
+
+  /// Goes back in the history of this WebView.
+  ///
+  /// If there is no back history item this is a no-op.
+  Future<void> goBack() {
+    throw UnimplementedError(
+        "WebView goBack is not implemented on the current platform");
+  }
+
+  /// Goes forward in the history of this WebView.
+  ///
+  /// If there is no forward history item this is a no-op.
+  Future<void> goForward() {
+    throw UnimplementedError(
+        "WebView goForward is not implemented on the current platform");
+  }
+
+  /// Reloads the current URL.
+  Future<void> reload() {
+    throw UnimplementedError(
+        "WebView reload is not implemented on the current platform");
+  }
+
+  /// Clears all caches used by the [WebView].
+  ///
+  /// The following caches are cleared:
+  ///	1. Browser HTTP Cache.
+  ///	2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches.
+  ///    These are not yet supported in iOS WkWebView. Service workers tend to use this cache.
+  ///	3. Application cache.
+  ///	4. Local Storage.
+  Future<void> clearCache() {
+    throw UnimplementedError(
+        "WebView clearCache is not implemented on the current platform");
+  }
+
+  /// Evaluates a JavaScript expression in the context of the current page.
+  ///
+  /// The Future completes with an error if a JavaScript error occurred, or if the type of the
+  /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated).
+  Future<String> evaluateJavascript(String javascriptString) {
+    throw UnimplementedError(
+        "WebView evaluateJavascript is not implemented on the current platform");
+  }
+
+  /// Adds new JavaScript channels to the set of enabled channels.
+  ///
+  /// For each value in this list the platform's webview should make sure that a corresponding
+  /// property with a postMessage method is set on `window`. For example for a JavaScript channel
+  /// named `Foo` it should be possible for JavaScript code executing in the webview to do
+  ///
+  /// ```javascript
+  /// Foo.postMessage('hello');
+  /// ```
+  ///
+  /// See also: [CreationParams.javascriptChannelNames].
+  Future<void> addJavascriptChannels(Set<String> javascriptChannelNames) {
+    throw UnimplementedError(
+        "WebView addJavascriptChannels is not implemented on the current platform");
+  }
+
+  /// Removes JavaScript channel names from the set of enabled channels.
+  ///
+  /// This disables channels that were previously enabled by [addJavaScriptChannels] or through
+  /// [CreationParams.javascriptChannelNames].
+  Future<void> removeJavascriptChannels(Set<String> javascriptChannelNames) {
+    throw UnimplementedError(
+        "WebView removeJavascriptChannels is not implemented on the current platform");
+  }
+
+  /// Returns the title of the currently loaded page.
+  Future<String?> getTitle() {
+    throw UnimplementedError(
+        "WebView getTitle is not implemented on the current platform");
+  }
+
+  /// Set the scrolled position of this view.
+  ///
+  /// The parameters `x` and `y` specify the position to scroll to in WebView pixels.
+  Future<void> scrollTo(int x, int y) {
+    throw UnimplementedError(
+        "WebView scrollTo is not implemented on the current platform");
+  }
+
+  /// Move the scrolled position of this view.
+  ///
+  /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by.
+  Future<void> scrollBy(int x, int y) {
+    throw UnimplementedError(
+        "WebView scrollBy is not implemented on the current platform");
+  }
+
+  /// Return the horizontal scroll position of this view.
+  ///
+  /// Scroll position is measured from left.
+  Future<int> getScrollX() {
+    throw UnimplementedError(
+        "WebView getScrollX is not implemented on the current platform");
+  }
+
+  /// Return the vertical scroll position of this view.
+  ///
+  /// Scroll position is measured from top.
+  Future<int> getScrollY() {
+    throw UnimplementedError(
+        "WebView getScrollY is not implemented on the current platform");
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart
new file mode 100644
index 0000000..7d6927a
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart
@@ -0,0 +1,22 @@
+// 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.
+
+/// Specifies possible restrictions on automatic media playback.
+///
+/// This is typically used in [WebView.initialMediaPlaybackPolicy].
+// The method channel implementation is marshalling this enum to the value's index, so the order
+// is important.
+enum AutoMediaPlaybackPolicy {
+  /// Starting any kind of media playback requires a user action.
+  ///
+  /// For example: JavaScript code cannot start playing media unless the code was executed
+  /// as a result of a user action (like a touch event).
+  require_user_action_for_all_media_types,
+
+  /// Starting any kind of media playback is always allowed.
+  ///
+  /// For example: JavaScript code that's triggered when the page is loaded can start playing
+  /// video or audio.
+  always_allow,
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart
new file mode 100644
index 0000000..f213e97
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart
@@ -0,0 +1,60 @@
+// 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 'auto_media_playback_policy.dart';
+import 'web_settings.dart';
+
+/// Configuration to use when creating a new [WebViewPlatformController].
+///
+/// The `autoMediaPlaybackPolicy` parameter must not be null.
+class CreationParams {
+  /// Constructs an instance to use when creating a new
+  /// [WebViewPlatformController].
+  ///
+  /// The `autoMediaPlaybackPolicy` parameter must not be null.
+  CreationParams({
+    this.initialUrl,
+    this.webSettings,
+    this.javascriptChannelNames = const <String>{},
+    this.userAgent,
+    this.autoMediaPlaybackPolicy =
+        AutoMediaPlaybackPolicy.require_user_action_for_all_media_types,
+  }) : assert(autoMediaPlaybackPolicy != null);
+
+  /// The initialUrl to load in the webview.
+  ///
+  /// When null the webview will be created without loading any page.
+  final String? initialUrl;
+
+  /// The initial [WebSettings] for the new webview.
+  ///
+  /// This can later be updated with [WebViewPlatformController.updateSettings].
+  final WebSettings? webSettings;
+
+  /// The initial set of JavaScript channels that are configured for this webview.
+  ///
+  /// For each value in this set the platform's webview should make sure that a corresponding
+  /// property with a postMessage method is set on `window`. For example for a JavaScript channel
+  /// named `Foo` it should be possible for JavaScript code executing in the webview to do
+  ///
+  /// ```javascript
+  /// Foo.postMessage('hello');
+  /// ```
+  // TODO(amirh): describe what should happen when postMessage is called once that code is migrated
+  // to PlatformWebView.
+  final Set<String> javascriptChannelNames;
+
+  /// The value used for the HTTP User-Agent: request header.
+  ///
+  /// When null the platform's webview default is used for the User-Agent header.
+  final String? userAgent;
+
+  /// Which restrictions apply on automatic media playback.
+  final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy;
+
+  @override
+  String toString() {
+    return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)';
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart
new file mode 100644
index 0000000..8b31f5b
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart
@@ -0,0 +1,39 @@
+// 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 'javascript_message.dart';
+
+/// Callback type for handling messages sent from Javascript running in a web view.
+typedef void JavascriptMessageHandler(JavascriptMessage message);
+
+final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$');
+
+/// A named channel for receiving messaged from JavaScript code running inside a web view.
+class JavascriptChannel {
+  /// Constructs a Javascript channel.
+  ///
+  /// The parameters `name` and `onMessageReceived` must not be null.
+  JavascriptChannel({
+    required this.name,
+    required this.onMessageReceived,
+  })  : assert(name != null),
+        assert(onMessageReceived != null),
+        assert(_validChannelNames.hasMatch(name));
+
+  /// The channel's name.
+  ///
+  /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to
+  /// the Javascript window object's property named `name`.
+  ///
+  /// The name must start with a letter or underscore(_), followed by any combination of those
+  /// characters plus digits.
+  ///
+  /// Note that any JavaScript existing `window` property with this name will be overriden.
+  ///
+  /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism.
+  final String name;
+
+  /// A callback that's invoked when a message is received through the channel.
+  final JavascriptMessageHandler onMessageReceived;
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart
new file mode 100644
index 0000000..8d08045
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart
@@ -0,0 +1,14 @@
+// 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.
+
+/// A message that was sent by JavaScript code running in a [WebView].
+class JavascriptMessage {
+  /// Constructs a JavaScript message object.
+  ///
+  /// The `message` parameter must not be null.
+  const JavascriptMessage(this.message) : assert(message != null);
+
+  /// The contents of the message that was sent by the JavaScript code.
+  final String message;
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart
new file mode 100644
index 0000000..53d0491
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart
@@ -0,0 +1,12 @@
+// 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.
+
+/// Describes the state of JavaScript support in a given web view.
+enum JavascriptMode {
+  /// JavaScript execution is disabled.
+  disabled,
+
+  /// JavaScript execution is not restricted.
+  unrestricted,
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart
new file mode 100644
index 0000000..b1a9b9b
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart
@@ -0,0 +1,12 @@
+// 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.
+
+export 'auto_media_playback_policy.dart';
+export 'creation_params.dart';
+export 'javascript_channel.dart';
+export 'javascript_message.dart';
+export 'javascript_mode.dart';
+export 'web_resource_error.dart';
+export 'web_resource_error_type.dart';
+export 'web_settings.dart';
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart
new file mode 100644
index 0000000..b61671f
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart
@@ -0,0 +1,57 @@
+// 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 'web_resource_error_type.dart';
+
+/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred.
+class WebResourceError {
+  /// Creates a new [WebResourceError]
+  ///
+  /// A user should not need to instantiate this class, but will receive one in
+  /// [WebResourceErrorCallback].
+  WebResourceError({
+    required this.errorCode,
+    required this.description,
+    this.domain,
+    this.errorType,
+    this.failingUrl,
+  })  : assert(errorCode != null),
+        assert(description != null);
+
+  /// Raw code of the error from the respective platform.
+  ///
+  /// On Android, the error code will be a constant from a
+  /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and
+  /// will have a corresponding [errorType].
+  ///
+  /// On iOS, the error code will be a constant from `NSError.code` in
+  /// Objective-C. See
+  /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html
+  /// for more information on error handling on iOS. Some possible error codes
+  /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc.
+  final int errorCode;
+
+  /// The domain of where to find the error code.
+  ///
+  /// This field is only available on iOS and represents a "domain" from where
+  /// the [errorCode] is from. This value is taken directly from an `NSError`
+  /// in Objective-C. See
+  /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html
+  /// for more information on error handling on iOS.
+  final String? domain;
+
+  /// Description of the error that can be used to communicate the problem to the user.
+  final String description;
+
+  /// The type this error can be categorized as.
+  ///
+  /// This will never be `null` on Android, but can be `null` on iOS.
+  final WebResourceErrorType? errorType;
+
+  /// Gets the URL for which the resource request was made.
+  ///
+  /// This value is not provided on iOS. Alternatively, you can keep track of
+  /// the last values provided to [WebViewPlatformController.loadUrl].
+  final String? failingUrl;
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart
new file mode 100644
index 0000000..a45816d
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart
@@ -0,0 +1,66 @@
+// 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.
+
+/// Possible error type categorizations used by [WebResourceError].
+enum WebResourceErrorType {
+  /// User authentication failed on server.
+  authentication,
+
+  /// Malformed URL.
+  badUrl,
+
+  /// Failed to connect to the server.
+  connect,
+
+  /// Failed to perform SSL handshake.
+  failedSslHandshake,
+
+  /// Generic file error.
+  file,
+
+  /// File not found.
+  fileNotFound,
+
+  /// Server or proxy hostname lookup failed.
+  hostLookup,
+
+  /// Failed to read or write to the server.
+  io,
+
+  /// User authentication failed on proxy.
+  proxyAuthentication,
+
+  /// Too many redirects.
+  redirectLoop,
+
+  /// Connection timed out.
+  timeout,
+
+  /// Too many requests during this load.
+  tooManyRequests,
+
+  /// Generic error.
+  unknown,
+
+  /// Resource load was canceled by Safe Browsing.
+  unsafeResource,
+
+  /// Unsupported authentication scheme (not basic or digest).
+  unsupportedAuthScheme,
+
+  /// Unsupported URI scheme.
+  unsupportedScheme,
+
+  /// The web content process was terminated.
+  webContentProcessTerminated,
+
+  /// The web view was invalidated.
+  webViewInvalidated,
+
+  /// A JavaScript exception occurred.
+  javaScriptExceptionOccurred,
+
+  /// The result of JavaScript execution could not be returned.
+  javaScriptResultTypeIsUnsupported,
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart
new file mode 100644
index 0000000..48b2de9
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart
@@ -0,0 +1,123 @@
+// 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:flutter/widgets.dart';
+
+import 'javascript_mode.dart';
+
+/// A single setting for configuring a WebViewPlatform which may be absent.
+class WebSetting<T> {
+  /// Constructs an absent setting instance.
+  ///
+  /// The [isPresent] field for the instance will be false.
+  ///
+  /// Accessing [value] for an absent instance will throw.
+  WebSetting.absent()
+      : _value = null,
+        isPresent = false;
+
+  /// Constructs a setting of the given `value`.
+  ///
+  /// The [isPresent] field for the instance will be true.
+  WebSetting.of(T value)
+      : _value = value,
+        isPresent = true;
+
+  final T? _value;
+
+  /// The setting's value.
+  ///
+  /// Throws if [WebSetting.isPresent] is false.
+  T get value {
+    if (!isPresent) {
+      throw StateError('Cannot access a value of an absent WebSetting');
+    }
+    assert(isPresent);
+    // The intention of this getter is to return T whether it is nullable or
+    // not whereas _value is of type T? since _value can be null even when
+    // T is not nullable (when isPresent == false).
+    //
+    // We promote _value to T using `as T` instead of `!` operator to handle
+    // the case when _value is legitimately null (and T is a nullable type).
+    // `!` operator would always throw if _value is null.
+    return _value as T;
+  }
+
+  /// True when this web setting instance contains a value.
+  ///
+  /// When false the [WebSetting.value] getter throws.
+  final bool isPresent;
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) return false;
+    final WebSetting<T> typedOther = other as WebSetting<T>;
+    return typedOther.isPresent == isPresent && typedOther._value == _value;
+  }
+
+  @override
+  int get hashCode => hashValues(_value, isPresent);
+}
+
+/// Settings for configuring a WebViewPlatform.
+///
+/// Initial settings are passed as part of [CreationParams], settings updates are sent with
+/// [WebViewPlatform#updateSettings].
+///
+/// The `userAgent` parameter must not be null.
+class WebSettings {
+  /// Construct an instance with initial settings. Future setting changes can be
+  /// sent with [WebviewPlatform#updateSettings].
+  ///
+  /// The `userAgent` parameter must not be null.
+  WebSettings({
+    this.javascriptMode,
+    this.hasNavigationDelegate,
+    this.hasProgressTracking,
+    this.debuggingEnabled,
+    this.gestureNavigationEnabled,
+    this.allowsInlineMediaPlayback,
+    required this.userAgent,
+  }) : assert(userAgent != null);
+
+  /// The JavaScript execution mode to be used by the webview.
+  final JavascriptMode? javascriptMode;
+
+  /// Whether the [WebView] has a [NavigationDelegate] set.
+  final bool? hasNavigationDelegate;
+
+  /// Whether the [WebView] should track page loading progress.
+  /// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress.
+  final bool? hasProgressTracking;
+
+  /// Whether to enable the platform's webview content debugging tools.
+  ///
+  /// See also: [WebView.debuggingEnabled].
+  final bool? debuggingEnabled;
+
+  /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS.
+  ///
+  /// This will have no effect on Android.
+  final bool? allowsInlineMediaPlayback;
+
+  /// The value used for the HTTP `User-Agent:` request header.
+  ///
+  /// If [userAgent.value] is null the platform's default user agent should be used.
+  ///
+  /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the
+  /// last time it was set.
+  ///
+  /// See also [WebView.userAgent].
+  final WebSetting<String?> userAgent;
+
+  /// Whether to allow swipe based navigation in iOS.
+  ///
+  /// See also: [WebView.gestureNavigationEnabled]
+  final bool? gestureNavigationEnabled;
+
+  @override
+  String toString() {
+    return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)';
+  }
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart
new file mode 100644
index 0000000..b508989
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart
@@ -0,0 +1,7 @@
+// 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.
+
+export 'src/platform_interface/platform_interface.dart';
+export 'src/types/types.dart';
+export 'src/method_channel/webview_method_channel.dart';
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml
new file mode 100644
index 0000000..bf43c26
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml
@@ -0,0 +1,22 @@
+name: webview_flutter_platform_interface
+description: A common platform interface for the webview_flutter plugin.
+repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_platform_interface
+issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22
+# NOTE: We strongly prefer non-breaking changes, even at the expense of a
+# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
+version: 1.0.0
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=2.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+  plugin_platform_interface: ^2.0.0
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  mockito: ^5.0.0
+  pedantic: ^1.10.0
\ No newline at end of file
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart
new file mode 100644
index 0000000..2f845ea
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart
@@ -0,0 +1,457 @@
+// 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:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+
+import 'package:webview_flutter_platform_interface/src/method_channel/webview_method_channel.dart';
+import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('Tests on `plugin.flutter.io/webview_<channel_id>` channel', () {
+    const int channelId = 1;
+    const MethodChannel channel =
+        MethodChannel('plugins.flutter.io/webview_$channelId');
+    final WebViewPlatformCallbacksHandler callbacksHandler =
+        MockWebViewPlatformCallbacksHandler();
+    final JavascriptChannelRegistry javascriptChannelRegistry =
+        MockJavascriptChannelRegistry();
+
+    final List<MethodCall> log = <MethodCall>[];
+    channel.setMockMethodCallHandler((MethodCall methodCall) async {
+      log.add(methodCall);
+
+      switch (methodCall.method) {
+        case 'currentUrl':
+          return 'https://test.url';
+        case 'canGoBack':
+        case 'canGoForward':
+          return true;
+        case 'evaluateJavascript':
+          return methodCall.arguments as String;
+        case 'getScrollX':
+          return 10;
+        case 'getScrollY':
+          return 20;
+      }
+
+      // Return null explicitly instead of relying on the implicit null
+      // returned by the method channel if no return statement is specified.
+      return null;
+    });
+
+    final MethodChannelWebViewPlatform webViewPlatform =
+        MethodChannelWebViewPlatform(
+      channelId,
+      callbacksHandler,
+      javascriptChannelRegistry,
+    );
+
+    tearDown(() {
+      log.clear();
+    });
+
+    test('loadUrl with headers', () async {
+      await webViewPlatform.loadUrl(
+        'https://test.url',
+        const <String, String>{
+          'Content-Type': 'text/plain',
+          'Accept': 'text/html',
+        },
+      );
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'loadUrl',
+            arguments: <String, dynamic>{
+              'url': 'https://test.url',
+              'headers': <String, String>{
+                'Content-Type': 'text/plain',
+                'Accept': 'text/html',
+              },
+            },
+          ),
+        ],
+      );
+    });
+
+    test('loadUrl without headers', () async {
+      await webViewPlatform.loadUrl(
+        'https://test.url',
+        null,
+      );
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'loadUrl',
+            arguments: <String, dynamic>{
+              'url': 'https://test.url',
+              'headers': null,
+            },
+          ),
+        ],
+      );
+    });
+
+    test('currentUrl', () async {
+      final String? currentUrl = await webViewPlatform.currentUrl();
+
+      expect(currentUrl, 'https://test.url');
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'currentUrl',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('canGoBack', () async {
+      final bool canGoBack = await webViewPlatform.canGoBack();
+
+      expect(canGoBack, true);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'canGoBack',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('canGoForward', () async {
+      final bool canGoForward = await webViewPlatform.canGoForward();
+
+      expect(canGoForward, true);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'canGoForward',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('goBack', () async {
+      await webViewPlatform.goBack();
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'goBack',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('goForward', () async {
+      await webViewPlatform.goForward();
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'goForward',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('reload', () async {
+      await webViewPlatform.reload();
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'reload',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('clearCache', () async {
+      await webViewPlatform.clearCache();
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'clearCache',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('updateSettings', () async {
+      final WebSettings settings =
+          WebSettings(userAgent: WebSetting<String?>.of('Dart Test'));
+      await webViewPlatform.updateSettings(settings);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'updateSettings',
+            arguments: <String, dynamic>{
+              'userAgent': 'Dart Test',
+            },
+          ),
+        ],
+      );
+    });
+
+    test('updateSettings all parameters', () async {
+      final WebSettings settings = WebSettings(
+        userAgent: WebSetting<String?>.of('Dart Test'),
+        javascriptMode: JavascriptMode.disabled,
+        hasNavigationDelegate: true,
+        hasProgressTracking: true,
+        debuggingEnabled: true,
+        gestureNavigationEnabled: true,
+        allowsInlineMediaPlayback: true,
+      );
+      await webViewPlatform.updateSettings(settings);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'updateSettings',
+            arguments: <String, dynamic>{
+              'userAgent': 'Dart Test',
+              'jsMode': 0,
+              'hasNavigationDelegate': true,
+              'hasProgressTracking': true,
+              'debuggingEnabled': true,
+              'gestureNavigationEnabled': true,
+              'allowsInlineMediaPlayback': true,
+            },
+          ),
+        ],
+      );
+    });
+
+    test('updateSettings without settings', () async {
+      final WebSettings settings =
+          WebSettings(userAgent: WebSetting<String?>.absent());
+      await webViewPlatform.updateSettings(settings);
+
+      expect(
+        log.isEmpty,
+        true,
+      );
+    });
+
+    test('evaluateJavascript', () async {
+      final String evaluateJavascript =
+          await webViewPlatform.evaluateJavascript(
+        'This simulates some Javascript code.',
+      );
+
+      expect('This simulates some Javascript code.', evaluateJavascript);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'evaluateJavascript',
+            arguments: 'This simulates some Javascript code.',
+          ),
+        ],
+      );
+    });
+
+    test('addJavascriptChannels', () async {
+      final Set<String> channels = <String>{'channel one', 'channel two'};
+      await webViewPlatform.addJavascriptChannels(channels);
+
+      expect(log, <Matcher>[
+        isMethodCall(
+          'addJavascriptChannels',
+          arguments: <String>[
+            'channel one',
+            'channel two',
+          ],
+        ),
+      ]);
+    });
+
+    test('addJavascriptChannels without channels', () async {
+      final Set<String> channels = <String>{};
+      await webViewPlatform.addJavascriptChannels(channels);
+
+      expect(log, <Matcher>[
+        isMethodCall(
+          'addJavascriptChannels',
+          arguments: <String>[],
+        ),
+      ]);
+    });
+
+    test('removeJavascriptChannels', () async {
+      final Set<String> channels = <String>{'channel one', 'channel two'};
+      await webViewPlatform.removeJavascriptChannels(channels);
+
+      expect(log, <Matcher>[
+        isMethodCall(
+          'removeJavascriptChannels',
+          arguments: <String>[
+            'channel one',
+            'channel two',
+          ],
+        ),
+      ]);
+    });
+
+    test('removeJavascriptChannels without channels', () async {
+      final Set<String> channels = <String>{};
+      await webViewPlatform.removeJavascriptChannels(channels);
+
+      expect(log, <Matcher>[
+        isMethodCall(
+          'removeJavascriptChannels',
+          arguments: <String>[],
+        ),
+      ]);
+    });
+
+    test('getTitle', () async {
+      final String? title = await webViewPlatform.getTitle();
+
+      expect(title, null);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getTitle', arguments: null),
+        ],
+      );
+    });
+
+    test('scrollTo', () async {
+      await webViewPlatform.scrollTo(10, 20);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'scrollTo',
+            arguments: <String, int>{
+              'x': 10,
+              'y': 20,
+            },
+          ),
+        ],
+      );
+    });
+
+    test('scrollBy', () async {
+      await webViewPlatform.scrollBy(10, 20);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'scrollBy',
+            arguments: <String, int>{
+              'x': 10,
+              'y': 20,
+            },
+          ),
+        ],
+      );
+    });
+
+    test('getScrollX', () async {
+      final int x = await webViewPlatform.getScrollX();
+
+      expect(x, 10);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'getScrollX',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+
+    test('getScrollY', () async {
+      final int y = await webViewPlatform.getScrollY();
+
+      expect(y, 20);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'getScrollY',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+  });
+
+  group('Tests on `plugins.flutter.io/cookie_manager` channel', () {
+    const MethodChannel cookieChannel =
+        MethodChannel('plugins.flutter.io/cookie_manager');
+
+    final List<MethodCall> log = <MethodCall>[];
+    cookieChannel.setMockMethodCallHandler((MethodCall methodCall) async {
+      log.add(methodCall);
+
+      if (methodCall.method == 'clearCookies') {
+        return true;
+      }
+
+      // Return null explicitly instead of relying on the implicit null
+      // returned by the method channel if no return statement is specified.
+      return null;
+    });
+
+    tearDown(() {
+      log.clear();
+    });
+
+    test('clearCookies', () async {
+      final bool clearCookies =
+          await MethodChannelWebViewPlatform.clearCookies();
+
+      expect(clearCookies, true);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall(
+            'clearCookies',
+            arguments: null,
+          ),
+        ],
+      );
+    });
+  });
+}
+
+class MockWebViewPlatformCallbacksHandler extends Mock
+    implements WebViewPlatformCallbacksHandler {}
+
+class MockJavascriptChannelRegistry extends Mock
+    implements JavascriptChannelRegistry {}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart
new file mode 100644
index 0000000..55d0e1e
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart
@@ -0,0 +1,119 @@
+// 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:flutter_test/flutter_test.dart';
+import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart';
+import 'package:webview_flutter_platform_interface/src/types/types.dart';
+import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart';
+
+void main() {
+  final Map<String, String> _log = <String, String>{};
+  final Set<JavascriptChannel> _channels = <JavascriptChannel>{
+    JavascriptChannel(
+      name: 'js_channel_1',
+      onMessageReceived: (JavascriptMessage message) =>
+          _log['js_channel_1'] = message.message,
+    ),
+    JavascriptChannel(
+      name: 'js_channel_2',
+      onMessageReceived: (JavascriptMessage message) =>
+          _log['js_channel_2'] = message.message,
+    ),
+    JavascriptChannel(
+      name: 'js_channel_3',
+      onMessageReceived: (JavascriptMessage message) =>
+          _log['js_channel_3'] = message.message,
+    ),
+  };
+
+  tearDown(() {
+    _log.clear();
+  });
+
+  test('ctor should initialize with channels.', () {
+    final JavascriptChannelRegistry registry =
+        JavascriptChannelRegistry(_channels);
+
+    expect(registry.channels.length, 3);
+    for (final JavascriptChannel channel in _channels) {
+      expect(registry.channels[channel.name], channel);
+    }
+  });
+
+  test('onJavascriptChannelMessage should forward message on correct channel.',
+      () {
+    final JavascriptChannelRegistry registry =
+        JavascriptChannelRegistry(_channels);
+
+    registry.onJavascriptChannelMessage(
+      'js_channel_2',
+      'test message on channel 2',
+    );
+
+    expect(
+        _log,
+        containsPair(
+          'js_channel_2',
+          'test message on channel 2',
+        ));
+  });
+
+  test(
+      'onJavascriptChannelMessage should throw ArgumentError when message arrives on non-existing channel.',
+      () {
+    final JavascriptChannelRegistry registry =
+        JavascriptChannelRegistry(_channels);
+
+    expect(
+        () => registry.onJavascriptChannelMessage(
+              'js_channel_4',
+              'test message on channel 2',
+            ),
+        throwsA(
+          isA<ArgumentError>().having((ArgumentError error) => error.message,
+              'message', 'No channel registered with name js_channel_4.'),
+        ));
+  });
+
+  test(
+      'updateJavascriptChannelsFromSet should clear all channels when null is supplied.',
+      () {
+    final JavascriptChannelRegistry registry =
+        JavascriptChannelRegistry(_channels);
+
+    expect(registry.channels.length, 3);
+
+    registry.updateJavascriptChannelsFromSet(null);
+
+    expect(registry.channels, isEmpty);
+  });
+
+  test('updateJavascriptChannelsFromSet should update registry with new set.',
+      () {
+    final JavascriptChannelRegistry registry =
+        JavascriptChannelRegistry(_channels);
+
+    expect(registry.channels.length, 3);
+
+    final Set<JavascriptChannel> newChannels = <JavascriptChannel>{
+      JavascriptChannel(
+        name: 'new_js_channel_1',
+        onMessageReceived: (JavascriptMessage message) =>
+            _log['new_js_channel_1'] = message.message,
+      ),
+      JavascriptChannel(
+        name: 'new_js_channel_2',
+        onMessageReceived: (JavascriptMessage message) =>
+            _log['new_js_channel_2'] = message.message,
+      ),
+    };
+
+    registry.updateJavascriptChannelsFromSet(newChannels);
+
+    expect(registry.channels.length, 2);
+    for (final JavascriptChannel channel in newChannels) {
+      expect(registry.channels[channel.name], channel);
+    }
+  });
+}
diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart
new file mode 100644
index 0000000..f481edd
--- /dev/null
+++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart
@@ -0,0 +1,48 @@
+// 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:flutter_test/flutter_test.dart';
+import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart';
+
+void main() {
+  final List<String> _validChars =
+      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'.split('');
+  final List<String> _commonInvalidChars =
+      r'`~!@#$%^&*()-=+[]{}\|"' ':;/?<>,. '.split('');
+  final List<int> _digits = List<int>.generate(10, (int index) => index++);
+
+  test(
+      'ctor should create JavascriptChannel when name starts with a valid character followed by a number.',
+      () {
+    for (final String char in _validChars) {
+      for (final int digit in _digits) {
+        final JavascriptChannel channel =
+            JavascriptChannel(name: '$char$digit', onMessageReceived: (_) {});
+
+        expect(channel.name, '$char$digit');
+      }
+    }
+  });
+
+  test('ctor should assert when channel name starts with a number.', () {
+    for (final int i in _digits) {
+      expect(
+        () => JavascriptChannel(name: '$i', onMessageReceived: (_) {}),
+        throwsAssertionError,
+      );
+    }
+  });
+
+  test('ctor should assert when channel contains invalid char.', () {
+    for (final String validChar in _validChars) {
+      for (final String invalidChar in _commonInvalidChars) {
+        expect(
+          () => JavascriptChannel(
+              name: validChar + invalidChar, onMessageReceived: (_) {}),
+          throwsAssertionError,
+        );
+      }
+    }
+  });
+}