Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/webview_flutter/webview_flutter_web/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

Google Inc.
Bodhi Mulders <info@bemacized.net>

Sophie Bremer
4 changes: 3 additions & 1 deletion packages/webview_flutter/webview_flutter_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## NEXT
## 0.3.0

* Adds support for `baseUrl` parameter in `loadHtmlString`.
* Adds `runJavaScript`.
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.

## 0.2.3+4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';
import 'dart:ui_web' as ui_web;

import 'package:flutter/widgets.dart';
import 'package:web/helpers.dart';
import 'package:web/web.dart' as web;
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';

Expand All @@ -18,7 +20,7 @@ import 'http_request_factory.dart';
@immutable
class WebWebViewControllerCreationParams
extends PlatformWebViewControllerCreationParams {
/// Creates a new [AndroidWebViewControllerCreationParams] instance.
/// Creates a new [WebWebViewControllerCreationParams] instance.
WebWebViewControllerCreationParams({
@visibleForTesting this.httpRequestFactory = const HttpRequestFactory(),
}) : super();
Expand All @@ -45,6 +47,8 @@ class WebWebViewControllerCreationParams
..style.width = '100%'
..style.height = '100%'
..style.border = 'none';

final Duration _iFrameWaitDelay = const Duration(milliseconds: 100);
}

/// An implementation of [PlatformWebViewController] using Flutter for Web API.
Expand All @@ -62,14 +66,72 @@ class WebWebViewController extends PlatformWebViewController {
WebWebViewControllerCreationParams get _webWebViewParams =>
params as WebWebViewControllerCreationParams;

// Retrieves the iFrame's content body after attachment to DOM.
Future<web.HTMLBodyElement> _getIFrameBody() async {
final web.Document document = await _getIFrameDocument();

while (document.body == null) {
await Future<void>.delayed(_webWebViewParams._iFrameWaitDelay);
}

return document.body! as HTMLBodyElement;
}

// Retrieves the iFrame's content document after attachment to DOM.
Future<web.Document> _getIFrameDocument() async {
try {
// If the document is not yet available, wait for the 'load' event.
if (_webWebViewParams.iFrame.contentDocument == null) {
final Completer<void> completer = Completer<void>();
_webWebViewParams.iFrame.addEventListener(
'load',
(web.Event _) {
completer.complete();
}.toJS,
AddEventListenerOptions(once: true),
);
// If src is not set, the iframe will never load.
if (_webWebViewParams.iFrame.src.isEmpty) {
_webWebViewParams.iFrame.src = 'about:blank';
}
await completer.future;
}
// Test origin permission
_webWebViewParams.iFrame.contentDocument!.body;
// Return on success
return _webWebViewParams.iFrame.contentDocument!;
} catch (_) {
throw StateError('Web view origin mismatch');
}
}

@override
Future<void> loadHtmlString(String html, {String? baseUrl}) async {
_webWebViewParams.iFrame.src =
Uri.dataFromString(
html,
mimeType: 'text/html',
encoding: utf8,
).toString();
Future<void> loadHtmlString(String html, {String? baseUrl}) {
final Completer<void> loading = Completer<void>();

// Load listener for load completion
_webWebViewParams.iFrame.addEventListener(
'load',
() {
try {
_webWebViewParams.iFrame.contentDocument?.write(html.toJS);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

team-web will still need to do a full review, but in the meantime: won't this change to loadHtmlString silently break any case where baseUrl is passed, as a regression from the previous behavior?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is valid concern. I see two different options to address it:

  1. The previous implementation gets added as a fallback in the catch block.

  2. We introduce a new controller parameter option to activate this PRs implementation and use the existing implementation by default as before.

What are your suggestions / thoughts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdebbar How do you want to handle branching for different origin setups?

2. We introduce a new controller parameter option to activate this PRs implementation and use the existing implementation by default as before.

Would the parameter add any information we don't already have? Can't we determine at runtime if the origins match?

} finally {
loading.complete();
}
}.toJS,
AddEventListenerOptions(once: true),
);

// Initiate load
_webWebViewParams.iFrame.src = baseUrl ?? 'about:blank';

// Time out in case load listener is not triggered
Future<void>.delayed(
const Duration(minutes: 3),
).then<void>((_) => loading.complete());

// Return future completion
return loading.future;
}

@override
Expand All @@ -89,6 +151,49 @@ class WebWebViewController extends PlatformWebViewController {
}
}

@override
Future<void> runJavaScript(String javaScript) async {
final Completer<void> run = Completer<void>();
final web.Document document = await _getIFrameDocument();
final web.HTMLBodyElement body = await _getIFrameBody();
final web.HTMLScriptElement script =
document.createElement('script') as web.HTMLScriptElement;

// Load listener for script completion
script.addEventListener(
'load',
() {
try {
body.removeChild(script);
} finally {
run.complete();
}
}.toJS,
AddEventListenerOptions(once: true),
);

// Prepare script
script.src =
Uri.dataFromString(
javaScript,
mimeType: 'text/javascript',
encoding: utf8,
).toString();

// Initiate script execution
body.appendChild(script);

// Time out in case load listener is not triggered
unawaited(
Future<void>.delayed(
const Duration(seconds: 3),
).then<void>((_) => run.complete()),
);

// Return future completion
await run.future;
}

/// Performs an AJAX request defined by [params].
Future<void> _updateIFrameFromXhr(LoadRequestParams params) async {
final web.Response response =
Expand Down
2 changes: 1 addition & 1 deletion packages/webview_flutter/webview_flutter_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter_web
description: A Flutter plugin that provides a WebView widget on web.
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_web
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 0.2.3+4
version: 0.3.0

environment:
sdk: ^3.7.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,18 @@ void main() {
WebWebViewControllerCreationParams(),
);

await controller.loadHtmlString('test html');
await controller.loadHtmlString('test #html');
expect(
(controller.params as WebWebViewControllerCreationParams).iFrame.src,
'data:text/html;charset=utf-8,${Uri.encodeFull('test html')}',
'about:blank',
);
});

test('loadHtmlString escapes "#" correctly', () async {
final WebWebViewController controller = WebWebViewController(
WebWebViewControllerCreationParams(),
);

await controller.loadHtmlString('#');
expect(
(controller.params as WebWebViewControllerCreationParams).iFrame.src,
contains('%23'),
(controller.params as WebWebViewControllerCreationParams)
.iFrame
.contentDocument!
.body!
.innerHTML,
'test #html',
);
});
});
Expand Down Expand Up @@ -242,5 +238,23 @@ void main() {
);
});
});

group('runJavaScript', () {
test('throws StateError on origin mismatch', () async {
final WebWebViewController controller = WebWebViewController(
WebWebViewControllerCreationParams(),
);
await controller.loadHtmlString(
'<html></html>',
baseUrl: 'https://flutter.dev',
);

await expectLater(
() async =>
controller.runJavaScript('console.log("StateError failed");'),
throwsA(const TypeMatcher<StateError>()),
);
});
});
});
}