diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 9f8374332e9d..630f97240ab0 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## 4.5.0 + +* Adds support to intercept JavaScript dialog. + Sees `WebViewController.setOnJavaScriptAlertDialog`, + `WebViewController.setOnJavaScriptConfirmDialog`, + `WebViewController.setOnJavaScriptPromptDialog`. + ## 4.4.1 * Exposes `JavaScriptLogLevel` from platform interface. diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 96cbbf8f7e36..fd6b09ab160f 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -243,6 +243,9 @@ enum MenuOptions { transparentBackground, setCookie, logExample, + jsAlert, + jsConfirm, + jsPrompt, } class SampleMenu extends StatelessWidget { @@ -302,6 +305,15 @@ class SampleMenu extends StatelessWidget { case MenuOptions.logExample: _onLogExample(); break; + case MenuOptions.jsAlert: + _onJavaScriptAlertExample(context); + break; + case MenuOptions.jsConfirm: + _onJavaScriptConfirmExample(context); + break; + case MenuOptions.jsPrompt: + _onJavaScriptPromptExample(context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -362,6 +374,18 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.logExample, child: Text('Log example'), ), + const PopupMenuItem( + value: MenuOptions.jsAlert, + child: Text('JavaScript Alert example'), + ), + const PopupMenuItem( + value: MenuOptions.jsConfirm, + child: Text('JavaScript Confirm example'), + ), + const PopupMenuItem( + value: MenuOptions.jsPrompt, + child: Text('JavaScript Prompt example'), + ), ], ); } @@ -481,6 +505,98 @@ class SampleMenu extends StatelessWidget { return webViewController.loadHtmlString(kTransparentBackgroundPage); } + Future _onJavaScriptAlertExample(BuildContext context) { + webViewController.setOnJavaScriptAlertDialog((String message) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: ( + BuildContext context, + ) { + return AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK')) + ], + ); + }); + }); + return webViewController + .runJavaScript("alert('This is a JavaScript alert dialog');"); + } + + Future _onJavaScriptConfirmExample(BuildContext context) { + webViewController.setOnJavaScriptConfirmDialog((String message) async { + final bool? result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('OK'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ) + ], + ); + }); + debugPrint('result=$result'); + return result ?? false; + }); + return webViewController + .runJavaScript("confirm('This is a JavaScript confirm dialog');"); + } + + Future _onJavaScriptPromptExample(BuildContext context) { + final TextEditingController textEditingController = TextEditingController(); + webViewController.setOnJavaScriptPromptDialog( + (String message, String? defaultText) async { + textEditingController.text = defaultText ?? ''; + final String? result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text(message), + content: TextField( + controller: textEditingController, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(textEditingController.text); + }, + child: const Text('OK'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + child: const Text('Cancel'), + ) + ], + ); + }); + debugPrint('result=$result'); + return result ?? ''; + }); + return webViewController + .runJavaScript("prompt('This is a JavaScript prompt dialog');"); + } + Widget _getCookieList(String cookies) { if (cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart index 0b81978d6fcc..55f9c9e53828 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart @@ -370,6 +370,28 @@ class WebViewController { return platform.setOnConsoleMessage(onConsoleMessage); } + /// Sets a callback that notifies the host application that the web page + /// wants to display a JavaScript alert() dialog. + Future setOnJavaScriptAlertDialog( + Future Function(String message) onJavaScriptAlertDialog) async { + return platform.setOnJavaScriptAlertDialog(onJavaScriptAlertDialog); + } + + /// Sets a callback that notifies the host application that the web page + /// wants to display a JavaScript confirm() dialog. + Future setOnJavaScriptConfirmDialog( + Future Function(String message) onJavaScriptConfirmDialog) async { + return platform.setOnJavaScriptConfirmDialog(onJavaScriptConfirmDialog); + } + + /// Sets a callback that notifies the host application that the web page + /// wants to display a JavaScript prompt() dialog. + Future setOnJavaScriptPromptDialog( + Future Function(String message, String? defaultText) + onJavaScriptPromptDialog) async { + return platform.setOnJavaScriptPromptDialog(onJavaScriptPromptDialog); + } + /// Gets the value used for the HTTP `User-Agent:` request header. Future getUserAgent() { return platform.getUserAgent(); diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 75b9efcc23a4..925d04e4f5fc 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 4.4.1 +version: 4.5.0 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 12f7ba007d26..38b10f1389e0 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.13.0 + +* Adds support to intercept JavaScript dialog. + Sees `PlatformWebViewController.setOnJavaScriptAlertDialog`, + `PlatformWebViewController.setOnJavaScriptConfirmDialog`, + `PlatformWebViewController.setOnJavaScriptPromptDialog`. + ## 3.12.0 * Adds support for `PlatformWebViewController.getUserAgent`. 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 8e8a12cbd305..50d1566ac226 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 @@ -2620,6 +2620,15 @@ void setSynchronousReturnValueForOnShowFileChooser( void setSynchronousReturnValueForOnConsoleMessage( @NonNull Long instanceId, @NonNull Boolean value); + void setSynchronousReturnValueForOnJsAlert( + @NonNull Long instanceId, @NonNull Boolean value); + + void setSynchronousReturnValueForOnJsConfirm( + @NonNull Long instanceId, @NonNull Boolean value); + + void setSynchronousReturnValueForOnJsPrompt( + @NonNull Long instanceId, @NonNull Boolean value); + /** The codec used by WebChromeClientHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -2709,6 +2718,81 @@ static void setup( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnJsAlert", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + Boolean valueArg = (Boolean) args.get(1); + try { + api.setSynchronousReturnValueForOnJsAlert((instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnJsConfirm", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + Boolean valueArg = (Boolean) args.get(1); + try { + api.setSynchronousReturnValueForOnJsConfirm((instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnJsPrompt", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + Boolean valueArg = (Boolean) args.get(1); + try { + api.setSynchronousReturnValueForOnJsPrompt((instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); + wrapped.add(0, null); + } + catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ @@ -2862,6 +2946,57 @@ public void onShowFileChooser( callback.reply(output); }); } + public void onJsAlert( + @NonNull Long instanceIdArg, + @NonNull String messageArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsAlert", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList(instanceIdArg, messageArg)), + channelReply -> callback.reply(null)); + } + public void onJsConfirm( + @NonNull Long instanceIdArg, + @NonNull String messageArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsConfirm", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList(instanceIdArg, messageArg)), + channelReply -> { + @SuppressWarnings("ConstantConditions") + Boolean output = (Boolean) channelReply; + callback.reply(output); + }); + } + public void onJsPrompt( + @NonNull Long instanceIdArg, + @NonNull String messageArg, + @NonNull String defaultValueArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsPrompt", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList(instanceIdArg, messageArg, defaultValueArg)), + channelReply -> { + @SuppressWarnings("ConstantConditions") + String output = (String) channelReply; + callback.reply(output); + }); + } /** Callback to Dart function `WebChromeClient.onPermissionRequest`. */ public void onPermissionRequest( @NonNull Long instanceIdArg, diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index b383dfdba11e..5ff230aee543 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -12,6 +12,7 @@ import android.webkit.WebChromeClient; import android.webkit.WebView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; @@ -93,6 +94,41 @@ public void onShowFileChooser( callback); } + /** Passes arguments from {@link WebChromeClient#onJsAlert} to Dart. */ + public void onJsAlert( + @NonNull WebChromeClient webChromeClient, + @NonNull String message, + @NonNull Reply callback) { + onJsAlert( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webChromeClient)), + message, + callback); + } + + /** Passes arguments from {@link WebChromeClient#onJsConfirm} to Dart. */ + public void onJsConfirm( + @NonNull WebChromeClient webChromeClient, + @NonNull String message, + @NonNull Reply callback) { + onJsConfirm( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webChromeClient)), + message, + callback); + } + + /** Passes arguments from {@link WebChromeClient#onJsPrompt} to Dart. */ + public void onJsPrompt( + @NonNull WebChromeClient webChromeClient, + @NonNull String message, + @Nullable String defaultValue, + @NonNull Reply callback) { + onJsPrompt( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webChromeClient)), + message, + defaultValue, + callback); + } + /** Passes arguments from {@link WebChromeClient#onGeolocationPermissionsShowPrompt} to Dart. */ public void onGeolocationPermissionsShowPrompt( @NonNull WebChromeClient webChromeClient, diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index cb382d51f2b1..65422806f3d9 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -11,6 +11,8 @@ import android.view.View; import android.webkit.ConsoleMessage; import android.webkit.GeolocationPermissions; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; import android.webkit.PermissionRequest; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; @@ -42,7 +44,9 @@ public static class WebChromeClientImpl extends SecureWebChromeClient { private final WebChromeClientFlutterApiImpl flutterApi; private boolean returnValueForOnShowFileChooser = false; private boolean returnValueForOnConsoleMessage = false; - + private boolean returnValueForOnJsAlert = false; + private boolean returnValueForOnJsConfirm = false; + private boolean returnValueForOnJsPrompt = false; /** * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart. * @@ -103,6 +107,62 @@ public boolean onShowFileChooser( return currentReturnValueForOnShowFileChooser; } + @Override + public boolean onJsAlert( + WebView view, + String url, + String message, + JsResult result) { + final boolean currentReturnValueForOnJsAlert = returnValueForOnJsAlert; + flutterApi.onJsAlert( + this, + message, + reply -> result.confirm()); + return currentReturnValueForOnJsAlert; + } + + @Override + public boolean onJsConfirm( + WebView view, + String url, + String message, + JsResult result) { + final boolean currentReturnValueForOnJsConfirm = returnValueForOnJsConfirm; + flutterApi.onJsConfirm( + this, + message, + reply -> { + if (reply) { + result.confirm(); + } else { + result.cancel(); + } + }); + return currentReturnValueForOnJsConfirm; + } + + @Override + public boolean onJsPrompt( + WebView view, + String url, + String message, + String defaultValue, + JsPromptResult result) { + final boolean currentReturnValueForOnJsPrompt = returnValueForOnJsPrompt; + flutterApi.onJsPrompt( + this, + message, + defaultValue, + reply -> { + if (reply != null) { + result.confirm(reply); + } else { + result.cancel(); + } + }); + return currentReturnValueForOnJsPrompt; + } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override public void onPermissionRequest(@NonNull PermissionRequest request) { @@ -124,6 +184,21 @@ public void setReturnValueForOnShowFileChooser(boolean value) { public void setReturnValueForOnConsoleMessage(boolean value) { returnValueForOnConsoleMessage = value; } + + /** Sets return value for {@link #onJsAlert}. */ + public void setReturnValueForOnJsAlert(boolean value) { + returnValueForOnJsAlert = value; + } + + /** Sets return value for {@link #onJsConfirm}. */ + public void setReturnValueForOnJsConfirm(boolean value) { + returnValueForOnJsConfirm = value; + } + + /** Sets return value for {@link #onJsPrompt}. */ + public void setReturnValueForOnJsPrompt(boolean value) { + returnValueForOnJsPrompt = value; + } } /** @@ -267,4 +342,25 @@ public void setSynchronousReturnValueForOnConsoleMessage( Objects.requireNonNull(instanceManager.getInstance(instanceId)); webChromeClient.setReturnValueForOnConsoleMessage(value); } + + @Override + public void setSynchronousReturnValueForOnJsAlert(@NonNull Long instanceId, @NonNull Boolean value) { + final WebChromeClientImpl webChromeClient = + Objects.requireNonNull(instanceManager.getInstance(instanceId)); + webChromeClient.setReturnValueForOnJsAlert(value); + } + + @Override + public void setSynchronousReturnValueForOnJsConfirm(@NonNull Long instanceId, @NonNull Boolean value) { + final WebChromeClientImpl webChromeClient = + Objects.requireNonNull(instanceManager.getInstance(instanceId)); + webChromeClient.setReturnValueForOnJsConfirm(value); + } + + @Override + public void setSynchronousReturnValueForOnJsPrompt(@NonNull Long instanceId, @NonNull Boolean value) { + final WebChromeClientImpl webChromeClient = + Objects.requireNonNull(instanceManager.getInstance(instanceId)); + webChromeClient.setReturnValueForOnJsPrompt(value); + } } 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 92a8fe2bd770..a6bf84372581 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -237,6 +237,9 @@ enum MenuOptions { setCookie, videoExample, logExample, + jsAlert, + jsConfirm, + jsPrompt, } class SampleMenu extends StatelessWidget { @@ -303,6 +306,15 @@ class SampleMenu extends StatelessWidget { case MenuOptions.logExample: _onLogExample(); break; + case MenuOptions.jsAlert: + _onJavaScriptAlertExample(context); + break; + case MenuOptions.jsConfirm: + _onJavaScriptConfirmExample(context); + break; + case MenuOptions.jsPrompt: + _onJavaScriptPromptExample(context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -367,6 +379,18 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.videoExample, child: Text('Video example'), ), + const PopupMenuItem( + value: MenuOptions.jsAlert, + child: Text('JavaScript Alert example'), + ), + const PopupMenuItem( + value: MenuOptions.jsConfirm, + child: Text('JavaScript Confirm example'), + ), + const PopupMenuItem( + value: MenuOptions.jsPrompt, + child: Text('JavaScript Prompt example'), + ), ], ); } @@ -515,6 +539,98 @@ class SampleMenu extends StatelessWidget { return webViewController.loadHtmlString(kTransparentBackgroundPage); } + Future _onJavaScriptAlertExample(BuildContext context) { + webViewController.setOnJavaScriptAlertDialog((String message) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: ( + BuildContext context, + ) { + return AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK')) + ], + ); + }); + }); + return webViewController + .runJavaScript("alert('This is a JavaScript alert dialog');"); + } + + Future _onJavaScriptConfirmExample(BuildContext context) { + webViewController.setOnJavaScriptConfirmDialog((String message) async { + final bool? result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('OK'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ) + ], + ); + }); + debugPrint('result=$result'); + return result ?? false; + }); + return webViewController + .runJavaScript("confirm('This is a JavaScript confirm dialog');"); + } + + Future _onJavaScriptPromptExample(BuildContext context) { + final TextEditingController textEditingController = TextEditingController(); + webViewController.setOnJavaScriptPromptDialog( + (String message, String? defaultText) async { + textEditingController.text = defaultText ?? ''; + final String? result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text(message), + content: TextField( + controller: textEditingController, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(textEditingController.text); + }, + child: const Text('OK'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + child: const Text('Cancel'), + ) + ], + ); + }); + debugPrint('result=$result'); + return result ?? ''; + }); + return webViewController + .runJavaScript("prompt('This is a JavaScript prompt dialog');"); + } + Widget _getCookieList(String cookies) { if (cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index f3d00ebd88e3..f5453f7113da 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -35,6 +35,9 @@ class AndroidWebViewProxy { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + Future Function(String message)? onJsAlert, + Future Function(String message)? onJsConfirm, + Future Function(String message, String? defaultText)? onJsPrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, 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 c0778ed07224..e1b57a9e8c7b 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 @@ -1048,6 +1048,9 @@ class WebChromeClient extends JavaObject { WebChromeClient({ this.onProgressChanged, this.onShowFileChooser, + this.onJsAlert, + this.onJsConfirm, + this.onJsPrompt, this.onPermissionRequest, this.onGeolocationPermissionsShowPrompt, this.onGeolocationPermissionsHidePrompt, @@ -1070,6 +1073,9 @@ class WebChromeClient extends JavaObject { WebChromeClient.detached({ this.onProgressChanged, this.onShowFileChooser, + this.onJsAlert, + this.onJsConfirm, + this.onJsPrompt, this.onPermissionRequest, this.onGeolocationPermissionsShowPrompt, this.onGeolocationPermissionsHidePrompt, @@ -1100,6 +1106,30 @@ class WebChromeClient extends JavaObject { FileChooserParams params, )? onShowFileChooser; + /// Indicates the client should display a javascript alert dialog. + /// + /// To handle the request for a javascript alert dialog with this callback, passing true + /// to [setSynchronousReturnValueForOnJsAlert] is required. Otherwise, + /// the client will use the default handling of a javascript alert dialog. + final Future Function(String message)? onJsAlert; + + /// Indicates the client should display a javascript confirm dialog. + /// + /// To handle the request for a javascript confirm dialog with this callback, passing true + /// to [setSynchronousReturnValueForOnJsConfirm] is required. Otherwise, + /// the returned result will be ignored and the client will use the + /// default handling of a javascript confirm dialog. + final Future Function(String message)? onJsConfirm; + + /// Indicates the client should display a javascript prompt dialog. + /// + /// To handle the request for a javascript prompt dialog with this callback, passing true + /// to [setSynchronousReturnValueForOnJsPrompt] is required. Otherwise, + /// the returned result will be ignored and the client will use the + /// default handling of a javascript prompt dialog. + final Future Function(String message, String? defaultValue)? + onJsPrompt; + /// Notify the host application that web content is requesting permission to /// access the specified resources and the permission currently isn't granted /// or denied. @@ -1189,11 +1219,100 @@ class WebChromeClient extends JavaObject { ); } + /// Sets the required synchronous return value for the Java method, + /// `WebChromeClient.onJsAlert(...)`. + /// + /// The Java method, `WebChromeClient.onJsAlert(...)`, requires + /// a boolean to be returned and this method sets the returned value for all + /// calls to the Java method. + /// + /// Setting this to true indicates that all javascript alert requests should be + /// handled by [onJsAlert]. Otherwise, the client will use the default + /// handling and the returned value in [onJsAlert] will be ignored. + /// + /// Requires [onJsAlert] to be nonnull. + /// + /// Defaults to false. + Future setSynchronousReturnValueForOnJsAlert( + bool value, + ) { + if (value && onJsAlert == null) { + throw StateError( + 'Setting this to true requires `onJsAlert` to be nonnull.', + ); + } + return api.setSynchronousReturnValueForOnJsAlertFromInstance( + this, + value, + ); + } + + /// Sets the required synchronous return value for the Java method, + /// `WebChromeClient.onJsConfirm(...)`. + /// + /// The Java method, `WebChromeClient.onJsConfirm(...)`, requires + /// a boolean to be returned and this method sets the returned value for all + /// calls to the Java method. + /// + /// Setting this to true indicates that all javascript confirm requests should be + /// handled by [onJsConfirm] and the returned result will be + /// returned to the WebView. Otherwise, the client will use the default + /// handling and the returned value in [onJsConfirm] will be ignored. + /// + /// Requires [onJsConfirm] to be nonnull. + /// + /// Defaults to false. + Future setSynchronousReturnValueForOnJsConfirm( + bool value, + ) { + if (value && onJsConfirm == null) { + throw StateError( + 'Setting this to true requires `onJsConfirm` to be nonnull.', + ); + } + return api.setSynchronousReturnValueForOnJsConfirmFromInstance( + this, + value, + ); + } + + /// Sets the required synchronous return value for the Java method, + /// `WebChromeClient.onJsPrompt(...)`. + /// + /// The Java method, `WebChromeClient.onJsPrompt(...)`, requires + /// a boolean to be returned and this method sets the returned value for all + /// calls to the Java method. + /// + /// Setting this to true indicates that all javascript prompt requests should be + /// handled by [onJsPrompt] and the returned result will be + /// returned to the WebView. Otherwise, the client will use the default + /// handling and the returned value in [onJsPrompt] will be ignored. + /// + /// Requires [onJsPrompt] to be nonnull. + /// + /// Defaults to false. + Future setSynchronousReturnValueForOnJsPrompt( + bool value, + ) { + if (value && onJsPrompt == null) { + throw StateError( + 'Setting this to true requires `onJsPrompt` to be nonnull.', + ); + } + return api.setSynchronousReturnValueForOnJsPromptFromInstance( + this, + value, + ); + } + @override WebChromeClient copy() { return WebChromeClient.detached( onProgressChanged: onProgressChanged, onShowFileChooser: onShowFileChooser, + onJsAlert: onJsAlert, + onJsConfirm: onJsConfirm, + onJsPrompt: onJsPrompt, onPermissionRequest: onPermissionRequest, onGeolocationPermissionsShowPrompt: onGeolocationPermissionsShowPrompt, onGeolocationPermissionsHidePrompt: onGeolocationPermissionsHidePrompt, diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index 8435f7302fa0..852b90e827aa 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart @@ -2070,6 +2070,78 @@ class WebChromeClientHostApi { return; } } + + Future setSynchronousReturnValueForOnJsAlert( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnJsAlert', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setSynchronousReturnValueForOnJsConfirm( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnJsConfirm', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setSynchronousReturnValueForOnJsPrompt( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientHostApi.setSynchronousReturnValueForOnJsPrompt', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_value]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } } class FlutterAssetManagerHostApi { @@ -2170,6 +2242,13 @@ abstract class WebChromeClientFlutterApi { Future> onShowFileChooser( int instanceId, int webViewInstanceId, int paramsInstanceId); + Future onJsAlert(int instanceId, String message); + + Future onJsConfirm(int instanceId, String message); + + Future onJsPrompt( + int instanceId, String message, String? defaultValue); + /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); @@ -2246,6 +2325,78 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsAlert', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsAlert was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsAlert was null, expected non-null int.'); + final String? arg_message = (args[1] as String?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsAlert was null, expected non-null String.'); + await api.onJsAlert(arg_instanceId!, arg_message!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsConfirm', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsConfirm was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsConfirm was null, expected non-null int.'); + final String? arg_message = (args[1] as String?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsConfirm was null, expected non-null String.'); + final bool output = + await api.onJsConfirm(arg_instanceId!, arg_message!); + return output; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsPrompt', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsPrompt was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsPrompt was null, expected non-null int.'); + final String? arg_message = (args[1] as String?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onJsPrompt was null, expected non-null String.'); + final String? arg_defaultValue = (args[2] as String?); + final String output = await api.onJsPrompt( + arg_instanceId!, arg_message!, arg_defaultValue); + return output; + }); + } + } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.webview_flutter_android.WebChromeClientFlutterApi.onPermissionRequest', diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index 4ecb06b05790..0a8fcab03047 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -909,6 +909,39 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { value, ); } + + /// Helper method to convert instances ids to objects. + Future setSynchronousReturnValueForOnJsAlertFromInstance( + WebChromeClient instance, + bool value, + ) { + return setSynchronousReturnValueForOnJsAlert( + instanceManager.getIdentifier(instance)!, + value, + ); + } + + /// Helper method to convert instances ids to objects. + Future setSynchronousReturnValueForOnJsConfirmFromInstance( + WebChromeClient instance, + bool value, + ) { + return setSynchronousReturnValueForOnJsConfirm( + instanceManager.getIdentifier(instance)!, + value, + ); + } + + /// Helper method to convert instances ids to objects. + Future setSynchronousReturnValueForOnJsPromptFromInstance( + WebChromeClient instance, + bool value, + ) { + return setSynchronousReturnValueForOnJsPrompt( + instanceManager.getIdentifier(instance)!, + value, + ); + } } /// Flutter api implementation for [DownloadListener]. @@ -959,6 +992,40 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { return Future>.value(const []); } + @override + Future onJsAlert(int instanceId, String message) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onJsAlert != null) { + return instance.onJsAlert!(message); + } + + return Future.value(); + } + + @override + Future onJsConfirm(int instanceId, String message) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onJsConfirm != null) { + return instance.onJsConfirm!(message); + } + + return Future.value(false); + } + + @override + Future onJsPrompt( + int instanceId, String message, String? defaultValue) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onJsPrompt != null) { + return instance.onJsPrompt!(message, defaultValue); + } + + return Future.value(''); + } + @override void onGeolocationPermissionsShowPrompt( int instanceId, int paramsInstanceId, String origin) { diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index ccfe14391e8f..7d5946101c63 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -225,6 +225,35 @@ class AndroidWebViewController extends PlatformWebViewController { }; }, ), + onJsAlert: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (String message) async { + if (weakReference.target?._onJavaScriptAlertDialogCallback != null) { + return weakReference + .target!._onJavaScriptAlertDialogCallback!(message); + } + }; + }), + onJsConfirm: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (String message) async { + if (weakReference.target?._onJavaScriptConfirmDialogCallback != null) { + return weakReference + .target!._onJavaScriptConfirmDialogCallback!(message); + } + return false; + }; + }), + onJsPrompt: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (String message, String? defaultValue) async { + if (weakReference.target?._onJavaScriptPromptDialogCallback != null) { + return weakReference.target!._onJavaScriptPromptDialogCallback!( + message, defaultValue); + } + return ''; + }; + }), onPermissionRequest: withWeakReferenceTo( this, (WeakReference weakReference) { @@ -282,6 +311,12 @@ class AndroidWebViewController extends PlatformWebViewController { Future> Function(FileSelectorParams)? _onShowFileSelectorCallback; + Future Function(String)? _onJavaScriptAlertDialogCallback; + + Future Function(String)? _onJavaScriptConfirmDialogCallback; + + Future Function(String, String?)? _onJavaScriptPromptDialogCallback; + OnGeolocationPermissionsShowPrompt? _onGeolocationPermissionsShowPrompt; OnGeolocationPermissionsHidePrompt? _onGeolocationPermissionsHidePrompt; @@ -536,6 +571,33 @@ class AndroidWebViewController extends PlatformWebViewController { ); } + /// Sets the callback that is invoked when the client should display a javascript alert dialog. + @override + Future setOnJavaScriptAlertDialog( + Future Function(String)? onJavaScriptAlertDialog) { + _onJavaScriptAlertDialogCallback = onJavaScriptAlertDialog; + return _webChromeClient + .setSynchronousReturnValueForOnJsAlert(onJavaScriptAlertDialog != null); + } + + /// Sets the callback that is invoked when the client should display a javascript confirm dialog. + @override + Future setOnJavaScriptConfirmDialog( + Future Function(String)? onJavaScriptConfirmDialog) { + _onJavaScriptConfirmDialogCallback = onJavaScriptConfirmDialog; + return _webChromeClient.setSynchronousReturnValueForOnJsConfirm( + onJavaScriptConfirmDialog != null); + } + + /// Sets the callback that is invoked when the client should display a javascript prompt dialog. + @override + Future setOnJavaScriptPromptDialog( + Future Function(String, String?)? onJavaScriptPromptDialog) { + _onJavaScriptPromptDialogCallback = onJavaScriptPromptDialog; + return _webChromeClient.setSynchronousReturnValueForOnJsPrompt( + onJavaScriptPromptDialog != null); + } + /// Sets a callback that notifies the host application that web content is /// requesting permission to access the specified resources. /// 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 6cce34382a19..ef1e026b8378 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -390,6 +390,21 @@ abstract class WebChromeClientHostApi { int instanceId, bool value, ); + + void setSynchronousReturnValueForOnJsAlert( + int instanceId, + bool value, + ); + + void setSynchronousReturnValueForOnJsConfirm( + int instanceId, + bool value, + ); + + void setSynchronousReturnValueForOnJsPrompt( + int instanceId, + bool value, + ); } @HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') @@ -410,6 +425,15 @@ abstract class WebChromeClientFlutterApi { int paramsInstanceId, ); + @async + void onJsAlert(int instanceId, String message); + + @async + bool onJsConfirm(int instanceId, String message); + + @async + String onJsPrompt(int instanceId, String message, String? defaultValue); + /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 8087d7957489..b6980f5f070e 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/packages/tree/main/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: 3.12.0 +version: 3.13.0 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 269123e081db..ca00dfc58cee 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -515,6 +515,9 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { CapturingWebChromeClient({ super.onProgressChanged, super.onShowFileChooser, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, super.onGeolocationPermissionsShowPrompt, super.onGeolocationPermissionsHidePrompt, super.onShowCustomView, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index 6a7bf30e6145..746fa10a7b17 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -59,6 +59,9 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + Future Function(String message)? onJsAlert, + Future Function(String message)? onJsConfirm, + Future Function(String message, String? defaultValue)? onJsPrompt, android_webview.GeolocationPermissionsShowPrompt? onGeolocationPermissionsShowPrompt, android_webview.GeolocationPermissionsHidePrompt? @@ -97,6 +100,11 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + Future Function(String message)? onJsAlert, + Future Function(String message)? onJsConfirm, + Future Function( + String message, String? defaultValue)? + onJsPrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, @@ -607,6 +615,10 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + Future Function(String message)? onJsAlert, + Future Function(String message)? onJsConfirm, + Future Function(String message, String? defaultValue)? + onJsPrompt, dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, @@ -675,6 +687,9 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onJsAlert, + dynamic onJsConfirm, + dynamic onJsPrompt, Future Function(String origin, android_webview.GeolocationPermissionsCallback callback)? onGeolocationPermissionsShowPrompt, @@ -748,6 +763,9 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onJsAlert, + dynamic onJsConfirm, + dynamic onJsPrompt, dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, @@ -803,6 +821,9 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onJsAlert, + dynamic onJsConfirm, + dynamic onJsPrompt, dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, void Function( @@ -858,6 +879,9 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onJsAlert, + dynamic onJsConfirm, + dynamic onJsPrompt, dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, void Function( @@ -903,6 +927,9 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onJsAlert, + dynamic onJsConfirm, + dynamic onJsPrompt, dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, @@ -1448,6 +1475,9 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onJsAlert, + dynamic onJsConfirm, + dynamic onJsPrompt, dynamic onGeolocationPermissionsShowPrompt, dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 2748ea394cc3..09bbbe1d9b15 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.7.0 + +* Adds support to intercept JavaScript dialog. + Sees `PlatformWebViewController.setOnJavaScriptAlertDialog`, + `PlatformWebViewController.setOnJavaScriptConfirmDialog`, + `PlatformWebViewController.setOnJavaScriptPromptDialog`. + ## 2.6.0 * Adds support to register a callback to intercept messages that are written to diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart index 806d2a7a6b66..bc2c48fe7884 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_webview_controller.dart @@ -286,6 +286,34 @@ abstract class PlatformWebViewController extends PlatformInterface { 'setOnConsoleMessage is not implemented on the current platform', ); } + + /// Sets a callback that notifies the host application that the web page + /// wants to display a JavaScript alert() dialog. + Future setOnJavaScriptAlertDialog( + Future Function(String message) onJavaScriptAlertDialog) async { + throw UnimplementedError( + 'setOnJavaScriptAlertDialog is not implemented on the current platform', + ); + } + + /// Sets a callback that notifies the host application that the web page + /// wants to display a JavaScript confirm() dialog. + Future setOnJavaScriptConfirmDialog( + Future Function(String message) onJavaScriptConfirmDialog) async { + throw UnimplementedError( + 'setOnJavaScriptConfirmDialog is not implemented on the current platform', + ); + } + + /// Sets a callback that notifies the host application that the web page + /// wants to display a JavaScript prompt() dialog. + Future setOnJavaScriptPromptDialog( + Future Function(String message, String? defaultText) + onJavaScriptPromptDialog) async { + throw UnimplementedError( + 'setOnJavaScriptPromptDialog is not implemented on the current platform', + ); + } } /// Describes the parameters necessary for registering a JavaScript channel. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 71e85f32ded2..bce2d4ac6580 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/webview_flutt 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: 2.6.0 +version: 2.7.0 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart index ba83cc119922..8bef66d4150e 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_webview_controller_test.dart @@ -414,6 +414,50 @@ void main() { throwsUnimplementedError, ); }); + + test( + 'Default implementation of setOnJavaScriptAlertDialog should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.setOnJavaScriptAlertDialog((_) async {}), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setOnJavaScriptConfirmDialog should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.setOnJavaScriptConfirmDialog((_) async { + return false; + }), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setOnJavaScriptPromptDialog should throw unimplemented error', + () { + final PlatformWebViewController controller = + ExtendsPlatformWebViewController( + const PlatformWebViewControllerCreationParams()); + + expect( + () => controller.setOnJavaScriptPromptDialog( + (String message, String? defaultText) async { + return ''; + }), + throwsUnimplementedError, + ); + }); } class MockWebViewPlatformWithMixin extends MockWebViewPlatform diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 4de08406bba1..70b6765826a5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.10.0 + +* Adds support to intercept JavaScript dialog. + Sees `PlatformWebViewController.setOnJavaScriptAlertDialog`, + `PlatformWebViewController.setOnJavaScriptConfirmDialog`, + `PlatformWebViewController.setOnJavaScriptPromptDialog`. + ## 3.9.1 * Fixes bug where `WebkitWebViewController.getUserAgent` was incorrectly returning an empty String. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index 4180650574db..192bc7e5bbc8 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -237,6 +237,9 @@ enum MenuOptions { transparentBackground, setCookie, logExample, + jsAlert, + jsConfirm, + jsPrompt, } class SampleMenu extends StatelessWidget { @@ -300,6 +303,15 @@ class SampleMenu extends StatelessWidget { case MenuOptions.logExample: _onLogExample(); break; + case MenuOptions.jsAlert: + _onJavaScriptAlertExample(context); + break; + case MenuOptions.jsConfirm: + _onJavaScriptConfirmExample(context); + break; + case MenuOptions.jsPrompt: + _onJavaScriptPromptExample(context); + break; } }, itemBuilder: (BuildContext context) => >[ @@ -360,6 +372,18 @@ class SampleMenu extends StatelessWidget { value: MenuOptions.logExample, child: Text('Log example'), ), + const PopupMenuItem( + value: MenuOptions.jsAlert, + child: Text('JavaScript Alert example'), + ), + const PopupMenuItem( + value: MenuOptions.jsConfirm, + child: Text('JavaScript Confirm example'), + ), + const PopupMenuItem( + value: MenuOptions.jsPrompt, + child: Text('JavaScript Prompt example'), + ), ], ); } @@ -484,6 +508,98 @@ class SampleMenu extends StatelessWidget { return webViewController.loadHtmlString(kTransparentBackgroundPage); } + Future _onJavaScriptAlertExample(BuildContext context) { + webViewController.setOnJavaScriptAlertDialog((String message) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: ( + BuildContext context, + ) { + return AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('OK')) + ], + ); + }); + }); + return webViewController + .runJavaScript("alert('This is a JavaScript alert dialog');"); + } + + Future _onJavaScriptConfirmExample(BuildContext context) { + webViewController.setOnJavaScriptConfirmDialog((String message) async { + final bool? result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('OK'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ) + ], + ); + }); + debugPrint('result=$result'); + return result ?? false; + }); + return webViewController + .runJavaScript("confirm('This is a JavaScript confirm dialog');"); + } + + Future _onJavaScriptPromptExample(BuildContext context) { + final TextEditingController textEditingController = TextEditingController(); + webViewController.setOnJavaScriptPromptDialog( + (String message, String? defaultText) async { + textEditingController.text = defaultText ?? ''; + final String? result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return AlertDialog( + title: Text(message), + content: TextField( + controller: textEditingController, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(textEditingController.text); + }, + child: const Text('OK'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(''); + }, + child: const Text('Cancel'), + ) + ], + ); + }); + debugPrint('result=$result'); + return result ?? ''; + }); + return webViewController + .runJavaScript("prompt('This is a JavaScript prompt dialog');"); + } + Widget _getCookieList(String cookies) { if (cookies == '""') { return Container(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h index 11c49d24b541..a8ba6b5e2068 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -798,6 +798,24 @@ NSObject *FWFWKUIDelegateFlutterApiGetCodec(void); (void (^)( FWFWKPermissionDecisionData *_Nullable, FlutterError *_Nullable))completion; +/// Callback to Dart function `WKUIDelegate.runJavaScriptAlertPanelWithMessage`. +- (void)runJavaScriptAlertPanelForDelegateWithIdentifier:(NSNumber *)identifier + message:(NSString *)message + completion:(void (^)( + FlutterError *_Nullable))completion; +/// Callback to Dart function `WKUIDelegate.runJavaScriptConfirmPanelWithMessage`. +- (void)runJavaScriptConfirmPanelForDelegateWithIdentifier:(NSNumber *)identifier + message:(NSString *)message + completion:(void (^)( + NSNumber *_Nullable, + FlutterError *_Nullable))completion; +/// Callback to Dart function `WKUIDelegate.runJavaScriptTextInputPanelWithPrompt`. +- (void)runJavaScriptTextInputPanelForDelegateWithIdentifier:(NSNumber *)identifier + prompt:(NSString *)prompt + defaultText:(nullable NSString *)defaultText + completion:(void (^)( + NSString *_Nullable, + FlutterError *_Nullable))completion; @end /// The codec used by FWFWKHttpCookieStoreHostApi. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m index cc58067418e9..5caed33162bc 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -2720,6 +2720,50 @@ - (void)requestMediaCapturePermissionForDelegateWithIdentifier:(NSNumber *)arg_i completion(output, nil); }]; } +- (void)runJavaScriptAlertPanelForDelegateWithIdentifier:(NSNumber *)arg_identifier + message:(NSString *)arg_message + completion:(void (^)( + FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptAlertPanel" + binaryMessenger:self.binaryMessenger + codec:FWFWKUIDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[arg_identifier ?: [NSNull null], arg_message ?: [NSNull null]] reply:^(id reply) { + completion(nil); + }]; +} +- (void)runJavaScriptConfirmPanelForDelegateWithIdentifier:(NSNumber *)arg_identifier + message:(NSString *)arg_message + completion:(void (^)( + NSNumber *_Nullable, + FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptConfirmPanel" + binaryMessenger:self.binaryMessenger + codec:FWFWKUIDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[arg_identifier ?: [NSNull null], arg_message ?: [NSNull null]] reply:^(id reply) { + NSNumber *output = reply; + completion(output, nil); + }]; +} +- (void)runJavaScriptTextInputPanelForDelegateWithIdentifier:(NSNumber *)arg_identifier + prompt:(NSString *)arg_prompt + defaultText:(nullable NSString *)arg_defaultText + completion:(void (^)( + NSString *_Nullable, + FlutterError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel + messageChannelWithName:@"dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptTextInputPanel" + binaryMessenger:self.binaryMessenger + codec:FWFWKUIDelegateFlutterApiGetCodec()]; + [channel sendMessage:@[arg_identifier ?: [NSNull null], arg_prompt ?: [NSNull null], arg_defaultText ?: [NSNull null]] reply:^(id reply) { + NSString *output = reply; + completion(output, nil); + }]; +} @end @interface FWFWKHttpCookieStoreHostApiCodecReader : FlutterStandardReader diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m index a7010509ef22..60deb71d3508 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFUIDelegateHostApi.m @@ -90,6 +90,41 @@ - (void)requestMediaCapturePermissionForDelegateWithIdentifier:(FWFUIDelegate *) decision)); }]; } + +- (void)runJavaScriptAlertPanelForDelegateWithIdentifier:(FWFUIDelegate *)instance + message:(NSString *)message + completionHandler:(void (^)(void))completionHandler { + [self runJavaScriptAlertPanelForDelegateWithIdentifier:@([self identifierForDelegate:instance]) + message:message + completion:^(FlutterError *_Nullable error) { + NSAssert(!error, @"%@", error); + completionHandler(); + }]; +} + +- (void)runJavaScriptConfirmPanelForDelegateWithIdentifier:(FWFUIDelegate *)instance + message:(NSString *)message + completionHandler:(void (^)(BOOL))completionHandler { + [self runJavaScriptConfirmPanelForDelegateWithIdentifier:@([self identifierForDelegate:instance]) + message:message + completion:^(NSNumber *_Nullable result, FlutterError *_Nullable error) { + NSAssert(!error, @"%@", error); + completionHandler(result.boolValue); + }]; +} + +- (void)runJavaScriptTextInputPanelForDelegateWithIdentifier:(FWFUIDelegate *)instance + prompt:(NSString *)prompt + defaultText:(NSString *)defaultText + completionHandler:(void (^)(NSString * _Nullable))completionHandler { + [self runJavaScriptTextInputPanelForDelegateWithIdentifier:@([self identifierForDelegate:instance]) + prompt:prompt + defaultText:defaultText + completion:^(NSString *_Nullable result, FlutterError *_Nullable error) { + NSAssert(!error, @"%@", error); + completionHandler(result); + }]; +} @end @implementation FWFUIDelegate @@ -133,6 +168,44 @@ - (void)webView:(WKWebView *)webView decisionHandler(decision); }]; } + +- (void)webView :(WKWebView *)webView + runJavaScriptAlertPanelWithMessage:(NSString *)message + initiatedByFrame:(WKFrameInfo *)frame + completionHandler:(void (^)(void))completionHandler { + [self.UIDelegateAPI + runJavaScriptAlertPanelForDelegateWithIdentifier:self + message:message + completionHandler:^{ + completionHandler(); + }]; +} + +- (void)webView:(WKWebView *)webView + runJavaScriptConfirmPanelWithMessage:(NSString *)message + initiatedByFrame:(WKFrameInfo *)frame + completionHandler:(void (^)(BOOL))completionHandler { + [self.UIDelegateAPI + runJavaScriptConfirmPanelForDelegateWithIdentifier:self + message:message + completionHandler:^(BOOL result) { + completionHandler(result); + }]; +} + +- (void)webView:(WKWebView *)webView + runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt + defaultText:(nullable NSString *)defaultText + initiatedByFrame:(WKFrameInfo *)frame + completionHandler:(void (^)(NSString *_Nullable))completionHandler { + [self.UIDelegateAPI + runJavaScriptTextInputPanelForDelegateWithIdentifier:self + prompt:prompt + defaultText:defaultText + completionHandler:^(NSString *_Nullable result) { + completionHandler(result); + }]; +} @end @interface FWFUIDelegateHostApiImpl () diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index b0eaadeb739f..6847b7331313 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -2711,6 +2711,16 @@ abstract class WKUIDelegateFlutterApi { WKFrameInfoData frame, WKMediaCaptureTypeData type); + /// Callback to Dart function `WKUIDelegate.runJavaScriptAlertPanelWithMessage`. + Future runJavaScriptAlertPanel(int identifier, String message); + + /// Callback to Dart function `WKUIDelegate.runJavaScriptConfirmPanelWithMessage`. + Future runJavaScriptConfirmPanel(int identifier, String message); + + /// Callback to Dart function `WKUIDelegate.runJavaScriptTextInputPanelWithPrompt`. + Future runJavaScriptTextInputPanel( + int identifier, String prompt, String? defaultText); + static void setup(WKUIDelegateFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2780,6 +2790,78 @@ abstract class WKUIDelegateFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptAlertPanel', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptAlertPanel was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptAlertPanel was null, expected non-null int.'); + final String? arg_message = (args[1] as String?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptAlertPanel was null, expected non-null String.'); + await api.runJavaScriptAlertPanel(arg_identifier!, arg_message!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptConfirmPanel', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptConfirmPanel was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptConfirmPanel was null, expected non-null int.'); + final String? arg_message = (args[1] as String?); + assert(arg_message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptConfirmPanel was null, expected non-null String.'); + final bool output = await api.runJavaScriptConfirmPanel( + arg_identifier!, arg_message!); + return output; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptTextInputPanel', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptTextInputPanel was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptTextInputPanel was null, expected non-null int.'); + final String? arg_prompt = (args[1] as String?); + assert(arg_prompt != null, + 'Argument for dev.flutter.pigeon.webview_flutter_wkwebview.WKUIDelegateFlutterApi.runJavaScriptTextInputPanel was null, expected non-null String.'); + final String? arg_defaultText = (args[2] as String?); + final String output = await api.runJavaScriptTextInputPanel( + arg_identifier!, arg_prompt!, arg_defaultText); + return output; + }); + } + } } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index db9f41c28519..4ff2926f1f41 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -728,6 +728,9 @@ class WKUIDelegate extends NSObject { WKUIDelegate({ this.onCreateWebView, this.requestMediaCapturePermission, + this.runJavaScriptAlertPanel, + this.runJavaScriptConfirmPanel, + this.runJavaScriptTextInputPanel, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -749,6 +752,9 @@ class WKUIDelegate extends NSObject { WKUIDelegate.detached({ this.onCreateWebView, this.requestMediaCapturePermission, + this.runJavaScriptAlertPanel, + this.runJavaScriptConfirmPanel, + this.runJavaScriptTextInputPanel, super.observeValue, super.binaryMessenger, super.instanceManager, @@ -780,6 +786,25 @@ class WKUIDelegate extends NSObject { WKMediaCaptureType type, )? requestMediaCapturePermission; + /// Indicates the client should display a javascript alert dialog. + /// + /// If you do not set this callback, + /// the web view will behave as if the user selected the OK button. + final Future Function(String message)? runJavaScriptAlertPanel; + + /// Indicates the client should display a javascript confirm dialog. + /// + /// If you do not set this callback, + /// the web view will behave as if the user selected the Cancel button. + final Future Function(String message)? runJavaScriptConfirmPanel; + + /// Indicates the client should display a javascript prompt dialog. + /// + /// If you do not set this callback, + /// the web view will behave as if the user selected the Cancel button. + final Future Function(String prompt, String? defaultText)? + runJavaScriptTextInputPanel; + @override WKUIDelegate copy() { return WKUIDelegate.detached( diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart index ee545d45b718..231d6a217d00 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -767,6 +767,37 @@ class WKUIDelegateFlutterApiImpl extends WKUIDelegateFlutterApi { return WKPermissionDecisionData(value: decision); } + + @override + Future runJavaScriptAlertPanel(int identifier, String message) { + final WKUIDelegate instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance.runJavaScriptAlertPanel != null) { + return instance.runJavaScriptAlertPanel!(message); + } + return Future.value(); + } + + @override + Future runJavaScriptConfirmPanel(int identifier, String message) { + final WKUIDelegate instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance.runJavaScriptConfirmPanel != null) { + return instance.runJavaScriptConfirmPanel!(message); + } + return Future.value(false); + } + + @override + Future runJavaScriptTextInputPanel( + int identifier, String prompt, String? defaultText) { + final WKUIDelegate instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance.runJavaScriptTextInputPanel != null) { + return instance.runJavaScriptTextInputPanel!(prompt, defaultText); + } + return Future.value(''); + } } /// Host api implementation for [WKNavigationDelegate]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart index 68ce91320219..327fee1cecfd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart @@ -87,6 +87,10 @@ class WebKitProxy { WKFrameInfo frame, WKMediaCaptureType type, )? requestMediaCapturePermission, + Future Function(String message)? runJavaScriptAlertPanel, + Future Function(String message)? runJavaScriptConfirmPanel, + Future Function(String prompt, String? defaultText)? + runJavaScriptTextInputPanel, InstanceManager? instanceManager, }) createUIDelegate; } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index a02ed61069e7..beb999a022d4 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -217,6 +217,29 @@ class WebKitWebViewController extends PlatformWebViewController { return decisionCompleter.future; } }, + runJavaScriptAlertPanel: (String message) async { + final Future Function(String)? callback = + weakThis.target?._onJavaScriptAlertDialogCallback; + if (callback != null) { + return callback.call(message); + } + }, + runJavaScriptConfirmPanel: (String message) async { + final Future Function(String)? callback = + weakThis.target?._onJavaScriptConfirmDialogCallback; + if (callback != null) { + return callback.call(message); + } + return false; + }, + runJavaScriptTextInputPanel: (String prompt, String? defaultText) async { + final Future Function(String, String?)? callback = + weakThis.target?._onJavaScriptPromptDialogCallback; + if (callback != null) { + return callback.call(prompt, defaultText); + } + return ''; + }, ); _webView.setUIDelegate(_uiDelegate); @@ -272,6 +295,9 @@ class WebKitWebViewController extends PlatformWebViewController { void Function(JavaScriptConsoleMessage)? _onConsoleMessageCallback; void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; + Future Function(String)? _onJavaScriptAlertDialogCallback; + Future Function(String)? _onJavaScriptConfirmDialogCallback; + Future Function(String, String?)? _onJavaScriptPromptDialogCallback; WebKitWebViewControllerCreationParams get _webKitParams => params as WebKitWebViewControllerCreationParams; @@ -651,6 +677,28 @@ window.addEventListener("error", function(e) { _onPermissionRequestCallback = onPermissionRequest; } + /// Sets the callback that is invoked when the client should display a javascript alert dialog. + @override + Future setOnJavaScriptAlertDialog( + Future Function(String message) onJavaScriptAlertDialog) async { + _onJavaScriptAlertDialogCallback = onJavaScriptAlertDialog; + } + + /// Sets the callback that is invoked when the client should display a javascript confirm dialog. + @override + Future setOnJavaScriptConfirmDialog( + Future Function(String message) onJavaScriptConfirmDialog) async { + _onJavaScriptConfirmDialogCallback = onJavaScriptConfirmDialog; + } + + /// Sets the callback that is invoked when the client should display a javascript prompt dialog. + @override + Future setOnJavaScriptPromptDialog( + Future Function(String message, String? defaultText) + onJavaScriptPromptDialog) async { + _onJavaScriptPromptDialogCallback = onJavaScriptPromptDialog; + } + /// Whether to enable tools for debugging the current WKWebView content. /// /// It needs to be activated in each WKWebView where you want to enable it. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index 8e9e16ff425d..1428121331c7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -738,6 +738,37 @@ abstract class WKUIDelegateFlutterApi { WKFrameInfoData frame, WKMediaCaptureTypeData type, ); + + /// Callback to Dart function `WKUIDelegate.runJavaScriptAlertPanelWithMessage`. + @ObjCSelector( + 'runJavaScriptAlertPanelForDelegateWithIdentifier:message:', + ) + @async + void runJavaScriptAlertPanel( + int identifier, + String message, + ); + + /// Callback to Dart function `WKUIDelegate.runJavaScriptConfirmPanelWithMessage`. + @ObjCSelector( + 'runJavaScriptConfirmPanelForDelegateWithIdentifier:message:', + ) + @async + bool runJavaScriptConfirmPanel( + int identifier, + String message, + ); + + /// Callback to Dart function `WKUIDelegate.runJavaScriptTextInputPanelWithPrompt`. + @ObjCSelector( + 'runJavaScriptTextInputPanelForDelegateWithIdentifier:prompt:defaultText:', + ) + @async + String runJavaScriptTextInputPanel( + int identifier, + String prompt, + String? defaultText, + ); } /// Mirror of WKHttpCookieStore. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 63ed6e4db5f0..3c557781702d 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.9.1 +version: 3.10.0 environment: sdk: ">=2.19.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart index 4581c92b788f..57c304ba4936 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart @@ -238,6 +238,9 @@ class CapturingUIDelegate extends WKUIDelegate { CapturingUIDelegate({ super.onCreateWebView, super.requestMediaCapturePermission, + super.runJavaScriptAlertPanel, + super.runJavaScriptConfirmPanel, + super.runJavaScriptTextInputPanel, super.instanceManager, }) : super.detached() { lastCreatedDelegate = this; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index d1854df4bce0..9be872712dee 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -91,6 +91,10 @@ void main() { WKFrameInfo frame, WKMediaCaptureType type, )? requestMediaCapturePermission, + Future Function(String message)? runJavaScriptAlertPanel, + Future Function(String message)? runJavaScriptConfirmPanel, + Future Function(String prompt, String? defaultText)? + runJavaScriptTextInputPanel, InstanceManager? instanceManager, }) { return uiDelegate ?? @@ -1360,6 +1364,9 @@ class CapturingUIDelegate extends WKUIDelegate { CapturingUIDelegate({ super.onCreateWebView, super.requestMediaCapturePermission, + super.runJavaScriptAlertPanel, + super.runJavaScriptConfirmPanel, + super.runJavaScriptTextInputPanel, super.instanceManager, }) : super.detached() { lastCreatedDelegate = this; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart index f7bd373b35bb..5fd0da716c35 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart @@ -195,6 +195,9 @@ WebKitWebViewController createTestWebViewController( }, createUIDelegate: ({ dynamic onCreateWebView, dynamic requestMediaCapturePermission, + dynamic runJavaScriptAlertPanel, + dynamic runJavaScriptConfirmPanel, + dynamic runJavaScriptTextInputPanel, InstanceManager? instanceManager, }) { final MockWKUIDelegate mockWKUIDelegate = MockWKUIDelegate();