[webview_flutter] Deprecate evaluateJavascript in favour of runJavaScript and runJavaScriptForResult. (#4403)

diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md
index f599889..b499cba 100644
--- a/packages/webview_flutter/webview_flutter/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 2.2.0
+
+* Added `runJavascript` and `runJavascriptForResult` to supersede `evaluateJavascript`.
+* Deprecated `evaluateJavascript`.
+
 ## 2.1.2
 
 * Fix typos in the README.
diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart
index 3379baf..9279f31 100644
--- a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart
+++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart
@@ -70,6 +70,28 @@
     expect(currentUrl, secondaryUrl);
   }, skip: _skipDueToIssue86757);
 
+  testWidgets('evaluateJavascript', (WidgetTester tester) async {
+    final Completer<WebViewController> controllerCompleter =
+        Completer<WebViewController>();
+    await tester.pumpWidget(
+      Directionality(
+        textDirection: TextDirection.ltr,
+        child: WebView(
+          key: GlobalKey(),
+          initialUrl: primaryUrl,
+          onWebViewCreated: (WebViewController controller) {
+            controllerCompleter.complete(controller);
+          },
+          javascriptMode: JavascriptMode.unrestricted,
+        ),
+      ),
+    );
+    final WebViewController controller = await controllerCompleter.future;
+    // ignore: deprecated_member_use
+    final String result = await controller.evaluateJavascript('1 + 1');
+    expect(result, equals('2'));
+  });
+
   // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
   testWidgets('loadUrl with headers', (WidgetTester tester) async {
     final Completer<WebViewController> controllerCompleter =
@@ -108,12 +130,12 @@
     await pageLoads.stream.firstWhere((String url) => url == currentUrl);
 
     final String content = await controller
-        .evaluateJavascript('document.documentElement.innerText');
+        .runJavascriptReturningResult('document.documentElement.innerText');
     expect(content.contains('flutter_test_header'), isTrue);
   }, skip: Platform.isAndroid && _skipDueToIssue86757);
 
   // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757.
-  testWidgets('JavaScriptChannel', (WidgetTester tester) async {
+  testWidgets('JavascriptChannel', (WidgetTester tester) async {
     final Completer<WebViewController> controllerCompleter =
         Completer<WebViewController>();
     final Completer<void> pageStarted = Completer<void>();
@@ -153,11 +175,7 @@
     await pageLoaded.future;
 
     expect(messagesReceived, isEmpty);
-    // Append a return value "1" in the end will prevent an iOS platform exception.
-    // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380
-    // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed.
-    // https://github.com/flutter/flutter/issues/66318
-    await controller.evaluateJavascript('Echo.postMessage("hello");1;');
+    await controller.runJavascript('Echo.postMessage("hello");');
     expect(messagesReceived, equals(<String>['hello']));
   }, skip: Platform.isAndroid && _skipDueToIssue86757);
 
@@ -404,7 +422,8 @@
       WebViewController controller = await controllerCompleter.future;
       await pageLoaded.future;
 
-      String isPaused = await controller.evaluateJavascript('isPaused();');
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(false));
 
       controllerCompleter = Completer<WebViewController>();
@@ -433,7 +452,7 @@
       controller = await controllerCompleter.future;
       await pageLoaded.future;
 
-      isPaused = await controller.evaluateJavascript('isPaused();');
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(true));
     });
 
@@ -464,7 +483,8 @@
       final WebViewController controller = await controllerCompleter.future;
       await pageLoaded.future;
 
-      String isPaused = await controller.evaluateJavascript('isPaused();');
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(false));
 
       pageLoaded = Completer<void>();
@@ -492,7 +512,7 @@
 
       await pageLoaded.future;
 
-      isPaused = await controller.evaluateJavascript('isPaused();');
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(false));
     });
 
@@ -542,7 +562,7 @@
       await videoPlaying.future;
 
       String fullScreen =
-          await controller.evaluateJavascript('isFullScreen();');
+          await controller.runJavascriptReturningResult('isFullScreen();');
       expect(fullScreen, _webviewBool(false));
     });
 
@@ -594,7 +614,7 @@
       await videoPlaying.future;
 
       String fullScreen =
-          await controller.evaluateJavascript('isFullScreen();');
+          await controller.runJavascriptReturningResult('isFullScreen();');
       expect(fullScreen, _webviewBool(true));
     }, skip: Platform.isAndroid);
   });
@@ -660,7 +680,8 @@
       await pageStarted.future;
       await pageLoaded.future;
 
-      String isPaused = await controller.evaluateJavascript('isPaused();');
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(false));
 
       controllerCompleter = Completer<WebViewController>();
@@ -694,7 +715,7 @@
       await pageStarted.future;
       await pageLoaded.future;
 
-      isPaused = await controller.evaluateJavascript('isPaused();');
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(true));
     });
 
@@ -730,7 +751,8 @@
       await pageStarted.future;
       await pageLoaded.future;
 
-      String isPaused = await controller.evaluateJavascript('isPaused();');
+      String isPaused =
+          await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(false));
 
       pageStarted = Completer<void>();
@@ -763,7 +785,7 @@
       await pageStarted.future;
       await pageLoaded.future;
 
-      isPaused = await controller.evaluateJavascript('isPaused();');
+      isPaused = await controller.runJavascriptReturningResult('isPaused();');
       expect(isPaused, _webviewBool(false));
     });
   });
@@ -1028,15 +1050,16 @@
 
       final WebViewController controller = await controllerCompleter.future;
       await pageLoaded.future;
-      final String viewportRectJSON = await _evaluateJavascript(
+      final String viewportRectJSON = await _runJavascriptReturningResult(
           controller, 'JSON.stringify(viewport.getBoundingClientRect())');
       final Map<String, dynamic> viewportRectRelativeToViewport =
           jsonDecode(viewportRectJSON);
 
       // Check that the input is originally outside of the viewport.
 
-      final String initialInputClientRectJSON = await _evaluateJavascript(
-          controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
+      final String initialInputClientRectJSON =
+          await _runJavascriptReturningResult(
+              controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
       final Map<String, dynamic> initialInputClientRectRelativeToViewport =
           jsonDecode(initialInputClientRectJSON);
 
@@ -1045,12 +1068,13 @@
               viewportRectRelativeToViewport['bottom'],
           isFalse);
 
-      await controller.evaluateJavascript('inputEl.focus()');
+      await controller.runJavascript('inputEl.focus()');
 
       // Check that focusing the input brought it into view.
 
-      final String lastInputClientRectJSON = await _evaluateJavascript(
-          controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
+      final String lastInputClientRectJSON =
+          await _runJavascriptReturningResult(
+              controller, 'JSON.stringify(inputEl.getBoundingClientRect())');
       final Map<String, dynamic> lastInputClientRectRelativeToViewport =
           jsonDecode(lastInputClientRectJSON);
 
@@ -1106,7 +1130,7 @@
 
       await pageLoads.stream.first; // Wait for initial page load.
       final WebViewController controller = await controllerCompleter.future;
-      await controller.evaluateJavascript('location.href = "$secondaryUrl"');
+      await controller.runJavascript('location.href = "$secondaryUrl"');
 
       await pageLoads.stream.first; // Wait for the next page load.
       final String? currentUrl = await controller.currentUrl();
@@ -1237,7 +1261,7 @@
       await pageLoads.stream.first; // Wait for initial page load.
       final WebViewController controller = await controllerCompleter.future;
       await controller
-          .evaluateJavascript('location.href = "https://www.youtube.com/"');
+          .runJavascript('location.href = "https://www.youtube.com/"');
 
       // There should never be any second page load, since our new URL is
       // blocked. Still wait for a potential page change for some time in order
@@ -1277,7 +1301,7 @@
 
       await pageLoads.stream.first; // Wait for initial page load.
       final WebViewController controller = await controllerCompleter.future;
-      await controller.evaluateJavascript('location.href = "$secondaryUrl"');
+      await controller.runJavascript('location.href = "$secondaryUrl"');
 
       await pageLoads.stream.first; // Wait for second page to load.
       final String? currentUrl = await controller.currentUrl();
@@ -1332,7 +1356,7 @@
       ),
     );
     final WebViewController controller = await controllerCompleter.future;
-    await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")');
+    await controller.runJavascript('window.open("$primaryUrl", "_blank")');
     await pageLoaded.future;
     final String? currentUrl = await controller.currentUrl();
     expect(currentUrl, primaryUrl);
@@ -1368,7 +1392,7 @@
       await pageLoaded.future;
       pageLoaded = Completer<void>();
 
-      await controller.evaluateJavascript('window.open("$secondaryUrl")');
+      await controller.runJavascript('window.open("$secondaryUrl")');
       await pageLoaded.future;
       pageLoaded = Completer<void>();
       expect(controller.currentUrl(), completion(secondaryUrl));
@@ -1382,7 +1406,7 @@
   );
 
   testWidgets(
-    'javascript does not run in parent window',
+    'JavaScript does not run in parent window',
     (WidgetTester tester) async {
       final String iframe = '''
         <!DOCTYPE html>
@@ -1439,9 +1463,10 @@
       final WebViewController controller = await controllerCompleter.future;
       await pageLoadCompleter.future;
 
-      expect(controller.evaluateJavascript('iframeLoaded'), completion('true'));
+      expect(controller.runJavascriptReturningResult('iframeLoaded'),
+          completion('true'));
       expect(
-        controller.evaluateJavascript(
+        controller.runJavascriptReturningResult(
             'document.querySelector("p") && document.querySelector("p").textContent'),
         completion('null'),
       );
@@ -1461,13 +1486,13 @@
 
 /// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests.
 Future<String> _getUserAgent(WebViewController controller) async {
-  return _evaluateJavascript(controller, 'navigator.userAgent;');
+  return _runJavascriptReturningResult(controller, 'navigator.userAgent;');
 }
 
-Future<String> _evaluateJavascript(
+Future<String> _runJavascriptReturningResult(
     WebViewController controller, String js) async {
   if (defaultTargetPlatform == TargetPlatform.iOS) {
-    return await controller.evaluateJavascript(js);
+    return await controller.runJavascriptReturningResult(js);
   }
-  return jsonDecode(await controller.evaluateJavascript(js));
+  return jsonDecode(await controller.runJavascriptReturningResult(js));
 }
diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart
index c456a96..2fd2087 100644
--- a/packages/webview_flutter/webview_flutter/example/lib/main.dart
+++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart
@@ -210,14 +210,14 @@
       WebViewController controller, BuildContext context) async {
     // Send a message with the user agent string to the Toaster JavaScript channel we registered
     // with the WebView.
-    await controller.evaluateJavascript(
+    await controller.runJavascript(
         'Toaster.postMessage("User Agent: " + navigator.userAgent);');
   }
 
   void _onListCookies(
       WebViewController controller, BuildContext context) async {
     final String cookies =
-        await controller.evaluateJavascript('document.cookie');
+        await controller.runJavascriptReturningResult('document.cookie');
     // ignore: deprecated_member_use
     Scaffold.of(context).showSnackBar(SnackBar(
       content: Column(
@@ -232,7 +232,7 @@
   }
 
   void _onAddToCache(WebViewController controller, BuildContext context) async {
-    await controller.evaluateJavascript(
+    await controller.runJavascript(
         'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
     // ignore: deprecated_member_use
     Scaffold.of(context).showSnackBar(const SnackBar(
@@ -241,7 +241,7 @@
   }
 
   void _onListCache(WebViewController controller, BuildContext context) async {
-    await controller.evaluateJavascript('caches.keys()'
+    await controller.runJavascript('caches.keys()'
         '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
         '.then((caches) => Toaster.postMessage(caches))');
   }
diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart
index 7699cc4..410a995 100644
--- a/packages/webview_flutter/webview_flutter/lib/src/webview.dart
+++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart
@@ -148,7 +148,7 @@
   /// The initial URL to load.
   final String? initialUrl;
 
-  /// Whether Javascript execution is enabled.
+  /// Whether JavaScript execution is enabled.
   final JavascriptMode javascriptMode;
 
   /// The set of [JavascriptChannel]s available to JavaScript code running in the web view.
@@ -221,9 +221,9 @@
   /// When [onPageFinished] is invoked on Android, the page being rendered may
   /// not be updated yet.
   ///
-  /// When invoked on iOS or Android, any Javascript code that is embedded
+  /// When invoked on iOS or Android, any JavaScript code that is embedded
   /// directly in the HTML has been loaded and code injected with
-  /// [WebViewController.evaluateJavascript] can assume this.
+  /// [WebViewController.runJavascript] or [WebViewController.runJavascriptReturningResult] can assume this.
   final PageFinishedCallback? onPageFinished;
 
   /// Invoked when a page is loading.
@@ -594,27 +594,75 @@
   ///
   /// On iOS depending on the value type the return value would be one of:
   ///
-  ///  - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100').
-  ///  - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.').
-  ///  - Other non-primitive types are not supported on iOS and will complete the Future with an error.
+  ///  - For primitive JavaScript types: the value string formatted
+  ///    (e.g JavaScript 100 returns '100').
+  ///  - For JavaScript arrays of supported types: a string formatted NSArray
+  ///    (e.g '(1,2,3), note that the string for NSArray is formatted and might
+  ///    contain newlines and extra spaces.').
+  ///  - Other non-primitive types are not supported on iOS and will complete
+  ///    the Future with an error.
   ///
-  /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the
-  /// evaluated expression is not supported as described above.
+  /// The Future completes with an error if a JavaScript error occurred,
+  /// or on iOS, if the type of the evaluated expression is
+  /// not supported as described above.
   ///
-  /// When evaluating Javascript in a [WebView], it is best practice to wait for
-  /// the [WebView.onPageFinished] callback. This guarantees all the Javascript
+  /// When evaluating JavaScript in a [WebView], it is best practice to wait for
+  /// the [WebView.onPageFinished] callback. This guarantees all the JavaScript
   /// embedded in the main frame HTML has been loaded.
+  @Deprecated('Use [runJavascript] or [runJavascriptReturningResult]')
   Future<String> evaluateJavascript(String javascriptString) {
     if (_settings.javascriptMode == JavascriptMode.disabled) {
       return Future<String>.error(FlutterError(
           'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.'));
     }
-    // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter.
-    // https://github.com/flutter/flutter/issues/26431
-    // ignore: strong_mode_implicit_dynamic_method
     return _webViewPlatformController.evaluateJavascript(javascriptString);
   }
 
+  /// Runs the given JavaScript in the context of the current page.
+  /// If you are looking for the result, use [runJavascriptReturningResult] instead.
+  /// The Future completes with an error if a JavaScript error occurred.
+  ///
+  /// When running JavaScript in a [WebView], it is best practice to wait for
+  //  the [WebView.onPageFinished] callback. This guarantees all the JavaScript
+  //  embedded in the main frame HTML has been loaded.
+  Future<void> runJavascript(String javaScriptString) {
+    if (_settings.javascriptMode == JavascriptMode.disabled) {
+      return Future<void>.error(FlutterError(
+          'JavaScript mode must be enabled/unrestricted when calling runJavascript.'));
+    }
+    return _webViewPlatformController.runJavascript(javaScriptString);
+  }
+
+  /// Runs the given JavaScript in the context of the current page,
+  /// and returns the result.
+  ///
+  /// On Android returns the evaluation result as a JSON formatted string.
+  ///
+  /// On iOS depending on the value type the return value would be one of:
+  ///
+  ///  - For primitive JavaScript types: the value string formatted
+  ///    (e.g JavaScript 100 returns '100').
+  ///  - For JavaScript arrays of supported types: a string formatted NSArray
+  ///    (e.g '(1,2,3), note that the string for NSArray is formatted and might
+  ///    contain newlines and extra spaces.').
+  ///
+  /// The Future completes with an error if a JavaScript error occurred,
+  /// or if the type the given expression evaluates to is unsupported.
+  /// Unsupported values include certain non primitive types on iOS, as well as
+  /// `undefined` or `null` on iOS 14+.
+  ///
+  /// When evaluating JavaScript in a [WebView], it is best practice to wait
+  /// for the [WebView.onPageFinished] callback. This guarantees all the
+  /// JavaScript embedded in the main frame HTML has been loaded.
+  Future<String> runJavascriptReturningResult(String javaScriptString) {
+    if (_settings.javascriptMode == JavascriptMode.disabled) {
+      return Future<String>.error(FlutterError(
+          'JavaScript mode must be enabled/unrestricted when calling runJavascriptReturningResult.'));
+    }
+    return _webViewPlatformController
+        .runJavascriptReturningResult(javaScriptString);
+  }
+
   /// Returns the title of the currently loaded page.
   Future<String?> getTitle() {
     return _webViewPlatformController.getTitle();
diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml
index dabfe0d..95a86fa 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/plugins/tree/master/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: 2.1.2
+version: 2.2.0
 
 environment:
   sdk: ">=2.14.0 <3.0.0"
@@ -19,9 +19,9 @@
 dependencies:
   flutter:
     sdk: flutter
-  webview_flutter_platform_interface: ^1.0.0
-  webview_flutter_android: ^2.0.13
-  webview_flutter_wkwebview: ^2.0.13
+  webview_flutter_platform_interface: ^1.2.0
+  webview_flutter_android: ^2.2.0
+  webview_flutter_wkwebview: ^2.2.0
 
 dev_dependencies:
   flutter_driver:
diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart
index f7d0926..8d0f754 100644
--- a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart
+++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart
@@ -372,7 +372,9 @@
       ),
     );
     expect(
-        await controller.evaluateJavascript("fake js string"), "fake js string",
+        // ignore: deprecated_member_use_from_same_package
+        await controller.evaluateJavascript("fake js string"),
+        "fake js string",
         reason: 'should get the argument');
   });
 
@@ -389,11 +391,80 @@
       ),
     );
     expect(
+      // ignore: deprecated_member_use_from_same_package
       () => controller.evaluateJavascript('fake js string'),
       throwsA(anything),
     );
   });
 
+  testWidgets('runJavaScript', (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        javascriptMode: JavascriptMode.unrestricted,
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    await controller.runJavascript('fake js string');
+    expect(fakePlatformViewsController.lastCreatedView?.lastRunJavaScriptString,
+        'fake js string');
+  });
+
+  testWidgets('runJavaScript with JavascriptMode disabled',
+      (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        javascriptMode: JavascriptMode.disabled,
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(
+      () => controller.runJavascript('fake js string'),
+      throwsA(anything),
+    );
+  });
+
+  testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        javascriptMode: JavascriptMode.unrestricted,
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(await controller.runJavascriptReturningResult("fake js string"),
+        "fake js string",
+        reason: 'should get the argument');
+  });
+
+  testWidgets('runJavaScriptReturningResult with JavascriptMode disabled',
+      (WidgetTester tester) async {
+    late WebViewController controller;
+    await tester.pumpWidget(
+      WebView(
+        initialUrl: 'https://flutter.io',
+        javascriptMode: JavascriptMode.disabled,
+        onWebViewCreated: (WebViewController webViewController) {
+          controller = webViewController;
+        },
+      ),
+    );
+    expect(
+      () => controller.runJavascriptReturningResult('fake js string'),
+      throwsA(anything),
+    );
+  });
+
   testWidgets('Cookies can be cleared once', (WidgetTester tester) async {
     await tester.pumpWidget(
       const WebView(
@@ -960,6 +1031,8 @@
   bool? debuggingEnabled;
   String? userAgent;
 
+  String? lastRunJavaScriptString;
+
   Future<dynamic> onMethodCall(MethodCall call) {
     switch (call.method) {
       case 'loadUrl':
@@ -993,8 +1066,13 @@
         return Future<void>.sync(() {});
       case 'currentUrl':
         return Future<String?>.value(currentUrl);
+      case 'runJavascriptReturningResult':
       case 'evaluateJavascript':
+        lastRunJavaScriptString = call.arguments;
         return Future<dynamic>.value(call.arguments);
+      case 'runJavascript':
+        lastRunJavaScriptString = call.arguments;
+        return Future<void>.sync(() {});
       case 'addJavascriptChannels':
         final List<String> channelNames = List<String>.from(call.arguments);
         javascriptChannelNames!.addAll(channelNames);