[webview_flutter] Adds app facing implementation to override console log (#4705)

Adds the app facing implementation for registering a JavaScript console callback. This will allow developers to receive JavaScript console messages in a Dart callback.

This PR contains the `webview_flutter` specific changes from PR #4541.

Fixes flutter/flutter#32908

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md
index 8d8a839..ec02159 100644
--- a/packages/webview_flutter/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.4.0
+
+* Adds support to register a callback to receive JavaScript console messages. See `WebViewController.setConsoleLogCallback`.
+
 ## 4.3.0
 
 * Adds support to retrieve the user agent. See `WebViewController.getUserAgent`.
diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart
index c7fbd63..96cbbf8 100644
--- a/packages/webview_flutter/webview_flutter/example/lib/main.dart
+++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart
@@ -76,6 +76,40 @@
   </html>
 ''';
 
+const String kLogExamplePage = '''
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>Load file or HTML string example</title>
+</head>
+<body onload="console.log('Logging that the page is loading.')">
+
+<h1>Local demo page</h1>
+<p>
+  This page is used to test the forwarding of console logs to Dart.
+</p>
+
+<style>
+    .btn-group button {
+      padding: 24px; 24px;
+      display: block;
+      width: 25%;
+      margin: 5px 0px 0px 0px;
+    }
+</style>
+
+<div class="btn-group">
+    <button onclick="console.error('This is an error message.')">Error</button>
+    <button onclick="console.warn('This is a warning message.')">Warning</button>
+    <button onclick="console.info('This is a info message.')">Info</button>
+    <button onclick="console.debug('This is a debug message.')">Debug</button>
+    <button onclick="console.log('This is a log message.')">Log</button>
+</div>
+
+</body>
+</html>
+''';
+
 class WebViewExample extends StatefulWidget {
   const WebViewExample({super.key});
 
@@ -208,6 +242,7 @@
   loadHtmlString,
   transparentBackground,
   setCookie,
+  logExample,
 }
 
 class SampleMenu extends StatelessWidget {
@@ -264,6 +299,9 @@
           case MenuOptions.setCookie:
             _onSetCookie();
             break;
+          case MenuOptions.logExample:
+            _onLogExample();
+            break;
         }
       },
       itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
@@ -320,6 +358,10 @@
           value: MenuOptions.setCookie,
           child: Text('Set cookie'),
         ),
+        const PopupMenuItem<MenuOptions>(
+          value: MenuOptions.logExample,
+          child: Text('Log example'),
+        ),
       ],
     );
   }
@@ -463,6 +505,16 @@
 
     return indexFile.path;
   }
+
+  Future<void> _onLogExample() {
+    webViewController
+        .setOnConsoleMessage((JavaScriptConsoleMessage consoleMessage) {
+      debugPrint(
+          '== JS == ${consoleMessage.level.name}: ${consoleMessage.message}');
+    });
+
+    return webViewController.loadHtmlString(kLogExamplePage);
+  }
 }
 
 class NavigationControls extends StatelessWidget {
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 b0333cb..0b81978 100644
--- a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart
+++ b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart
@@ -354,6 +354,22 @@
     return platform.setUserAgent(userAgent);
   }
 
+  /// Sets a callback that notifies the host application on any log messages
+  /// written to the JavaScript console.
+  ///
+  /// Platforms may not preserve all the log level information so clients should
+  /// not rely on a 1:1 mapping between the JavaScript calls.
+  ///
+  /// On iOS setting this callback will inject a custom [WKUserScript] which
+  /// overrides the default implementation of `console.debug`, `console.error`,
+  /// `console.info`, `console.log` and `console.warning` methods. The iOS
+  /// WebKit framework unfortunately doesn't provide a built-in method to
+  /// forward console messages.
+  Future<void> setOnConsoleMessage(
+      void Function(JavaScriptConsoleMessage message) onConsoleMessage) {
+    return platform.setOnConsoleMessage(onConsoleMessage);
+  }
+
   /// Gets the value used for the HTTP `User-Agent:` request header.
   Future<String?> getUserAgent() {
     return platform.getUserAgent();
diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart
index 3e85cc3..37dde9d 100644
--- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart
+++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart
@@ -6,6 +6,7 @@
 
 export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'
     show
+        JavaScriptConsoleMessage,
         JavaScriptMessage,
         JavaScriptMode,
         LoadRequestMethod,
diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml
index b5349c4..8722790 100644
--- a/packages/webview_flutter/webview_flutter/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter/pubspec.yaml
@@ -2,7 +2,7 @@
 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.3.0
+version: 4.4.0
 
 environment:
   sdk: ">=2.19.0 <4.0.0"
@@ -19,9 +19,9 @@
 dependencies:
   flutter:
     sdk: flutter
-  webview_flutter_android: ^3.0.0
+  webview_flutter_android: ^3.12.0
   webview_flutter_platform_interface: ^2.6.0
-  webview_flutter_wkwebview: ^3.0.0
+  webview_flutter_wkwebview: ^3.9.0
 
 dev_dependencies:
   build_runner: ^2.1.5
diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart
index ba184c5..695a781 100644
--- a/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart
+++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.dart
@@ -388,6 +388,21 @@
     expect(permissionRequestCallbackCalled, isTrue);
   });
 
+  test('setConsoleLogCallback', () async {
+    final MockPlatformWebViewController mockPlatformWebViewController =
+        MockPlatformWebViewController();
+
+    final WebViewController webViewController = WebViewController.fromPlatform(
+      mockPlatformWebViewController,
+    );
+
+    void onConsoleMessage(JavaScriptConsoleMessage message) {}
+
+    await webViewController.setOnConsoleMessage(onConsoleMessage);
+
+    verify(mockPlatformWebViewController.setOnConsoleMessage(onConsoleMessage));
+  });
+
   test('getUserAgent', () async {
     final MockPlatformWebViewController mockPlatformWebViewController =
         MockPlatformWebViewController();
@@ -401,7 +416,6 @@
     final WebViewController webViewController = WebViewController.fromPlatform(
       mockPlatformWebViewController,
     );
-
     await expectLater(webViewController.getUserAgent(), completion(userAgent));
   });
 }