diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index ce73ab26cad0..7bd33879cd7d 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.0 + +* Implements new cookie manager for setting cookies and providing initial cookies. + ## 2.7.0 * Adds support for the `loadRequest` method from the platform interface. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java new file mode 100644 index 000000000000..3e38ce94b3a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImpl.java @@ -0,0 +1,29 @@ +// 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. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.webkit.CookieManager; + +class CookieManagerHostApiImpl implements GeneratedAndroidWebView.CookieManagerHostApi { + @Override + public void clearCookies(GeneratedAndroidWebView.Result result) { + CookieManager cookieManager = CookieManager.getInstance(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + cookieManager.removeAllCookies(result::success); + } else { + final boolean hasCookies = cookieManager.hasCookies(); + if (hasCookies) { + cookieManager.removeAllCookie(); + } + result.success(hasCookies); + } + } + + @Override + public void setCookie(String url, String value) { + CookieManager.getInstance().setCookie(url, value); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java deleted file mode 100644 index df3f21daadeb..000000000000 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ /dev/null @@ -1,56 +0,0 @@ -// 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. - -package io.flutter.plugins.webviewflutter; - -import android.os.Build; -import android.os.Build.VERSION_CODES; -import android.webkit.CookieManager; -import android.webkit.ValueCallback; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; - -class FlutterCookieManager implements MethodCallHandler { - private final MethodChannel methodChannel; - - FlutterCookieManager(BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/cookie_manager"); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "clearCookies": - clearCookies(result); - break; - default: - result.notImplemented(); - } - } - - void dispose() { - methodChannel.setMethodCallHandler(null); - } - - private static void clearCookies(final Result result) { - CookieManager cookieManager = CookieManager.getInstance(); - final boolean hasCookies = cookieManager.hasCookies(); - if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies( - new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - result.success(hasCookies); - } - }); - } else { - cookieManager.removeAllCookie(); - result.success(hasCookies); - } - } -} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 0e6776ecd618..8ef0b8d11f96 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -162,6 +162,94 @@ public interface Result { void error(Throwable error); } + private static class CookieManagerHostApiCodec extends StandardMessageCodec { + public static final CookieManagerHostApiCodec INSTANCE = new CookieManagerHostApiCodec(); + + private CookieManagerHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface CookieManagerHostApi { + void clearCookies(Result result); + + void setCookie(String url, String value); + + /** The codec used by CookieManagerHostApi. */ + static MessageCodec getCodec() { + return CookieManagerHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `CookieManagerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.CookieManagerHostApi.clearCookies", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + Result resultCallback = + new Result() { + public void success(Boolean result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.clearCookies(resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CookieManagerHostApi.setCookie", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + String urlArg = (String) args.get(0); + if (urlArg == null) { + throw new NullPointerException("urlArg unexpectedly null."); + } + String valueArg = (String) args.get(1); + if (valueArg == null) { + throw new NullPointerException("valueArg unexpectedly null."); + } + api.setCookie(urlArg, valueArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static class WebViewHostApiCodec extends StandardMessageCodec { public static final WebViewHostApiCodec INSTANCE = new WebViewHostApiCodec(); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index cbeda8dcf493..4ef622fe47cb 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -13,6 +13,7 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; @@ -30,7 +31,6 @@ */ public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { private FlutterPluginBinding pluginBinding; - private FlutterCookieManager flutterCookieManager; private WebViewHostApiImpl webViewHostApi; private JavaScriptChannelHostApiImpl javaScriptChannelHostApi; @@ -65,7 +65,6 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra registrar.view(), new FlutterAssetManager.RegistrarFlutterAssetManager( registrar.context().getAssets(), registrar)); - new FlutterCookieManager(registrar.messenger()); } private void setUp( @@ -74,7 +73,6 @@ private void setUp( Context context, View containerView, FlutterAssetManager flutterAssetManager) { - new FlutterCookieManager(binaryMessenger); InstanceManager instanceManager = new InstanceManager(); @@ -117,6 +115,7 @@ private void setUp( instanceManager, new WebSettingsHostApiImpl.WebSettingsCreator())); FlutterAssetManagerHostApi.setup( binaryMessenger, new FlutterAssetManagerHostApiImpl(flutterAssetManager)); + CookieManagerHostApi.setup(binaryMessenger, new CookieManagerHostApiImpl()); } @Override @@ -132,14 +131,7 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { } @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - if (flutterCookieManager == null) { - return; - } - - flutterCookieManager.dispose(); - flutterCookieManager = null; - } + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java new file mode 100644 index 000000000000..6daeb1be7f63 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/CookieManagerHostApiImplTest.java @@ -0,0 +1,83 @@ +// 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. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Build; +import android.webkit.CookieManager; +import android.webkit.ValueCallback; +import io.flutter.plugins.webviewflutter.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class CookieManagerHostApiImplTest { + + private CookieManager cookieManager; + private MockedStatic staticMockCookieManager; + + @Before + public void setup() { + staticMockCookieManager = mockStatic(CookieManager.class); + cookieManager = mock(CookieManager.class); + when(CookieManager.getInstance()).thenReturn(cookieManager); + when(cookieManager.hasCookies()).thenReturn(true); + doAnswer( + answer -> { + ((ValueCallback) answer.getArgument(0)).onReceiveValue(true); + return null; + }) + .when(cookieManager) + .removeAllCookies(any()); + } + + @After + public void tearDown() { + staticMockCookieManager.close(); + } + + @Test + public void setCookieShouldCallSetCookie() { + // Setup + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.setCookie("flutter.dev", "foo=bar; path=/"); + // Verify + verify(cookieManager).setCookie("flutter.dev", "foo=bar; path=/"); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookiesOnAndroidLAbove() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.LOLLIPOP); + GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookies(any()); + verify(result).success(true); + } + + @Test + public void clearCookiesShouldCallRemoveAllCookieBelowAndroidL() { + // Setup + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.KITKAT_WATCH); + GeneratedAndroidWebView.Result result = mock(GeneratedAndroidWebView.Result.class); + CookieManagerHostApiImpl impl = new CookieManagerHostApiImpl(); + // Run + impl.clearCookies(result); + // Verify + verify(cookieManager).removeAllCookie(); + verify(result).success(true); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java new file mode 100644 index 000000000000..31e7d58ee13f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/utils/TestUtils.java @@ -0,0 +1,47 @@ +// 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. + +package io.flutter.plugins.webviewflutter.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import org.junit.Assert; + +public class TestUtils { + public static void setFinalStatic(Class classToModify, String fieldName, Object newValue) { + try { + Field field = classToModify.getField(fieldName); + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock static field: " + fieldName); + } + } + + public static void setPrivateField(T instance, String fieldName, Object newValue) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + } + } + + public static Object getPrivateField(T instance, String fieldName) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + return null; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index eb3162a179ff..0d0cd59af796 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -191,6 +191,7 @@ enum _MenuOptions { loadHtmlString, transparentBackground, doPostRequest, + setCookie, } class _SampleMenu extends StatelessWidget { @@ -244,6 +245,9 @@ class _SampleMenu extends StatelessWidget { case _MenuOptions.doPostRequest: _onDoPostRequest(controller.data!, context); break; + case _MenuOptions.setCookie: + _onSetCookie(controller.data!, context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -297,6 +301,10 @@ class _SampleMenu extends StatelessWidget { value: _MenuOptions.doPostRequest, child: Text('Post Request'), ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.setCookie, + child: Text('Set Cookie'), + ), ], ); }, @@ -356,7 +364,7 @@ class _SampleMenu extends StatelessWidget { Future _onClearCookies( WebViewController controller, BuildContext context) async { - final bool hadCookies = await WebView.platform.clearCookies(); + final bool hadCookies = await WebViewCookieManager.instance.clearCookies(); String message = 'There were cookies. Now, they are gone!'; if (!hadCookies) { message = 'There are no cookies.'; @@ -367,6 +375,15 @@ class _SampleMenu extends StatelessWidget { )); } + Future _onSetCookie( + WebViewController controller, BuildContext context) async { + await WebViewCookieManager.instance.setCookie( + const WebViewCookie( + name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'), + ); + await controller.loadUrl('https://httpbin.org/anything'); + } + Future _onNavigationDelegateExample( WebViewController controller, BuildContext context) async { final String contentBase64 = diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index adaf7fbb3ee8..91ea66376904 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -3,11 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'navigation_decision.dart'; @@ -61,6 +63,7 @@ class WebView extends StatefulWidget { Key? key, this.onWebViewCreated, this.initialUrl, + this.initialCookies = const [], this.javascriptMode = JavascriptMode.disabled, this.javascriptChannels, this.navigationDelegate, @@ -104,6 +107,9 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; + /// The initial cookies to set. + final List initialCookies; + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; @@ -295,6 +301,7 @@ class _WebViewState extends State { autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, userAgent: widget.userAgent, backgroundColor: widget.backgroundColor, + cookies: widget.initialCookies, ), javascriptChannelRegistry: _javascriptChannelRegistry, ); @@ -674,3 +681,21 @@ WebSettings _webSettingsFromWidget(WebView widget) { zoomEnabled: widget.zoomEnabled, ); } + +/// App-facing cookie manager that exposes the correct platform implementation. +class WebViewCookieManager extends WebViewCookieManagerPlatform { + WebViewCookieManager._(); + + /// Returns an instance of the cookie manager for the current platform. + static WebViewCookieManagerPlatform get instance { + if (WebViewCookieManagerPlatform.instance == null) { + if (Platform.isAndroid) { + WebViewCookieManagerPlatform.instance = WebViewAndroidCookieManager(); + } else { + throw AssertionError( + 'This platform is currently unsupported for webview_flutter_android.'); + } + } + return WebViewCookieManagerPlatform.instance!; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index d02701643291..dfa05cd92ee6 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -375,6 +375,49 @@ class WebView { } } +/// Manages cookies globally for all webviews. +class CookieManager { + CookieManager._(); + + static CookieManager? _instance; + + /// Gets the globally set CookieManager instance. + static CookieManager get instance => _instance ??= CookieManager._(); + + /// Setter for the singleton value, for testing purposes only. + @visibleForTesting + static set instance(CookieManager value) => _instance = value; + + /// Pigeon Host Api implementation for [CookieManager]. + @visibleForTesting + static CookieManagerHostApi api = CookieManagerHostApi(); + + /// Sets a single cookie (key-value pair) for the given URL. Any existing + /// cookie with the same host, path and name will be replaced with the new + /// cookie. The cookie being set will be ignored if it is expired. To set + /// multiple cookies, your application should invoke this method multiple + /// times. + /// + /// The value parameter must follow the format of the Set-Cookie HTTP + /// response header defined by RFC6265bis. This is a key-value pair of the + /// form "key=value", optionally followed by a list of cookie attributes + /// delimited with semicolons (ex. "key=value; Max-Age=123"). Please consult + /// the RFC specification for a list of valid attributes. + /// + /// Note: if specifying a value containing the "Secure" attribute, url must + /// use the "https://" scheme. + /// + /// Params: + /// url – the URL for which the cookie is to be set + /// value – the cookie as a string, using the format of the 'Set-Cookie' HTTP response header + Future setCookie(String url, String value) => api.setCookie(url, value); + + /// Removes all cookies. + /// + /// The returned future resolves to true if any cookies were removed. + Future clearCookies() => api.clearCookies(); +} + /// Manages settings state for a [WebView]. /// /// When a WebView is first created, it obtains a set of default settings. These diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart index e0a1db311b74..810a71732e7b 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart @@ -63,6 +63,72 @@ class WebResourceErrorData { } } +class _CookieManagerHostApiCodec extends StandardMessageCodec { + const _CookieManagerHostApiCodec(); +} + +class CookieManagerHostApi { + /// Constructor for [CookieManagerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + CookieManagerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _CookieManagerHostApiCodec(); + + Future clearCookies() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as bool?)!; + } + } + + Future setCookie(String arg_url, String arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_url, arg_value]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + class _WebViewHostApiCodec extends StandardMessageCodec { const _WebViewHostApiCodec(); } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart index ee474b9d43f8..1f0eb7bd7ded 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -65,5 +65,11 @@ class AndroidWebView implements WebViewPlatform { } @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); + Future clearCookies() { + if (WebViewCookieManagerPlatform.instance == null) { + throw Exception( + 'Could not clear cookies as no implementation for WebViewCookieManagerPlatform has been registered.'); + } + return WebViewCookieManagerPlatform.instance!.clearCookies(); + } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart new file mode 100644 index 000000000000..bba75ff4f613 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_cookie_manager.dart @@ -0,0 +1,36 @@ +// 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:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Handles all cookie operations for the current platform. +class WebViewAndroidCookieManager extends WebViewCookieManagerPlatform { + @override + Future clearCookies() => + android_webview.CookieManager.instance.clearCookies(); + + @override + Future setCookie(WebViewCookie cookie) { + if (!_isValidPath(cookie.path)) { + throw ArgumentError( + 'The path property for the provided cookie was not given a legal value.'); + } + return android_webview.CookieManager.instance.setCookie( + cookie.domain, + '${Uri.encodeComponent(cookie.name)}=${Uri.encodeComponent(cookie.value)}; path=${cookie.path}', + ); + } + + bool _isValidPath(String path) { + // Permitted ranges based on RFC6265bis: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-02#section-4.1.1 + for (final int char in path.codeUnits) { + if ((char < 0x20 || char > 0x3A) && (char < 0x3C || char > 0x7E)) { + return false; + } + } + return true; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart index fc295be65aa3..1dec9c105741 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android_widget.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'src/android_webview.dart' as android_webview; @@ -363,6 +364,14 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { } addJavascriptChannels(creationParams.javascriptChannelNames); + + // TODO(BeMacized): Remove once platform implementations + // are able to register themselves (Flutter >=2.8), + // https://github.com/flutter/flutter/issues/94224 + WebViewCookieManagerPlatform.instance ??= WebViewAndroidCookieManager(); + + creationParams.cookies + .forEach(WebViewCookieManagerPlatform.instance!.setCookie); } Future _setHasProgressTracking(bool hasProgressTracking) async { diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index 4907dadb41b7..36862f7cbacc 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -18,6 +18,14 @@ class WebResourceErrorData { String? description; } +@HostApi() +abstract class CookieManagerHostApi { + @async + bool clearCookies(); + + void setCookie(String url, String value); +} + @HostApi(dartHostTestHandler: 'TestWebViewHostApi') abstract class WebViewHostApi { void create(int instanceId, bool useHybridComposition); diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 08bcbb50602e..34bea570ae43 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.7.0 +version: 2.8.0 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index b903678ecef8..cc29fc755067 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -14,6 +14,7 @@ import 'android_webview.pigeon.dart'; import 'android_webview_test.mocks.dart'; @GenerateMocks([ + CookieManagerHostApi, DownloadListener, JavaScriptChannel, TestDownloadListenerHostApi, @@ -647,4 +648,20 @@ void main() { }); }); }); + + group('CookieManager', () { + test('setCookie calls setCookie on CookieManagerHostApi', () { + CookieManager.api = MockCookieManagerHostApi(); + CookieManager.instance.setCookie('foo', 'bar'); + verify(CookieManager.api.setCookie('foo', 'bar')); + }); + + test('clearCookies calls clearCookies on CookieManagerHostApi', () { + CookieManager.api = MockCookieManagerHostApi(); + when(CookieManager.api.clearCookies()) + .thenAnswer((_) => Future.value(true)); + CookieManager.instance.clearCookies(); + verify(CookieManager.api.clearCookies()); + }); + }); } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index 8bb569497b59..d25d2338886b 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -6,14 +6,15 @@ // in webview_flutter_android/test/android_webview_test.dart. // Do not manually edit this file. -import 'dart:async' as _i5; -import 'dart:typed_data' as _i4; -import 'dart:ui' as _i6; +import 'dart:async' as _i4; +import 'dart:typed_data' as _i6; +import 'dart:ui' as _i7; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; +import 'package:webview_flutter_android/src/android_webview.pigeon.dart' as _i3; -import 'android_webview.pigeon.dart' as _i3; +import 'android_webview.pigeon.dart' as _i5; // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters @@ -26,6 +27,28 @@ import 'android_webview.pigeon.dart' as _i3; class _FakeWebSettings_0 extends _i1.Fake implements _i2.WebSettings {} +/// A class which mocks [CookieManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCookieManagerHostApi extends _i1.Mock + implements _i3.CookieManagerHostApi { + MockCookieManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: Future.value(false)) as _i4.Future); + @override + _i4.Future setCookie(String? arg_url, String? arg_value) => + (super.noSuchMethod(Invocation.method(#setCookie, [arg_url, arg_value]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i4.Future); + @override + String toString() => super.toString(); +} + /// A class which mocks [DownloadListener]. /// /// See the documentation for Mockito's code generation for more information. @@ -69,7 +92,7 @@ class MockJavaScriptChannel extends _i1.Mock implements _i2.JavaScriptChannel { /// /// See the documentation for Mockito's code generation for more information. class MockTestDownloadListenerHostApi extends _i1.Mock - implements _i3.TestDownloadListenerHostApi { + implements _i5.TestDownloadListenerHostApi { MockTestDownloadListenerHostApi() { _i1.throwOnMissingStub(this); } @@ -86,7 +109,7 @@ class MockTestDownloadListenerHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestJavaScriptChannelHostApi extends _i1.Mock - implements _i3.TestJavaScriptChannelHostApi { + implements _i5.TestJavaScriptChannelHostApi { MockTestJavaScriptChannelHostApi() { _i1.throwOnMissingStub(this); } @@ -103,7 +126,7 @@ class MockTestJavaScriptChannelHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebChromeClientHostApi extends _i1.Mock - implements _i3.TestWebChromeClientHostApi { + implements _i5.TestWebChromeClientHostApi { MockTestWebChromeClientHostApi() { _i1.throwOnMissingStub(this); } @@ -121,7 +144,7 @@ class MockTestWebChromeClientHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebSettingsHostApi extends _i1.Mock - implements _i3.TestWebSettingsHostApi { + implements _i5.TestWebSettingsHostApi { MockTestWebSettingsHostApi() { _i1.throwOnMissingStub(this); } @@ -195,7 +218,7 @@ class MockTestWebSettingsHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebViewClientHostApi extends _i1.Mock - implements _i3.TestWebViewClientHostApi { + implements _i5.TestWebViewClientHostApi { MockTestWebViewClientHostApi() { _i1.throwOnMissingStub(this); } @@ -213,7 +236,7 @@ class MockTestWebViewClientHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestWebViewHostApi extends _i1.Mock - implements _i3.TestWebViewHostApi { + implements _i5.TestWebViewHostApi { MockTestWebViewHostApi() { _i1.throwOnMissingStub(this); } @@ -246,7 +269,7 @@ class MockTestWebViewHostApi extends _i1.Mock Invocation.method(#loadUrl, [instanceId, url, headers]), returnValueForMissingStub: null); @override - void postUrl(int? instanceId, String? url, _i4.Uint8List? data) => + void postUrl(int? instanceId, String? url, _i6.Uint8List? data) => super.noSuchMethod(Invocation.method(#postUrl, [instanceId, url, data]), returnValueForMissingStub: null); @override @@ -279,12 +302,12 @@ class MockTestWebViewHostApi extends _i1.Mock Invocation.method(#clearCache, [instanceId, includeDiskFiles]), returnValueForMissingStub: null); @override - _i5.Future evaluateJavascript( + _i4.Future evaluateJavascript( int? instanceId, String? javascriptString) => (super.noSuchMethod( Invocation.method( #evaluateJavascript, [instanceId, javascriptString]), - returnValue: Future.value('')) as _i5.Future); + returnValue: Future.value('')) as _i4.Future); @override String getTitle(int? instanceId) => (super.noSuchMethod(Invocation.method(#getTitle, [instanceId]), @@ -353,7 +376,7 @@ class MockTestWebViewHostApi extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockTestAssetManagerHostApi extends _i1.Mock - implements _i3.TestAssetManagerHostApi { + implements _i5.TestAssetManagerHostApi { MockTestAssetManagerHostApi() { _i1.throwOnMissingStub(this); } @@ -403,15 +426,15 @@ class MockWebView extends _i1.Mock implements _i2.WebView { (super.noSuchMethod(Invocation.getter(#settings), returnValue: _FakeWebSettings_0()) as _i2.WebSettings); @override - _i5.Future loadData( + _i4.Future loadData( {String? data, String? mimeType, String? encoding}) => (super.noSuchMethod( Invocation.method(#loadData, [], {#data: data, #mimeType: mimeType, #encoding: encoding}), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future loadDataWithBaseUrl( + _i4.Future loadDataWithBaseUrl( {String? baseUrl, String? data, String? mimeType, @@ -426,114 +449,114 @@ class MockWebView extends _i1.Mock implements _i2.WebView { #historyUrl: historyUrl }), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future loadUrl(String? url, Map? headers) => + _i4.Future loadUrl(String? url, Map? headers) => (super.noSuchMethod(Invocation.method(#loadUrl, [url, headers]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future postUrl(String? url, _i4.Uint8List? data) => + _i4.Future postUrl(String? url, _i6.Uint8List? data) => (super.noSuchMethod(Invocation.method(#postUrl, [url, data]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future getUrl() => + _i4.Future getUrl() => (super.noSuchMethod(Invocation.method(#getUrl, []), - returnValue: Future.value()) as _i5.Future); + returnValue: Future.value()) as _i4.Future); @override - _i5.Future canGoBack() => + _i4.Future canGoBack() => (super.noSuchMethod(Invocation.method(#canGoBack, []), - returnValue: Future.value(false)) as _i5.Future); + returnValue: Future.value(false)) as _i4.Future); @override - _i5.Future canGoForward() => + _i4.Future canGoForward() => (super.noSuchMethod(Invocation.method(#canGoForward, []), - returnValue: Future.value(false)) as _i5.Future); + returnValue: Future.value(false)) as _i4.Future); @override - _i5.Future goBack() => + _i4.Future goBack() => (super.noSuchMethod(Invocation.method(#goBack, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future goForward() => + _i4.Future goForward() => (super.noSuchMethod(Invocation.method(#goForward, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future reload() => + _i4.Future reload() => (super.noSuchMethod(Invocation.method(#reload, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future clearCache(bool? includeDiskFiles) => + _i4.Future clearCache(bool? includeDiskFiles) => (super.noSuchMethod(Invocation.method(#clearCache, [includeDiskFiles]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future evaluateJavascript(String? javascriptString) => (super + _i4.Future evaluateJavascript(String? javascriptString) => (super .noSuchMethod(Invocation.method(#evaluateJavascript, [javascriptString]), - returnValue: Future.value()) as _i5.Future); + returnValue: Future.value()) as _i4.Future); @override - _i5.Future getTitle() => + _i4.Future getTitle() => (super.noSuchMethod(Invocation.method(#getTitle, []), - returnValue: Future.value()) as _i5.Future); + returnValue: Future.value()) as _i4.Future); @override - _i5.Future scrollTo(int? x, int? y) => + _i4.Future scrollTo(int? x, int? y) => (super.noSuchMethod(Invocation.method(#scrollTo, [x, y]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future scrollBy(int? x, int? y) => + _i4.Future scrollBy(int? x, int? y) => (super.noSuchMethod(Invocation.method(#scrollBy, [x, y]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future getScrollX() => + _i4.Future getScrollX() => (super.noSuchMethod(Invocation.method(#getScrollX, []), - returnValue: Future.value(0)) as _i5.Future); + returnValue: Future.value(0)) as _i4.Future); @override - _i5.Future getScrollY() => + _i4.Future getScrollY() => (super.noSuchMethod(Invocation.method(#getScrollY, []), - returnValue: Future.value(0)) as _i5.Future); + returnValue: Future.value(0)) as _i4.Future); @override - _i5.Future setWebViewClient(_i2.WebViewClient? webViewClient) => + _i4.Future setWebViewClient(_i2.WebViewClient? webViewClient) => (super.noSuchMethod(Invocation.method(#setWebViewClient, [webViewClient]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future addJavaScriptChannel( + _i4.Future addJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( Invocation.method(#addJavaScriptChannel, [javaScriptChannel]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future removeJavaScriptChannel( + _i4.Future removeJavaScriptChannel( _i2.JavaScriptChannel? javaScriptChannel) => (super.noSuchMethod( Invocation.method(#removeJavaScriptChannel, [javaScriptChannel]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future setDownloadListener(_i2.DownloadListener? listener) => + _i4.Future setDownloadListener(_i2.DownloadListener? listener) => (super.noSuchMethod(Invocation.method(#setDownloadListener, [listener]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future setWebChromeClient(_i2.WebChromeClient? client) => + _i4.Future setWebChromeClient(_i2.WebChromeClient? client) => (super.noSuchMethod(Invocation.method(#setWebChromeClient, [client]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future setBackgroundColor(_i6.Color? color) => + _i4.Future setBackgroundColor(_i7.Color? color) => (super.noSuchMethod(Invocation.method(#setBackgroundColor, [color]), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override - _i5.Future release() => + _i4.Future release() => (super.noSuchMethod(Invocation.method(#release, []), returnValue: Future.value(), - returnValueForMissingStub: Future.value()) as _i5.Future); + returnValueForMissingStub: Future.value()) as _i4.Future); @override String toString() => super.toString(); } diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart new file mode 100644 index 000000000000..4f274ff4499f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.dart @@ -0,0 +1,53 @@ +// 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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' + as android_webview; +import 'package:webview_flutter_android/webview_android_cookie_manager.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'webview_android_cookie_manager_test.mocks.dart'; + +@GenerateMocks([android_webview.CookieManager]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + android_webview.CookieManager.instance = MockCookieManager(); + }); + + test('clearCookies should call android_webview.clearCookies', () { + when(android_webview.CookieManager.instance.clearCookies()) + .thenAnswer((_) => Future.value(true)); + WebViewAndroidCookieManager().clearCookies(); + verify(android_webview.CookieManager.instance.clearCookies()); + }); + + test('setCookie should throw ArgumentError for cookie with invalid path', () { + expect( + () => WebViewAndroidCookieManager().setCookie(const WebViewCookie( + name: 'foo', + value: 'bar', + domain: 'flutter.dev', + path: 'invalid;path', + )), + throwsA(const TypeMatcher()), + ); + }); + + test( + 'setCookie should call android_webview.csetCookie with properly formatted cookie value', + () { + WebViewAndroidCookieManager().setCookie(const WebViewCookie( + name: 'foo&', + value: 'bar@', + domain: 'flutter.dev', + )); + verify(android_webview.CookieManager.instance + .setCookie('flutter.dev', 'foo%26=bar%40; path=/')); + }); +} diff --git a/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart new file mode 100644 index 000000000000..131977bd3dfd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/test/webview_android_cookie_manager_test.mocks.dart @@ -0,0 +1,38 @@ +// Mocks generated by Mockito 5.0.16 from annotations +// in webview_flutter_android/test/webview_android_cookie_manager_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i3; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:webview_flutter_android/src/android_webview.dart' as _i2; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +/// A class which mocks [CookieManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCookieManager extends _i1.Mock implements _i2.CookieManager { + MockCookieManager() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future setCookie(String? url, String? value) => + (super.noSuchMethod(Invocation.method(#setCookie, [url, value]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i3.Future); + @override + _i3.Future clearCookies() => + (super.noSuchMethod(Invocation.method(#clearCookies, []), + returnValue: Future.value(false)) as _i3.Future); + @override + String toString() => super.toString(); +}