Skip to content
Draft
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
6 changes: 6 additions & 0 deletions packages/webview_flutter/webview_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 4.15.0

* Adds `WebViewController.addDocumentStartJavaScript` for injecting JavaScript
that runs at the start of future document loads. It returns a
`DocumentStartJavaScriptRegistration` that can be used to stop injecting the JavaScript.

## 4.14.1

* Adds documentation for `NavigationDelegate` callback parameters.
Expand Down
21 changes: 21 additions & 0 deletions packages/webview_flutter/webview_flutter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ See the Dartdocs for [WebViewController](https://pub.dev/documentation/webview_f
and [WebViewWidget](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html)
for more details.

### Running JavaScript at document start

`WebViewController.addDocumentStartJavaScript` registers JavaScript that runs at the start of every
document loaded after the call, before the scripts of the loaded page run:

<?code-excerpt "main.dart (document_start_javascript)"?>
```dart
final DocumentStartJavaScriptRegistration documentStartJavaScriptRegistration = await webViewController
.addDocumentStartJavaScript(
'window.exampleValue = "Hello from a document start script!";',
);
```

The script runs in every frame of the document, including cross-origin `<iframe>`s, so it should not
contain sensitive data. Calling `remove()` on the returned registration deregisters it, stopping
JavaScript injection into future document loads. The registration can be discarded if the script
should be injected for the lifetime of the controller.

This feature is not available on the web, or on Android devices whose WebView does not support the
`DOCUMENT_START_SCRIPT` feature; an `UnsupportedError` is thrown in those cases.

### Platform-Specific Features

Many classes have a subclass or an underlying implementation that provides access to platform-specific
Expand Down
59 changes: 59 additions & 0 deletions packages/webview_flutter/webview_flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ const String kLogExamplePage = '''
</html>
''';

const String kDocumentStartExamplePage = '''
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document start script example</title>
</head>
<body>

<h1>Document start script demo page</h1>
<p id="message">The document start script did not run.</p>

<script>
document.getElementById('message').textContent =
'The document start script set window.exampleValue to: ' +
window.exampleValue;
</script>

</body>
</html>
''';

class WebViewExample extends StatefulWidget {
const WebViewExample({super.key});

Expand Down Expand Up @@ -303,6 +324,7 @@ enum MenuOptions {
setCookie,
logExample,
basicAuthentication,
documentStartJavaScript,
}

class SampleMenu extends StatelessWidget {
Expand All @@ -311,6 +333,8 @@ class SampleMenu extends StatelessWidget {
final WebViewController webViewController;
late final WebViewCookieManager cookieManager = WebViewCookieManager();

static DocumentStartJavaScriptRegistration? _documentStartJavaScriptRegistration;

@override
Widget build(BuildContext context) {
return PopupMenuButton<MenuOptions>(
Expand Down Expand Up @@ -347,6 +371,8 @@ class SampleMenu extends StatelessWidget {
_onLogExample();
case MenuOptions.basicAuthentication:
_promptForUrl(context);
case MenuOptions.documentStartJavaScript:
_onDocumentStartJavaScriptExample(context);
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
Expand Down Expand Up @@ -399,6 +425,10 @@ class SampleMenu extends StatelessWidget {
value: MenuOptions.basicAuthentication,
child: Text('Basic Authentication Example'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.documentStartJavaScript,
child: Text('Document start JavaScript example'),
),
],
);
}
Expand Down Expand Up @@ -511,6 +541,35 @@ class SampleMenu extends StatelessWidget {
return webViewController.loadHtmlString(kTransparentBackgroundPage);
}

Future<void> _onDocumentStartJavaScriptExample(BuildContext context) async {
// Removing the previously added registration keeps this example from
// injecting the same JavaScript again every time it is run.
await _documentStartJavaScriptRegistration?.remove();
_documentStartJavaScriptRegistration = null;

try {
// #docregion document_start_javascript
final DocumentStartJavaScriptRegistration documentStartJavaScriptRegistration =
await webViewController.addDocumentStartJavaScript(
'window.exampleValue = "Hello from a document start script!";',
);
// #enddocregion document_start_javascript
_documentStartJavaScriptRegistration = documentStartJavaScriptRegistration;
} on UnsupportedError {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Document start JavaScript is not supported on this platform.'),
),
);
}
return;
}

// The script only affects documents that are loaded after it is added.
await webViewController.loadHtmlString(kDocumentStartExamplePage);
}

Widget _getCookieList(List<WebViewCookie> cookies) {
if (cookies.isEmpty) {
return Container();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,30 @@ class WebViewController {
return platform.runJavaScript(javaScript);
}

/// Adds JavaScript that runs at the start of future document loads.
///
/// This method should be called before loading a page if the script needs to
/// run for that page. It does not run JavaScript in the currently loaded
/// document. Scripts run in the order in which they were added.
///
/// The script runs in every frame of a loaded document, regardless of its
/// origin, including cross-origin `<iframe>`s, so it should not contain
/// sensitive data.
///
/// The returned [DocumentStartJavaScriptRegistration] can be used to stop injecting the
/// JavaScript into future document loads. It can be discarded if the
/// JavaScript should be injected for the lifetime of this controller.
///
/// Throws an [UnsupportedError] if the current platform does not support
/// document-start JavaScript injection. This is currently the case on the
/// web, and on Android when the WebView installed on the device does not
/// support the `DOCUMENT_START_SCRIPT` feature.
Future<DocumentStartJavaScriptRegistration> addDocumentStartJavaScript(String javaScript) async {
return DocumentStartJavaScriptRegistration.fromPlatform(
await platform.addDocumentStartJavaScript(javaScript),
);
}

/// Runs the given JavaScript in the context of the current page, and returns
/// the result.
///
Expand Down Expand Up @@ -423,6 +447,22 @@ class WebViewController {
}
}

/// A registration for JavaScript injected at the start of future document loads.
///
/// Returned by [WebViewController.addDocumentStartJavaScript].
class DocumentStartJavaScriptRegistration {
/// Constructs a [DocumentStartJavaScriptRegistration] from a specific platform
/// implementation.
DocumentStartJavaScriptRegistration.fromPlatform(this.platform);

/// Implementation of [PlatformDocumentStartJavaScriptRegistration] for the current
/// platform.
final PlatformDocumentStartJavaScriptRegistration platform;

/// Deregisters this registration, stopping JavaScript injection into future document loads.
Future<void> remove() => platform.remove();
}

/// Permissions request when web content requests access to protected resources.
///
/// A response MUST be provided by calling [grant], [deny], or a method from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte
NavigationRequest,
NavigationRequestCallback,
PageEventCallback,
PlatformDocumentStartJavaScriptRegistration,
PlatformNavigationDelegateCreationParams,
PlatformWebViewControllerCreationParams,
PlatformWebViewCookieManagerCreationParams,
Expand Down
8 changes: 4 additions & 4 deletions packages/webview_flutter/webview_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: webview_flutter
description: A Flutter plugin that provides a WebView widget backed by the system webview.
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.14.1
version: 4.15.0

environment:
sdk: ^3.10.0
Expand All @@ -21,9 +21,9 @@ flutter:
dependencies:
flutter:
sdk: flutter
webview_flutter_android: ^4.12.0
webview_flutter_platform_interface: ^2.15.1
webview_flutter_wkwebview: ^3.25.1
webview_flutter_android: ^4.15.0
webview_flutter_platform_interface: ^2.16.0
webview_flutter_wkwebview: ^3.27.0

dev_dependencies:
build_runner: ^2.1.5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte

import 'webview_controller_test.mocks.dart';

@GenerateMocks(<Type>[PlatformWebViewController, PlatformNavigationDelegate])
@GenerateMocks(<Type>[
PlatformWebViewController,
PlatformNavigationDelegate,
PlatformDocumentStartJavaScriptRegistration,
])
void main() {
test('loadFile', () async {
final mockPlatformWebViewController = MockPlatformWebViewController();
Expand Down Expand Up @@ -146,6 +150,40 @@ void main() {
verify(mockPlatformWebViewController.runJavaScript('1 + 1'));
});

test('addDocumentStartJavaScript', () async {
final mockPlatformWebViewController = MockPlatformWebViewController();
final mockPlatformDocumentStartJavaScriptRegistration =
MockPlatformDocumentStartJavaScriptRegistration();
when(
mockPlatformWebViewController.addDocumentStartJavaScript('window.test = true;'),
).thenAnswer((_) async => mockPlatformDocumentStartJavaScriptRegistration);

final webViewController = WebViewController.fromPlatform(mockPlatformWebViewController);

final DocumentStartJavaScriptRegistration documentStartJavaScript = await webViewController
.addDocumentStartJavaScript('window.test = true;');

verify(mockPlatformWebViewController.addDocumentStartJavaScript('window.test = true;'));
expect(documentStartJavaScript.platform, mockPlatformDocumentStartJavaScriptRegistration);
});

test('DocumentStartJavaScriptRegistration.remove', () async {
final mockPlatformWebViewController = MockPlatformWebViewController();
final mockPlatformDocumentStartJavaScriptRegistration =
MockPlatformDocumentStartJavaScriptRegistration();
when(
mockPlatformWebViewController.addDocumentStartJavaScript('window.test = true;'),
).thenAnswer((_) async => mockPlatformDocumentStartJavaScriptRegistration);

final webViewController = WebViewController.fromPlatform(mockPlatformWebViewController);

final DocumentStartJavaScriptRegistration documentStartJavaScript = await webViewController
.addDocumentStartJavaScript('window.test = true;');
await documentStartJavaScript.remove();

verify(mockPlatformDocumentStartJavaScriptRegistration.remove());
});

test('runJavaScriptReturningResult', () async {
final mockPlatformWebViewController = MockPlatformWebViewController();
when(
Expand Down
Loading