diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 0c2d9201dfc..50311c69cfc 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.13.0 + +* Adds support to respond to recoverable SSL certificate errors. See `NavigationDelegate(onSSlAuthError)`. + ## 4.12.0 * Adds support to set whether to draw the scrollbar. See diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index 6b157228134..ca1e3b9da9d 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -17,8 +17,8 @@ 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_android: ^4.5.0 - webview_flutter_wkwebview: ^3.21.0 + webview_flutter_android: ^4.7.0 + webview_flutter_wkwebview: ^3.22.0 dev_dependencies: build_runner: ^2.1.5 @@ -27,7 +27,7 @@ dev_dependencies: sdk: flutter integration_test: sdk: flutter - webview_flutter_platform_interface: ^2.12.0 + webview_flutter_platform_interface: ^2.13.0 flutter: uses-material-design: true diff --git a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart index 746caed140d..0256aecda5d 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart @@ -38,8 +38,11 @@ class NavigationDelegate { /// Constructs a [NavigationDelegate]. /// /// {@template webview_fluttter.NavigationDelegate.constructor} - /// `onUrlChange`: invoked when the underlying web view changes to a new url. - /// `onHttpAuthRequest`: invoked when the web view is requesting authentication. + /// **`onUrlChange`:** invoked when the underlying web view changes to a new url. + /// **`onHttpAuthRequest`:** invoked when the web view is requesting authentication. + /// **`onSslAuthError`:** Invoked when the web view receives a recoverable SSL + /// error for a certificate. The host application must call either + /// [SslAuthError.cancel] or [SslAuthError.proceed]. /// {@endtemplate} NavigationDelegate({ FutureOr Function(NavigationRequest request)? @@ -51,6 +54,7 @@ class NavigationDelegate { void Function(UrlChange change)? onUrlChange, void Function(HttpAuthRequest request)? onHttpAuthRequest, void Function(HttpResponseError error)? onHttpError, + void Function(SslAuthError request)? onSslAuthError, }) : this.fromPlatformCreationParams( const PlatformNavigationDelegateCreationParams(), onNavigationRequest: onNavigationRequest, @@ -61,6 +65,7 @@ class NavigationDelegate { onUrlChange: onUrlChange, onHttpAuthRequest: onHttpAuthRequest, onHttpError: onHttpError, + onSslAuthError: onSslAuthError, ); /// Constructs a [NavigationDelegate] from creation params for a specific @@ -105,6 +110,7 @@ class NavigationDelegate { void Function(UrlChange change)? onUrlChange, void Function(HttpAuthRequest request)? onHttpAuthRequest, void Function(HttpResponseError error)? onHttpError, + void Function(SslAuthError request)? onSslAuthError, }) : this.fromPlatform( PlatformNavigationDelegate(params), onNavigationRequest: onNavigationRequest, @@ -115,6 +121,7 @@ class NavigationDelegate { onUrlChange: onUrlChange, onHttpAuthRequest: onHttpAuthRequest, onHttpError: onHttpError, + onSslAuthError: onSslAuthError, ); /// Constructs a [NavigationDelegate] from a specific platform implementation. @@ -130,6 +137,7 @@ class NavigationDelegate { void Function(UrlChange change)? onUrlChange, HttpAuthRequestCallback? onHttpAuthRequest, void Function(HttpResponseError error)? onHttpError, + void Function(SslAuthError request)? onSslAuthError, }) { if (onNavigationRequest != null) { platform.setOnNavigationRequest(onNavigationRequest!); @@ -155,6 +163,13 @@ class NavigationDelegate { if (onHttpError != null) { platform.setOnHttpError(onHttpError); } + if (onSslAuthError != null) { + platform.setOnSSlAuthError( + (PlatformSslAuthError error) { + onSslAuthError(SslAuthError._fromPlatform(error)); + }, + ); + } } /// Implementation of [PlatformNavigationDelegate] for the current platform. @@ -184,3 +199,54 @@ class NavigationDelegate { /// Invoked when a resource loading error occurred. final WebResourceErrorCallback? onWebResourceError; } + +/// Represents an SSL error with the associated certificate. +/// +/// The host application must call [cancel] or, contrary to secure web +/// communication standards, [proceed] to provide the web view's response to the +/// error. [proceed] should generally only be used in test environments, as +/// using it in production can expose users to security and privacy risks. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final SslAuthError error = ...; +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitSslAuthError webKitError = +/// error.platform as WebKitSslAuthError; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidSslAuthError androidError = +/// error.platform as AndroidSslAuthError; +/// } +/// ``` +class SslAuthError { + SslAuthError._fromPlatform(this.platform); + + /// An implementation of [PlatformSslAuthError] for the current platform. + final PlatformSslAuthError platform; + + /// The certificate associated with this error. + X509Certificate? get certificate => platform.certificate; + + /// Instructs the WebView that encountered the SSL certificate error to + /// terminate communication with the server. + /// + /// The host application must call this method to prevent a resource from + /// loading when an SSL certificate is invalid. + Future cancel() => platform.cancel(); + + /// Instructs the WebView that encountered the SSL certificate error to ignore + /// the error and continue communicating with the server. + /// + /// **Warning:** Calling [proceed] in a production environment is strongly + /// discouraged, as an invalid SSL certificate means that the connection is + /// not secure, so proceeding can expose users to security and privacy risks. + Future proceed() => platform.proceed(); +} diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index b1b237d3140..573a3375542 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -36,7 +36,8 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_inte WebViewCredential, WebViewOverScrollMode, WebViewPermissionResourceType, - WebViewPlatform; + WebViewPlatform, + X509Certificate; export 'src/navigation_delegate.dart'; export 'src/webview_controller.dart'; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index 33484f3cead..5e98894c3c1 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -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.12.0 +version: 4.13.0 environment: sdk: ^3.6.0 @@ -21,9 +21,9 @@ flutter: dependencies: flutter: sdk: flutter - webview_flutter_android: ^4.5.0 - webview_flutter_platform_interface: ^2.12.0 - webview_flutter_wkwebview: ^3.21.0 + webview_flutter_android: ^4.7.0 + webview_flutter_platform_interface: ^2.13.0 + webview_flutter_wkwebview: ^3.22.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart index 64f5b16170d..0389b1c9e8e 100644 --- a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart @@ -11,7 +11,11 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'navigation_delegate_test.mocks.dart'; -@GenerateMocks([WebViewPlatform, PlatformNavigationDelegate]) +@GenerateMocks([ + WebViewPlatform, + PlatformNavigationDelegate, + PlatformSslAuthError, +]) void main() { group('NavigationDelegate', () { test('onNavigationRequest', () async { @@ -111,6 +115,28 @@ void main() { verify(delegate.platform.setOnHttpError(onHttpError)); }); + + test('onSslAuthError', () async { + WebViewPlatform.instance = TestWebViewPlatform(); + + final NavigationDelegate delegate = NavigationDelegate( + onSslAuthError: expectAsync1((SslAuthError error) { + error.proceed(); + }), + ); + + final void Function(PlatformSslAuthError) callback = verify( + (delegate.platform as MockPlatformNavigationDelegate) + .setOnSSlAuthError(captureAny)) + .captured + .single as void Function(PlatformSslAuthError); + + final MockPlatformSslAuthError mockPlatformError = + MockPlatformSslAuthError(); + callback(mockPlatformError); + + verify(mockPlatformError.proceed()); + }); }); } diff --git a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart index 4b02c37ace2..7c93bf20b7f 100644 --- a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.mocks.dart @@ -6,8 +6,11 @@ import 'dart:async' as _i8; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i10; import 'package:webview_flutter_platform_interface/src/platform_navigation_delegate.dart' as _i3; +import 'package:webview_flutter_platform_interface/src/platform_ssl_auth_error.dart' + as _i9; import 'package:webview_flutter_platform_interface/src/platform_webview_controller.dart' as _i4; import 'package:webview_flutter_platform_interface/src/platform_webview_cookie_manager.dart' @@ -211,4 +214,47 @@ class MockPlatformNavigationDelegate extends _i1.Mock returnValue: _i8.Future.value(), returnValueForMissingStub: _i8.Future.value(), ) as _i8.Future); + + @override + _i8.Future setOnSSlAuthError( + _i3.SslAuthErrorCallback? onSslAuthError, + ) => + (super.noSuchMethod( + Invocation.method(#setOnSSlAuthError, [onSslAuthError]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); +} + +/// A class which mocks [PlatformSslAuthError]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPlatformSslAuthError extends _i1.Mock + implements _i9.PlatformSslAuthError { + MockPlatformSslAuthError() { + _i1.throwOnMissingStub(this); + } + + @override + String get description => (super.noSuchMethod( + Invocation.getter(#description), + returnValue: _i10.dummyValue( + this, + Invocation.getter(#description), + ), + ) as String); + + @override + _i8.Future proceed() => (super.noSuchMethod( + Invocation.method(#proceed, []), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + + @override + _i8.Future cancel() => (super.noSuchMethod( + Invocation.method(#cancel, []), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); } diff --git a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart index 9c1b7e24961..ef5d46725fd 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_controller_test.mocks.dart @@ -458,4 +458,14 @@ class MockPlatformNavigationDelegate extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + + @override + _i5.Future setOnSSlAuthError( + _i6.SslAuthErrorCallback? onSslAuthError, + ) => + (super.noSuchMethod( + Invocation.method(#setOnSSlAuthError, [onSslAuthError]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart index b404fce0020..55cebdcaf54 100644 --- a/packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_export_test.dart @@ -30,6 +30,7 @@ void main() { main_file.PlatformWebViewPermissionRequest; main_file.PlatformWebViewWidgetCreationParams; main_file.ProgressCallback; + main_file.UrlChange; main_file.WebViewOverScrollMode; main_file.WebViewPermissionResourceType; main_file.WebResourceError; @@ -39,7 +40,7 @@ void main() { main_file.WebResourceErrorType; main_file.WebResourceRequest; main_file.WebResourceResponse; - main_file.UrlChange; + main_file.X509Certificate; }, ); });