pichillilorenzo / flutter_inappwebview

A Flutter plugin that allows you to add an inline webview, to use a headless webview, and to open an in-app browser window.
https://inappwebview.dev
Apache License 2.0
3.22k stars 1.58k forks source link

JavaScript Communication for Flutter Web #1329

Open wujekbogdan opened 2 years ago

wujekbogdan commented 2 years ago

In this topic I found that flutter_inappwebview works on the web. I gave it a try and indeed the webview renders inside an <iframe>, but I'm not able to implement JavaScript Communication.

  1. In case of JavaScript Handlers the window.flutter_inappwebview.callHandler ("outside" the iframe, in the global scope) resolves with undefined
  2. Web Message Listeners don't work either: window. myObject object resolves with undefined when accessed from the "inside" of the iframe.

I'm getting a bunch of errors in the console:

DOMException: Blocked a frame with origin "http://localhost:5005" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:5005/packages/flutter_inappwebview/assets/web/web_support.js:46:65)
web_support.js:77 DOMException: Blocked a frame with origin "http://localhost:5005" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:5005/packages/flutter_inappwebview/assets/web/web_support.js:54:61)
web_support.js:191 DOMException: Blocked a frame with origin "http://localhost:5005" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:5005/packages/flutter_inappwebview/assets/web/web_support.js:81:74)
web_support.js:227 DOMException: Blocked a frame with origin "http://localhost:5005" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:5005/packages/flutter_inappwebview/assets/web/web_support.js:197:59)

Is it expected? Is Javascript Communication supposed to be working already in the develop branch?

github-actions[bot] commented 2 years ago

👋 @wujekbogdan

NOTE: This comment is auto-generated.

Are you sure you have already searched for the same problem?

Some people open new issues but they didn't search for something similar or for the same issue. Please, search for it using the GitHub issue search box or on the official inappwebview.dev website, or, also, using Google, StackOverflow, etc. before posting a new one. You may already find an answer to your problem!

If this is really a new issue, then thank you for raising it. I will investigate it and get back to you as soon as possible. Please, make sure you have given me as much context as possible! Also, if you didn't already, post a code example that can replicate this issue.

In the meantime, you can already search for some possible solutions online! Because this plugin uses native WebView, you can search online for the same issue adding android WebView [MY ERROR HERE] or ios WKWebView [MY ERROR HERE] keywords.

Following these steps can save you, me, and other people a lot of time, thanks!

pichillilorenzo commented 1 year ago

Currently, It's not possible to use javascript handlers on Web platform. iframe objects are very very limited, unfortunately. To know if a method/event/class is supported on Web, check the code docs directly.

ert485 commented 1 year ago

EDIT - rephrased since I'm focusing on one part of @wujekbogdan's post (not the error output)

From outside the webview (in dart), it's possible to push in javascript to get run with eval (as long as the "origin" matches -- otherwise you will always get the errors mentioned in the original post).

In case of JavaScript Handlers the window.flutter_inappwebview.callHandler ("outside" the iframe, in the global scope) resolves with undefined Web Message Listeners don't work either: window. myObject object resolves with undefined when accessed from the "inside" of the iframe.

The above quote mentions things that if implemented would address my issue (and I believe it is possible as long as the origin matches). "evaluateJavascript" has a (synchronous) return value, but that won't work for all the cases. For a lot of cases the action originates inside the webview and you might want immediate recognition of that in flutter.

So even if you have the same origin, it being unnecessarily restrictive.

ert485 commented 1 year ago

Although it would be a bit of a hack, it is currently possible to use onConsoleMessage (the callback parameter for InAppWebView constructor) as a way to trigger dart things based on a JS statement (e.g. console.log(JSON.stringify({type: 'callback', name: 'foo', value: 'bar'}))).

jyr commented 1 year ago

Although it would be a bit of a hack, it should be currently possible to use onConsoleMessage (the callback parameter for InAppWebView constructor) as a way to trigger dart things based on a JS statement (e.g. console.log(JSON.stringify({type: 'callback', name: 'foo', value: 'bar'}))).

I don't understand what happen, I'm using the inappwebview v6.0.0-beta.22 and saw the documentation that javascript injection should works. But i have the same errors:

DOMException: Blocked a frame with origin "http://localhost:63239" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:63239/assets/packages/flutter_inappwebview/assets/web/web_support.js:49:65)
js_primitives.dart:30
DOMException: Blocked a frame with origin "http://localhost:63239" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:63239/assets/packages/flutter_inappwebview/assets/web/web_support.js:57:61)
DOMException: Blocked a frame with origin "http://localhost:63239" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:63239/assets/packages/flutter_inappwebview/assets/web/web_support.js:84:74)
DOMException: Blocked a frame with origin "http://localhost:63239" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:63239/assets/packages/flutter_inappwebview/assets/web/web_support.js:200:59)

And I get null value when run

onLoadStop: (controller, url) async {
                    var result =
                        await controller.evaluateJavascript(source: "1 + 1");
                    print(result.runtimeType); // int
                    print(result); // 2
                  }

Here my WebView

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';

class BelvoInAppWebView extends StatefulWidget {
  const BelvoInAppWebView({
    Key? key,
    required this.url,
    this.width,
    this.height,
    this.bypass = false,
    this.horizontalScroll = false,
    this.verticalScroll = false,
  }) : super(key: key);

  final bool bypass;
  final bool horizontalScroll;
  final bool verticalScroll;
  final double? height;
  final double? width;
  final String url;

  @override
  _BelvoInAppWebViewState createState() => new _BelvoInAppWebViewState();
}

class _BelvoInAppWebViewState extends State<BelvoInAppWebView> {
  final GlobalKey webViewKey = GlobalKey();

  InAppWebViewController? webViewController;
  InAppWebViewSettings settings = InAppWebViewSettings(
      useShouldOverrideUrlLoading: true,
      mediaPlaybackRequiresUserGesture: false,
      allowsInlineMediaPlayback: true,
      iframeAllow: "camera; microphone",
      iframeAllowFullscreen: true);

  PullToRefreshController? pullToRefreshController;
  String url = "";
  double progress = 0;
  final urlController = TextEditingController();

  @override
  void initState() {
    super.initState();

    pullToRefreshController = kIsWeb
        ? null
        : PullToRefreshController(
            settings: PullToRefreshSettings(
              color: Colors.blue,
            ),
            onRefresh: () async {
              if (defaultTargetPlatform == TargetPlatform.android) {
                webViewController?.reload();
              } else if (defaultTargetPlatform == TargetPlatform.iOS) {
                webViewController?.loadUrl(
                    urlRequest:
                        URLRequest(url: await webViewController?.getUrl()));
              }
            },
          );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text("Official InAppWebView website")),
        body: SafeArea(
            child: Column(children: <Widget>[
          TextField(
            decoration: InputDecoration(prefixIcon: Icon(Icons.search)),
            controller: urlController,
            keyboardType: TextInputType.url,
            onSubmitted: (value) {
              var url = WebUri(value);
              if (url.scheme.isEmpty) {
                url = WebUri("https://www.google.com/search?q=" + value);
              }
              webViewController?.loadUrl(urlRequest: URLRequest(url: url));
            },
          ),
          Expanded(
            child: Stack(
              children: [
                InAppWebView(
                  key: webViewKey,
                  initialUrlRequest:
                      URLRequest(url: WebUri("https://flutter.dev")),
                  initialSettings: settings,
                  pullToRefreshController: pullToRefreshController,
                  onWebViewCreated: (controller) {
                    debugPrint('onWebViewCreated');
                    webViewController = controller;
                  },
                  onLoadStart: (controller, url) {
                    debugPrint('onLoadStart');
                    setState(() {
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  onPermissionRequest: (controller, request) async {
                    return PermissionResponse(
                        resources: request.resources,
                        action: PermissionResponseAction.GRANT);
                  },
                  shouldOverrideUrlLoading:
                      (controller, navigationAction) async {
                    var uri = navigationAction.request.url!;

                    if (![
                      "http",
                      "https",
                      "file",
                      "chrome",
                      "data",
                      "javascript",
                      "about"
                    ].contains(uri.scheme)) {
                      if (await canLaunchUrl(uri)) {
                        // Launch the App
                        await launchUrl(
                          uri,
                        );
                        // and cancel the request
                        return NavigationActionPolicy.CANCEL;
                      }
                    }

                    return NavigationActionPolicy.ALLOW;
                  },
                  onLoadStop: (controller, url) async {
                    var result =
                        await controller.evaluateJavascript(source: "1 + 1");
                    print(result.runtimeType); // int
                    print(result); // 2
                  },
                  onReceivedError: (controller, request, error) {
                    pullToRefreshController?.endRefreshing();
                  },
                  onProgressChanged: (controller, progress) {
                    if (progress == 100) {
                      pullToRefreshController?.endRefreshing();
                    }
                    setState(() {
                      this.progress = progress / 100;
                      urlController.text = this.url;
                    });
                  },
                  onUpdateVisitedHistory: (controller, url, androidIsReload) {
                    setState(() {
                      this.url = url.toString();
                      urlController.text = this.url;
                    });
                  },
                  onConsoleMessage: (controller, consoleMessage) {
                    print(consoleMessage);
                  },
                ),
                progress < 1.0
                    ? LinearProgressIndicator(value: progress)
                    : Container(),
              ],
            ),
          ),
          ButtonBar(
            alignment: MainAxisAlignment.center,
            children: <Widget>[
              ElevatedButton(
                child: Icon(Icons.arrow_back),
                onPressed: () {
                  webViewController?.goBack();
                },
              ),
              ElevatedButton(
                child: Icon(Icons.arrow_forward),
                onPressed: () {
                  webViewController?.goForward();
                },
              ),
              ElevatedButton(
                child: Icon(Icons.refresh),
                onPressed: () {
                  webViewController?.reload();
                },
              ),
            ],
          ),
        ])));
  }
}
ert485 commented 1 year ago

@jyr

Blocked a frame with origin "http://localhost:63239" from accessing a cross-origin frame.

This is a web security feature that is built into the browser you view your app from (and every common web browser). See here:

initialUrlRequest: URLRequest(url: WebUri("https://flutter.dev")),

... since the iframe url is a different origin than your flutter app (flutter.dev is different than localhost:63239), you won't be able to do javascript communication across the barrier. If instead you loaded localhost:63239/some-other-page into the web view, then the error would go away.

This is mentioned in the docs here (require the iframe to have the same origin of the website.) Screen Shot 2023-03-27 at 8 56 32 AM

jyr commented 1 year ago

@jyr

Blocked a frame with origin "http://localhost:63239" from accessing a cross-origin frame.

This is a web security feature that is built into the browser you view your app from (and every common web browser). See here:

initialUrlRequest: URLRequest(url: WebUri("https://flutter.dev")),

... since the iframe url is a different origin than your flutter app (flutter.dev is different than localhost:63239), you won't be able to do javascript communication across the barrier. If instead you loaded localhost:63239/some-other-page into the web view, then the error would go away.

Does works with inappwebview 6xxx? because I disabled the cors with:

flutter run -d chrome --web-browser-flag "--disable-web-security"

and same errors

ert485 commented 1 year ago

Does works with inappwebview 6xxx?

The inappwebview version shouldn't have any effect on this behaviour (since all versions are implemented in Web with iframe).

because I disabled the cors

I'm not sure what scope --disable-web-security all has (how many security features it disables). It's not the typical CORS situation (at least from what I'm familiar with) because it's not a network request that's being blocked, it's javascript execution being blocked.

This also might help: a similar situation asked about on stack overflow

jyr commented 1 year ago

If instead you loaded localhost:63239/some-other-page into the web view, then the error would go away.

So, is it impossible to use a WebView with a external link? or Do I have to insert the external link in localhost:63239/some-other-page and this page loading it in a WebView?

ert485 commented 1 year ago

So, is it impossible to use a WebView with a external link?

In general, yes it's impossible (assuming you need javascript evaluations in the embedded page, and need to do web builds).

It's a browser security feature because evaluating arbitrary javascript would let you do things like steal passwords/tokens/cookies/etc. from the user.

For example, the attack could be:

The iframe goes to "google.com/signin", and on that page you enter your Google password to log in. If the Flutter app hosting the iframe can evaluate javascript inside the iframe, the Flutter app could inject something that observes keystrokes that happen inside the iframe, and then can send the password you entered to a hacker's server.

I'm kinda surprised the security feature isn't there for Android Webviews etc. But then again, nothing is stopping an app that is implementing its own "web browser" from skipping web security features.

There might be ways that the "external" page could allow certain data in/out (I would maybe look into things like "web messages"), but that would require the person developing that external page to do the implementation.

fvisticot commented 1 year ago

Using in Web platform with ^6.0.0-beta.23 version

In my case the HTML is local html injected with initialData In that case of local injected HTML (with initialData) is it possible to have communication between JS and Flutter ? are the 2 different piece of code (Flutter and JS) in the same origin ?

InAppWebView(
              initialData: InAppWebViewInitialData(data: html, encoding: 'utf-8'),
              onLoadStop: (controller, url) async {
                var result = await controller.evaluateJavascript(source: "1 + 1");
                print("===>${result.runtimeType}"); // int
                print("===>Result: $result"); // 2
                print("====>onLoadStop");
              },

I get the same cross origin frame error

DOMException: Blocked a frame with origin "http://localhost:64428" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:64428/assets/packages/flutter_inappwebview/assets/web/web_support.js:49:65)
web_support.js:80 DOMException: Blocked a frame with origin "http://localhost:64428" from accessing a cross-origin frame.
    at HTMLIFrameElement.<anonymous> (http://localhost:64428/assets/packages/flutter_inappwebview/assets/web/web_support.js:57:61)
ert485 commented 1 year ago

In that case of local injected HTML (with initialData) is it possible to have communication between JS and Flutter ?

I noticed the same problem in that situation, my workaround was to add the html as an asset file (where it gets hosted under the same origin at localhost:64428/assets/assets). But that only works if your html content is static enough to put into a file.

  /// for interactive JS, this URI needs to have the same origin as the flutter app (In Flutter Web builds, the assets folders will work for this).
  /// however, in non-web builds, assets aren't hosted in URI's like this, so you'll need to use something else
  static final WebUri htmlFileURI = WebUri.uri(Uri.base.replace(path: "/assets/assets/webview-content.html")); // note the double `assets/`, that's not a typo, it was actually needed

  /// Instead of using `htmlFileURI`, you can use a string with the content of the HTML.
  /// In Web builds, loading the WebView with this content doesn't seem to allow for interactive JS.
  static final Future<String> htmlString = rootBundle.loadString("assets/webview-content.html");

  /// If true we will use `htmlString` instead of `htmlFileURI`, for the content of the WebView.
  static bool loadFromString = !isWeb;

Alternatively.... another way (that should work but I haven't gotten to work yet)... You might be able to set the starting page that the html gets injected into with baseUrl in loadData. I recently noticed that the default baseUrl is actually about:blank instead of localhost

InAppWebView(
  onWebViewCreated: (InAppWebViewController controller) async {
    controller.loadData(
          baseUrl: WebUri.uri(Uri.base),
          data: html
sanalkv commented 9 months ago

@pichillilorenzo Any solution for this ?

Screenshot 2023-12-15 at 6 44 53 PM

The above is just returning null. I'm using flutter web.

alien142 commented 8 months ago

if webview open a localhost site or a site where you deploy web. It will work fine. I tried to use open document through docs.google.com, but almost none of the features work. Currently, look like inappwebview is very limited for web platform