diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index b26a46de5e7..25b494d90c8 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 4.9.0 + +* Adds support for `PlatformWebViewController.loadFileWithParams`. +* Introduces `AndroidLoadFileParams`, a platform-specific extension of `LoadFileParams` for Android that adds support for `headers`. + ## 4.8.2 * Bumps gradle from 8.9.0 to 8.11.1. diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 6d6293d88a3..53d3c68df62 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - webview_flutter_platform_interface: ^2.13.0 + webview_flutter_platform_interface: ^2.14.0 dev_dependencies: espresso: ^0.4.0 diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index d0ab423344e..a9954d76bad 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -18,6 +18,41 @@ import 'android_webkit_constants.dart'; import 'platform_views_service_proxy.dart'; import 'weak_reference_utils.dart'; +/// Object specifying parameters for loading a local file in a +/// [AndroidWebViewController]. +@immutable +base class AndroidLoadFileParams extends LoadFileParams { + /// Constructs a [AndroidLoadFileParams], the subclass of a [LoadFileParams]. + AndroidLoadFileParams({ + required String absoluteFilePath, + this.headers = const {}, + }) : super( + absoluteFilePath: absoluteFilePath.startsWith('file://') + ? absoluteFilePath + : Uri.file(absoluteFilePath).toString(), + ); + + /// Constructs a [AndroidLoadFileParams] using a [LoadFileParams]. + factory AndroidLoadFileParams.fromLoadFileParams( + LoadFileParams params, { + Map headers = const {}, + }) { + return AndroidLoadFileParams( + absoluteFilePath: params.absoluteFilePath, + headers: headers, + ); + } + + /// Additional HTTP headers to be included when loading the local file. + /// + /// If not provided at initialization time, doesn't add any additional headers. + /// + /// On Android, WebView supports adding headers when loading local or remote + /// content. This can be useful for scenarios like authentication, + /// content-type overrides, or custom request context. + final Map headers; +} + /// Object specifying creation parameters for creating a [AndroidWebViewController]. /// /// When adding additional fields make sure they can be null or have a default @@ -386,12 +421,29 @@ class AndroidWebViewController extends PlatformWebViewController { Future loadFile( String absoluteFilePath, ) { - final String url = absoluteFilePath.startsWith('file://') - ? absoluteFilePath - : Uri.file(absoluteFilePath).toString(); + return loadFileWithParams( + AndroidLoadFileParams( + absoluteFilePath: absoluteFilePath, + ), + ); + } - _webView.settings.setAllowFileAccess(true); - return _webView.loadUrl(url, {}); + @override + Future loadFileWithParams( + LoadFileParams params, + ) async { + switch (params) { + case final AndroidLoadFileParams params: + await Future.wait(>[ + _webView.settings.setAllowFileAccess(true), + _webView.loadUrl(params.absoluteFilePath, params.headers), + ]); + + default: + await loadFileWithParams( + AndroidLoadFileParams.fromLoadFileParams(params), + ); + } } @override diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 2c4bd97e8f0..6498db12370 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 4.8.2 +version: 4.9.0 environment: sdk: ^3.6.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter meta: ^1.10.0 - webview_flutter_platform_interface: ^2.13.0 + webview_flutter_platform_interface: ^2.14.0 dev_dependencies: build_runner: ^2.1.4 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index 14a9eba1cfa..d3a4c634930 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -274,7 +274,7 @@ void main() { )); } - test('loadFile without file prefix', () async { + test('Initializing WebView settings on controller creation', () async { final MockWebView mockWebView = MockWebView(); final MockWebSettings mockWebSettings = MockWebSettings(); createControllerWithMocks( @@ -292,56 +292,209 @@ void main() { verify(mockWebSettings.setUseWideViewPort(false)).called(1); }); - test('loadFile without file prefix', () async { - final MockWebView mockWebView = MockWebView(); - final MockWebSettings mockWebSettings = MockWebSettings(); - final AndroidWebViewController controller = createControllerWithMocks( - mockWebView: mockWebView, - mockSettings: mockWebSettings, - ); + group('loadFile', () { + test('Without file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); - await controller.loadFile('/path/to/file.html'); + await controller.loadFile('/path/to/file.html'); - verify(mockWebSettings.setAllowFileAccess(true)).called(1); - verify(mockWebView.loadUrl( - 'file:///path/to/file.html', - {}, - )).called(1); - }); + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); - test('loadFile without file prefix and characters to be escaped', () async { - final MockWebView mockWebView = MockWebView(); - final MockWebSettings mockWebSettings = MockWebSettings(); - final AndroidWebViewController controller = createControllerWithMocks( - mockWebView: mockWebView, - mockSettings: mockWebSettings, - ); + test('Without file prefix and characters to be escaped', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); - await controller.loadFile('/path/to/?_<_>_.html'); + await controller.loadFile('/path/to/?_<_>_.html'); - verify(mockWebSettings.setAllowFileAccess(true)).called(1); - verify(mockWebView.loadUrl( - 'file:///path/to/%3F_%3C_%3E_.html', - {}, - )).called(1); + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/%3F_%3C_%3E_.html', + {}, + )).called(1); + }); + + test('With file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + when(mockWebView.settings).thenReturn(mockWebSettings); + + await controller.loadFile('file:///path/to/file.html'); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); }); - test('loadFile with file prefix', () async { - final MockWebView mockWebView = MockWebView(); - final MockWebSettings mockWebSettings = MockWebSettings(); - final AndroidWebViewController controller = createControllerWithMocks( - mockWebView: mockWebView, - ); + group('loadFileWithParams', () { + group('Using LoadFileParams model', () { + test('Without file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); - when(mockWebView.settings).thenReturn(mockWebSettings); + await controller.loadFileWithParams( + const LoadFileParams(absoluteFilePath: '/path/to/file.html'), + ); - await controller.loadFile('file:///path/to/file.html'); + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); - verify(mockWebSettings.setAllowFileAccess(true)).called(1); - verify(mockWebView.loadUrl( - 'file:///path/to/file.html', - {}, - )).called(1); + test('Without file prefix and characters to be escaped', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFileWithParams( + const LoadFileParams(absoluteFilePath: '/path/to/?_<_>_.html'), + ); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/%3F_%3C_%3E_.html', + {}, + )).called(1); + }); + + test('With file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFileWithParams( + const LoadFileParams(absoluteFilePath: 'file:///path/to/file.html'), + ); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); + }); + + group('Using WebKitLoadFileParams model', () { + test('Without file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFileWithParams( + AndroidLoadFileParams(absoluteFilePath: '/path/to/file.html'), + ); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); + + test('Without file prefix and characters to be escaped', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFileWithParams( + AndroidLoadFileParams(absoluteFilePath: '/path/to/?_<_>_.html'), + ); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/%3F_%3C_%3E_.html', + {}, + )).called(1); + }); + + test('With file prefix', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFileWithParams( + AndroidLoadFileParams( + absoluteFilePath: 'file:///path/to/file.html'), + ); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + {}, + )).called(1); + }); + + test('With additional headers', () async { + final MockWebView mockWebView = MockWebView(); + final MockWebSettings mockWebSettings = MockWebSettings(); + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + mockSettings: mockWebSettings, + ); + + await controller.loadFileWithParams( + AndroidLoadFileParams( + absoluteFilePath: 'file:///path/to/file.html', + headers: const { + 'Authorization': 'Bearer test_token', + 'Cache-Control': 'no-cache', + 'X-Custom-Header': 'test-value', + }, + ), + ); + + verify(mockWebSettings.setAllowFileAccess(true)).called(1); + verify(mockWebView.loadUrl( + 'file:///path/to/file.html', + const { + 'Authorization': 'Bearer test_token', + 'Cache-Control': 'no-cache', + 'X-Custom-Header': 'test-value', + }, + )).called(1); + }); + }); }); test('loadFlutterAsset when asset does not exist', () async { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 58e0622917b..4b07bc9e2b9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.23.0 + +* Adds support for `PlatformWebViewController.loadFileWithParams`. +* Introduces `WebKitLoadFileParams`, a platform-specific extension of `LoadFileParams` for iOS and macOS that adds support for `readAccessPath`. + ## 3.22.1 * Changes the handling of a Flutter method failure from throwing an assertion error to logging the diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index 58d92637a46..adb1a66ebde 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter path_provider: ^2.0.6 - webview_flutter_platform_interface: ^2.13.0 + webview_flutter_platform_interface: ^2.14.0 webview_flutter_wkwebview: # When depending on this package from a real application you should use: # webview_flutter: ^x.y.z diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index f3e2cd202f6..039009cef48 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -38,6 +38,39 @@ enum PlaybackMediaTypes { } } +/// Object specifying parameters for loading a local file in a +/// [WebKitWebViewController]. +@immutable +base class WebKitLoadFileParams extends LoadFileParams { + /// Constructs a [WebKitLoadFileParams], the subclass of a [LoadFileParams]. + WebKitLoadFileParams({ + required super.absoluteFilePath, + String? readAccessPath, + }) : readAccessPath = readAccessPath ?? path.dirname(absoluteFilePath), + super(); + + /// Constructs a [WebKitLoadFileParams] using a [LoadFileParams]. + factory WebKitLoadFileParams.fromLoadFileParams( + LoadFileParams params, { + String? readAccessPath, + }) { + return WebKitLoadFileParams( + absoluteFilePath: params.absoluteFilePath, + readAccessPath: readAccessPath, + ); + } + + /// The directory to which the WebView is granted read access. + /// + /// If not provided at initialization time, it defaults to + /// the parent directory of [absoluteFilePath]. + /// + /// On iOS/macOS, this is required by WebKit to define the scope of readable + /// files when loading a local HTML file. It must include the location of + /// any resources (e.g., images, scripts) referenced by the HTML. + final String readAccessPath; +} + /// Object specifying creation parameters for a [WebKitWebViewController]. @immutable class WebKitWebViewControllerCreationParams @@ -405,12 +438,29 @@ class WebKitWebViewController extends PlatformWebViewController { @override Future loadFile(String absoluteFilePath) { - return _webView.loadFileUrl( - absoluteFilePath, - path.dirname(absoluteFilePath), + return loadFileWithParams( + WebKitLoadFileParams(absoluteFilePath: absoluteFilePath), ); } + @override + Future loadFileWithParams( + LoadFileParams params, + ) { + switch (params) { + case final WebKitLoadFileParams params: + return _webView.loadFileUrl( + params.absoluteFilePath, + params.readAccessPath, + ); + + default: + return loadFileWithParams( + WebKitLoadFileParams.fromLoadFileParams(params), + ); + } + } + @override Future loadFlutterAsset(String key) { assert(key.isNotEmpty); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 114667e8392..7ba904f1a30 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.22.1 +version: 3.23.0 environment: sdk: ^3.6.0 @@ -26,7 +26,7 @@ dependencies: sdk: flutter meta: ^1.10.0 path: ^1.8.0 - webview_flutter_platform_interface: ^2.13.0 + webview_flutter_platform_interface: ^2.14.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index 34eff72163f..302ab3bb70e 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -341,6 +341,40 @@ void main() { verify(mockWebView.loadFileUrl('/path/to/file.html', '/path/to')); }); + group('loadFileWithParams', () { + test('Using LoadFileParams model', () async { + final MockUIViewWKWebView mockWebView = MockUIViewWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadFileWithParams( + const LoadFileParams(absoluteFilePath: '/path/to/file.html'), + ); + verify(mockWebView.loadFileUrl('/path/to/file.html', '/path/to')); + }); + + test('Using WebKitLoadFileParams with custom readAccessPath', () async { + final MockUIViewWKWebView mockWebView = MockUIViewWKWebView(); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + ); + + await controller.loadFileWithParams( + WebKitLoadFileParams( + absoluteFilePath: '/path/to/file.html', + readAccessPath: '/path/to/resources/', + ), + ); + verify(mockWebView.loadFileUrl( + '/path/to/file.html', + '/path/to/resources/', + )); + }); + }); + test('loadFlutterAsset', () async { final MockUIViewWKWebView mockWebView = MockUIViewWKWebView();