Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
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
5 changes: 5 additions & 0 deletions packages/webview_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.3.1

* Added JavaScript channels to facilitate message passing from JavaScript code running inside
the WebView to the Flutter app's Dart code.

## 0.3.0

* **Breaking change**. Migrate from the deprecated original Android Support
Expand Down
56 changes: 35 additions & 21 deletions packages/webview_flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,34 @@ class WebViewExample extends StatelessWidget {
SampleMenu(_controller.future),
],
),
body: WebView(
initialUrl: 'https://flutter.io',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
),
// We're using a Builder here so we have a context that is below the Scaffold
// to allow calling Scaffold.of(context) so we can show a snackbar.
body: Builder(builder: (BuildContext context) {
return WebView(
initialUrl: 'https://flutter.io',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
javascriptChannels: <JavascriptChannel>[
_toasterJavascriptChannel(context),
].toSet(),
);
}),
floatingActionButton: favoriteButton(),
);
}

JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (JavascriptMessage message) {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}

Widget favoriteButton() {
return FutureBuilder<WebViewController>(
future: _controller.future,
Expand All @@ -44,7 +61,7 @@ class WebViewExample extends StatelessWidget {
onPressed: () async {
final String url = await controller.data.currentUrl();
Scaffold.of(context).showSnackBar(
SnackBar(content: Text("Favorited $url")),
SnackBar(content: Text('Favorited $url')),
);
},
child: const Icon(Icons.favorite),
Expand All @@ -56,7 +73,7 @@ class WebViewExample extends StatelessWidget {
}

enum MenuOptions {
evaluateJavascript,
showUserAgent,
toast,
}

Expand All @@ -73,8 +90,8 @@ class SampleMenu extends StatelessWidget {
return PopupMenuButton<MenuOptions>(
onSelected: (MenuOptions value) {
switch (value) {
case MenuOptions.evaluateJavascript:
_onEvaluateJavascript(controller.data, context);
case MenuOptions.showUserAgent:
_onShowUserAgent(controller.data, context);
break;
case MenuOptions.toast:
Scaffold.of(context).showSnackBar(
Expand All @@ -87,8 +104,8 @@ class SampleMenu extends StatelessWidget {
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
PopupMenuItem<MenuOptions>(
value: MenuOptions.evaluateJavascript,
child: const Text('Evaluate JavaScript'),
value: MenuOptions.showUserAgent,
child: const Text('Show user agent'),
enabled: controller.hasData,
),
const PopupMenuItem<MenuOptions>(
Expand All @@ -101,15 +118,12 @@ class SampleMenu extends StatelessWidget {
);
}

void _onEvaluateJavascript(
void _onShowUserAgent(
WebViewController controller, BuildContext context) async {
final String result = await controller
.evaluateJavascript("document.body.style.backgroundColor = 'red'");
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('JavaScript evaluated, the result is: $result'),
),
);
// Send a message with the user agent string to the Toaster JavaScript channel we registered
// with the WebView.
controller.evaluateJavascript(
'Toaster.postMessage("User Agent: " + navigator.userAgent);');
}
}

Expand Down
177 changes: 169 additions & 8 deletions packages/webview_flutter/lib/webview_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,51 @@ enum JavascriptMode {
unrestricted,
}

/// A message that was sent by JavaScript code running in a [WebView].
class JavascriptMessage {
/// Constructs a JavaScript message object.
///
/// The `message` parameter must not be null.
const JavascriptMessage(this.message) : assert(message != null);

/// The contents of the message that was sent by the JavaScript code.
final String message;
}

/// Callback type for handling messages sent from Javascript running in a web view.
typedef void JavascriptMessageHandler(JavascriptMessage message);

final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9]*\$');

/// A named channel for receiving messaged from JavaScript code running inside a web view.
class JavascriptChannel {
/// Constructs a Javascript channel.
///
/// The parameters `name` and `onMessageReceived` must not be null.
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if somebody tries to pass in foo.bar as a name here? I'm asking because it's a common practice to try and namespace everything under one global namespaced foo object if you need to start creating objects in the global namespace at all in WFE development, so it's something that people are likely to try.

It's described as "Nested Namespacing" on this page.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.

I changed it to require that channel names follow the Dart identifier naming rules (only letters digits and underscores allowed, can't start with a digit).

JavascriptChannel({
@required this.name,
@required this.onMessageReceived,
}) : assert(name != null),
assert(onMessageReceived != null),
assert(_validChannelNames.hasMatch(name));

/// The channel's name.
///
/// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to
/// the Javascript window object's property named `name`.
///
/// The name must start with a letter or underscore(_), followed by any combination of those
/// characters plus digits.
///
/// Note that any JavaScript existing `window` property with this name will be overriden.
///
/// See also [WebView.javascriptChannels] for more details on the channel registration mechanism.
final String name;

/// A callback that's invoked when a message is received through the channel.
final JavascriptMessageHandler onMessageReceived;
}

/// A web view widget for showing html content.
class WebView extends StatefulWidget {
/// Creates a new web view.
Expand All @@ -32,6 +77,7 @@ class WebView extends StatefulWidget {
this.onWebViewCreated,
this.initialUrl,
this.javascriptMode = JavascriptMode.disabled,
this.javascriptChannels,
this.gestureRecognizers,
}) : assert(javascriptMode != null),
super(key: key);
Expand All @@ -56,6 +102,35 @@ class WebView extends StatefulWidget {
/// Whether Javascript execution is enabled.
final JavascriptMode javascriptMode;

/// The set of [JavascriptChannel]s available to JavaScript code running in the web view.
///
/// For each [JavascriptChannel] in the set, a channel object is made available for the
/// JavaScript code in a window property named [JavascriptChannel.name].
/// The JavaScript code can then call `postMessage` on that object to send a message that will be
/// passed to [JavascriptChannel.onMessageReceived].
///
/// For example for the following JavascriptChannel:
///
/// ```dart
/// JavascriptChannel(name: 'Print', onMessageReceived: (String message) { print(message); });
/// ```
///
/// JavaScript code can call:
///
/// ```javascript
/// Print.postMessage('Hello');
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be worth establishing here how namespace collision is handled on the JS side. What happens if somebody tries to name their channel Array?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline, we're going to override whatever window property that is there as this is what Android is doing. We don't think it's a security risk as the app developer has the power to do something like this anyway by injecting JavaScript.

/// ```
///
/// To asynchronously invoke the message handler which will print the message to standard output.
///
/// Adding a new JavaScript channel only takes affect after the next page is loaded.
///
/// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple
/// channels in the list.
///
/// A null value is equivalent to an empty set.
final Set<JavascriptChannel> javascriptChannels;

@override
State<StatefulWidget> createState() => _WebViewState();
}
Expand Down Expand Up @@ -103,45 +178,79 @@ class _WebViewState extends State<WebView> {
'$defaultTargetPlatform is not yet supported by the webview_flutter plugin');
}

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

@override
void didUpdateWidget(WebView oldWidget) {
super.didUpdateWidget(oldWidget);
_updateSettings(_WebSettings.fromWidget(widget));
_assertJavascriptChannelNamesAreUnique();
_updateConfiguration(_WebSettings.fromWidget(widget));
}

Future<void> _updateSettings(_WebSettings settings) async {
Future<void> _updateConfiguration(_WebSettings settings) async {
_settings = settings;
final WebViewController controller = await _controller.future;
controller._updateSettings(settings);
controller._updateJavascriptChannels(widget.javascriptChannels);
}

void _onPlatformViewCreated(int id) {
final WebViewController controller =
WebViewController._(id, _WebSettings.fromWidget(widget));
final WebViewController controller = WebViewController._(
id,
_WebSettings.fromWidget(widget),
widget.javascriptChannels,
);
_controller.complete(controller);
if (widget.onWebViewCreated != null) {
widget.onWebViewCreated(controller);
}
}

void _assertJavascriptChannelNamesAreUnique() {
if (widget.javascriptChannels == null ||
widget.javascriptChannels.isEmpty) {
return;
}
assert(_extractChannelNames(widget.javascriptChannels).length ==
widget.javascriptChannels.length);
}
}

Set<String> _extractChannelNames(Set<JavascriptChannel> channels) {
final Set<String> channelNames = channels == null
? Set<String>()
: channels.map((JavascriptChannel channel) => channel.name).toSet();
return channelNames;
}

class _CreationParams {
_CreationParams({this.initialUrl, this.settings});
_CreationParams(
{this.initialUrl, this.settings, this.javascriptChannelNames});

static _CreationParams fromWidget(WebView widget) {
return _CreationParams(
initialUrl: widget.initialUrl,
settings: _WebSettings.fromWidget(widget),
javascriptChannelNames:
_extractChannelNames(widget.javascriptChannels).toList(),
);
}

final String initialUrl;

final _WebSettings settings;

final List<String> javascriptChannelNames;

Map<String, dynamic> toMap() {
return <String, dynamic>{
'initialUrl': initialUrl,
'settings': settings.toMap(),
'javascriptChannelNames': javascriptChannelNames,
};
}
}
Expand Down Expand Up @@ -178,14 +287,32 @@ class _WebSettings {
/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated]
/// callback for a [WebView] widget.
class WebViewController {
WebViewController._(int id, _WebSettings settings)
: _channel = MethodChannel('plugins.flutter.io/webview_$id'),
_settings = settings;
WebViewController._(
int id, this._settings, Set<JavascriptChannel> javascriptChannels)
: _channel = MethodChannel('plugins.flutter.io/webview_$id') {
_updateJavascriptChannelsFromSet(javascriptChannels);
_channel.setMethodCallHandler(_onMethodCall);
}

final MethodChannel _channel;

_WebSettings _settings;

// Maps a channel name to a channel.
Map<String, JavascriptChannel> _javascriptChannels =
<String, JavascriptChannel>{};

Future<void> _onMethodCall(MethodCall call) async {
switch (call.method) {
case 'javascriptChannelMessage':
final String channel = call.arguments['channel'];
final String message = call.arguments['message'];
_javascriptChannels[channel]
.onMessageReceived(JavascriptMessage(message));
break;
}
}

/// Loads the specified URL.
///
/// `url` must not be null.
Expand Down Expand Up @@ -279,6 +406,40 @@ class WebViewController {
return _channel.invokeMethod('updateSettings', updateMap);
}

Future<void> _updateJavascriptChannels(
Set<JavascriptChannel> newChannels) async {
final Set<String> currentChannels = _javascriptChannels.keys.toSet();
final Set<String> newChannelNames = _extractChannelNames(newChannels);
final Set<String> channelsToAdd =
newChannelNames.difference(currentChannels);
final Set<String> channelsToRemove =
currentChannels.difference(newChannelNames);
if (channelsToRemove.isNotEmpty) {
// TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter.
// https://github.com/flutter/flutter/issues/26431
// ignore: strong_mode_implicit_dynamic_method
_channel.invokeMethod(
'removeJavascriptChannels', channelsToRemove.toList());
}
if (channelsToAdd.isNotEmpty) {
// TODO(amirh): remove this when the invokeMethod update makes it to stable Flutter.
// https://github.com/flutter/flutter/issues/26431
// ignore: strong_mode_implicit_dynamic_method
_channel.invokeMethod('addJavascriptChannels', channelsToAdd.toList());
}
_updateJavascriptChannelsFromSet(newChannels);
}

void _updateJavascriptChannelsFromSet(Set<JavascriptChannel> channels) {
_javascriptChannels.clear();
if (channels == null) {
return;
}
for (JavascriptChannel channel in channels) {
_javascriptChannels[channel.name] = channel;
}
}

/// Evaluates a JavaScript expression in the context of the current page.
///
/// On Android returns the evaluation result as a JSON formatted string.
Expand Down
2 changes: 1 addition & 1 deletion packages/webview_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: webview_flutter
description: A Flutter plugin that provides a WebView widget on Android and iOS.
version: 0.3.0
version: 0.3.1
author: Flutter Team <[email protected]>
homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter

Expand Down
Loading