[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,
+ );
+ }
+ }
+ });
+}