diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 85f5838755d..05eb0360a25 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.24.0 + +* Adds support for `javaScriptCanOpenWindowsAutomatically` to allow JavaScript's + `window.open()` to work without user interaction on iOS and macOS. + ## 3.23.5 * Removes internal native library Dart proxy. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationDelegateProxyAPITests.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationDelegateProxyAPITests.swift index 11df2e04329..081bff28828 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationDelegateProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationDelegateProxyAPITests.swift @@ -170,11 +170,12 @@ class TestNavigationDelegateApi: PigeonApiProtocolWKNavigationDelegate { func decidePolicyForNavigationAction( pigeonInstance pigeonInstanceArg: WKNavigationDelegate, webView webViewArg: WKWebView, navigationAction navigationActionArg: WKNavigationAction, - completion: @escaping ( - Result< - webview_flutter_wkwebview.NavigationActionPolicy, webview_flutter_wkwebview.PigeonError - > - ) -> Void + completion: + @escaping ( + Result< + webview_flutter_wkwebview.NavigationActionPolicy, webview_flutter_wkwebview.PigeonError + > + ) -> Void ) { decidePolicyForNavigationActionArgs = [webViewArg, navigationActionArg] completion(.success(.allow)) @@ -183,11 +184,12 @@ class TestNavigationDelegateApi: PigeonApiProtocolWKNavigationDelegate { func decidePolicyForNavigationResponse( pigeonInstance pigeonInstanceArg: WKNavigationDelegate, webView webViewArg: WKWebView, navigationResponse navigationResponseArg: WKNavigationResponse, - completion: @escaping ( - Result< - webview_flutter_wkwebview.NavigationResponsePolicy, webview_flutter_wkwebview.PigeonError - > - ) -> Void + completion: + @escaping ( + Result< + webview_flutter_wkwebview.NavigationResponsePolicy, webview_flutter_wkwebview.PigeonError + > + ) -> Void ) { decidePolicyForNavigationResponseArgs = [webViewArg, navigationResponseArg] completion(.success(.cancel)) @@ -219,12 +221,13 @@ class TestNavigationDelegateApi: PigeonApiProtocolWKNavigationDelegate { func didReceiveAuthenticationChallenge( pigeonInstance pigeonInstanceArg: WKNavigationDelegate, webView webViewArg: WKWebView, challenge challengeArg: URLAuthenticationChallenge, - completion: @escaping ( - Result< - webview_flutter_wkwebview.AuthenticationChallengeResponse, - webview_flutter_wkwebview.PigeonError - > - ) -> Void + completion: + @escaping ( + Result< + webview_flutter_wkwebview.AuthenticationChallengeResponse, + webview_flutter_wkwebview.PigeonError + > + ) -> Void ) { didReceiveAuthenticationChallengeArgs = [webViewArg, challengeArg] completion( @@ -241,7 +244,8 @@ class TestWebView: WKWebView { } } -class TestURLAuthenticationChallengeSender: NSObject, URLAuthenticationChallengeSender, @unchecked +class TestURLAuthenticationChallengeSender: NSObject, URLAuthenticationChallengeSender, + @unchecked Sendable { func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/PreferencesProxyAPITests.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/PreferencesProxyAPITests.swift index 61a25074a28..150f8cd45cb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/PreferencesProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/PreferencesProxyAPITests.swift @@ -24,4 +24,16 @@ class PreferencesProxyAPITests: XCTestCase { XCTAssertEqual(instance.javaScriptEnabled, enabled) } } + + @MainActor func testSetJavaScriptCanOpenWindowsAutomatically() throws { + let registrar = TestProxyApiRegistrar() + let api = registrar.apiDelegate.pigeonApiWKPreferences(registrar) + + let instance = WKPreferences() + let enabled = true + try api.pigeonDelegate.setJavaScriptCanOpenWindowsAutomatically( + pigeonApi: api, pigeonInstance: instance, enabled: enabled) + + XCTAssertEqual(instance.javaScriptCanOpenWindowsAutomatically, enabled) + } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/NavigationDelegateProxyAPIDelegate.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/NavigationDelegateProxyAPIDelegate.swift index ac5f269fff6..7c407e278a6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/NavigationDelegateProxyAPIDelegate.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/NavigationDelegateProxyAPIDelegate.swift @@ -233,7 +233,8 @@ public class NavigationDelegateImpl: NSObject, WKNavigationDelegate { #if compiler(>=6.0) public func webView( _ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping @MainActor (URLSession.AuthChallengeDisposition, URLCredential?) + completionHandler: + @escaping @MainActor (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { @@ -256,7 +257,8 @@ public class NavigationDelegateImpl: NSObject, WKNavigationDelegate { #else public func webView( _ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> + completionHandler: + @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { registrar.dispatchOnMainThread { onFailure in diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/PreferencesProxyAPIDelegate.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/PreferencesProxyAPIDelegate.swift index 53f702455ac..00252a87ff7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/PreferencesProxyAPIDelegate.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/PreferencesProxyAPIDelegate.swift @@ -23,4 +23,10 @@ class PreferencesProxyAPIDelegate: PigeonApiDelegateWKPreferences { pigeonInstance.javaScriptEnabled = enabled } } + + func setJavaScriptCanOpenWindowsAutomatically( + pigeonApi: PigeonApiWKPreferences, pigeonInstance: WKPreferences, enabled: Bool + ) throws { + pigeonInstance.javaScriptCanOpenWindowsAutomatically = enabled + } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/ProxyAPIRegistrar.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/ProxyAPIRegistrar.swift index 131c8ffde7a..c4d446d8020 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/ProxyAPIRegistrar.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/ProxyAPIRegistrar.swift @@ -78,9 +78,10 @@ open class ProxyAPIRegistrar: WebKitLibraryPigeonProxyApiRegistrar { /// Handles calling a Flutter method on the main thread. func dispatchOnMainThread( - execute work: @escaping ( - _ onFailure: @escaping (_ methodName: String, _ error: PigeonError) -> Void - ) -> Void + execute work: + @escaping ( + _ onFailure: @escaping (_ methodName: String, _ error: PigeonError) -> Void + ) -> Void ) { DispatchQueue.main.async { work { methodName, error in diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/WebKitLibrary.g.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/WebKitLibrary.g.swift index d0bea7528c0..04e46a4282d 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/WebKitLibrary.g.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/WebKitLibrary.g.swift @@ -3917,6 +3917,15 @@ protocol PigeonApiDelegateWKPreferences { /// A Boolean value that indicates whether JavaScript is enabled. func setJavaScriptEnabled( pigeonApi: PigeonApiWKPreferences, pigeonInstance: WKPreferences, enabled: Bool) throws + /// A Boolean value that indicates whether JavaScript can open windows without user interaction. + /// + /// The default value is `false` on iOS and `true` on macOS. + /// Set to `true` to allow JavaScript to open windows automatically + /// through `window.open()` calls without requiring user gestures. + /// + /// See https://developer.apple.com/documentation/webkit/wkpreferences/javascriptcanopenwindowsautomatically + func setJavaScriptCanOpenWindowsAutomatically( + pigeonApi: PigeonApiWKPreferences, pigeonInstance: WKPreferences, enabled: Bool) throws } protocol PigeonApiProtocolWKPreferences { @@ -3964,6 +3973,26 @@ final class PigeonApiWKPreferences: PigeonApiProtocolWKPreferences { } else { setJavaScriptEnabledChannel.setMessageHandler(nil) } + let setJavaScriptCanOpenWindowsAutomaticallyChannel = FlutterBasicMessageChannel( + name: + "dev.flutter.pigeon.webview_flutter_wkwebview.WKPreferences.setJavaScriptCanOpenWindowsAutomatically", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + setJavaScriptCanOpenWindowsAutomaticallyChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let pigeonInstanceArg = args[0] as! WKPreferences + let enabledArg = args[1] as! Bool + do { + try api.pigeonDelegate.setJavaScriptCanOpenWindowsAutomatically( + pigeonApi: api, pigeonInstance: pigeonInstanceArg, enabled: enabledArg) + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + setJavaScriptCanOpenWindowsAutomaticallyChannel.setMessageHandler(nil) + } } ///Creates a Dart instance of WKPreferences and attaches it to [pigeonInstance]. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index 82dd5a93f8e..aa88dd6b957 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -4687,6 +4687,41 @@ class WKPreferences extends NSObject { } } + /// A Boolean value that indicates whether JavaScript can open windows without user interaction. + /// + /// The default value is `false` on iOS and `true` on macOS. + /// Set to `true` to allow JavaScript to open windows automatically + /// through `window.open()` calls without requiring user gestures. + /// + /// See https://developer.apple.com/documentation/webkit/wkpreferences/javascriptcanopenwindowsautomatically + Future setJavaScriptCanOpenWindowsAutomatically(bool enabled) async { + final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = + _pigeonVar_codecWKPreferences; + final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger; + const pigeonVar_channelName = + 'dev.flutter.pigeon.webview_flutter_wkwebview.WKPreferences.setJavaScriptCanOpenWindowsAutomatically'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [this, enabled], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + @override WKPreferences pigeon_copy() { return WKPreferences.pigeon_detached( 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 6eda612ef31..c21f11eb5be 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 @@ -82,6 +82,7 @@ class WebKitWebViewControllerCreationParams }, this.allowsInlineMediaPlayback = false, this.limitsNavigationsToAppBoundDomains = false, + this.javaScriptCanOpenWindowsAutomatically, }) { _configuration = WKWebViewConfiguration(); @@ -122,10 +123,13 @@ class WebKitWebViewControllerCreationParams }, bool allowsInlineMediaPlayback = false, bool limitsNavigationsToAppBoundDomains = false, + bool? javaScriptCanOpenWindowsAutomatically, }) : this( mediaTypesRequiringUserAction: mediaTypesRequiringUserAction, allowsInlineMediaPlayback: allowsInlineMediaPlayback, limitsNavigationsToAppBoundDomains: limitsNavigationsToAppBoundDomains, + javaScriptCanOpenWindowsAutomatically: + javaScriptCanOpenWindowsAutomatically, ); late final WKWebViewConfiguration _configuration; @@ -147,6 +151,18 @@ class WebKitWebViewControllerCreationParams /// (Only available for iOS > 14.0) /// Defaults to false. final bool limitsNavigationsToAppBoundDomains; + + /// Whether JavaScript can open windows without user interaction. + /// + /// Setting this to `true` allows JavaScript's `window.open()` to create + /// new windows automatically without requiring a user gesture. + /// + /// When `null`, the platform's native default is used: + /// - iOS: `false` + /// - macOS: `true` + /// + /// See https://developer.apple.com/documentation/webkit/wkpreferences/1536573-javascriptcanopenwindowsautomati + final bool? javaScriptCanOpenWindowsAutomatically; } /// An implementation of [PlatformWebViewController] with the WebKit api. @@ -643,6 +659,16 @@ class WebKitWebViewController extends PlatformWebViewController { case JavaScriptMode.unrestricted: await webpagePreferences.setAllowsContentJavaScript(true); } + // Set javaScriptCanOpenWindowsAutomatically on WKPreferences only if explicitly set + final bool? javaScriptCanOpenWindowsAutomatically = + _webKitParams.javaScriptCanOpenWindowsAutomatically; + if (javaScriptCanOpenWindowsAutomatically != null) { + final WKPreferences preferences = await _webView.configuration + .getPreferences(); + await preferences.setJavaScriptCanOpenWindowsAutomatically( + javaScriptCanOpenWindowsAutomatically, + ); + } return; } on PlatformException catch (exception) { if (exception.code != 'PigeonUnsupportedOperationError') { @@ -660,6 +686,13 @@ class WebKitWebViewController extends PlatformWebViewController { case JavaScriptMode.unrestricted: await preferences.setJavaScriptEnabled(true); } + final bool? javaScriptCanOpenWindowsAutomatically = + _webKitParams.javaScriptCanOpenWindowsAutomatically; + if (javaScriptCanOpenWindowsAutomatically != null) { + await preferences.setJavaScriptCanOpenWindowsAutomatically( + javaScriptCanOpenWindowsAutomatically, + ); + } } @override diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index 51b6e680a32..1fc635c7651 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -747,6 +747,15 @@ abstract class WKUserContentController extends NSObject { abstract class WKPreferences extends NSObject { /// A Boolean value that indicates whether JavaScript is enabled. void setJavaScriptEnabled(bool enabled); + + /// A Boolean value that indicates whether JavaScript can open windows without user interaction. + /// + /// The default value is `false` on iOS and `true` on macOS. + /// Set to `true` to allow JavaScript to open windows automatically + /// through `window.open()` calls without requiring user gestures. + /// + /// See https://developer.apple.com/documentation/webkit/wkpreferences/javascriptcanopenwindowsautomatically + void setJavaScriptCanOpenWindowsAutomatically(bool enabled); } /// An interface for receiving messages from JavaScript code running in a webpage. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 714304d9b34..7217c1c5504 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.23.5 +version: 3.24.0 environment: sdk: ^3.9.0 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart index c230dfacf48..575b652e1f4 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/legacy/web_kit_webview_widget_test.mocks.dart @@ -565,6 +565,17 @@ class MockWKPreferences extends _i1.Mock implements _i2.WKPreferences { ) as _i4.Future); + @override + _i4.Future setJavaScriptCanOpenWindowsAutomatically(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setJavaScriptCanOpenWindowsAutomatically, [ + enabled, + ]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); + @override _i2.WKPreferences pigeon_copy() => (super.noSuchMethod( 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 579e84a2233..4720bab470a 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 @@ -67,6 +67,7 @@ void main() { MockURLRequest Function({required String url, dynamic observeValue})? createURLRequest, MockWKWebpagePreferences? mockWebpagePreferences, + bool? javaScriptCanOpenWindowsAutomatically, }) { final MockWKWebViewConfiguration nonNullMockWebViewConfiguration = mockWebViewConfiguration ?? MockWKWebViewConfiguration(); @@ -190,7 +191,10 @@ void main() { ); }; final PlatformWebViewControllerCreationParams controllerCreationParams = - WebKitWebViewControllerCreationParams(); + WebKitWebViewControllerCreationParams( + javaScriptCanOpenWindowsAutomatically: + javaScriptCanOpenWindowsAutomatically, + ); final controller = WebKitWebViewController(controllerCreationParams); @@ -870,6 +874,43 @@ void main() { }, ); + test( + 'setJavaScriptMode sets javaScriptCanOpenWindowsAutomatically from creation params', + () async { + final mockPreferences = MockWKPreferences(); + final mockWebpagePreferences = MockWKWebpagePreferences(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockPreferences: mockPreferences, + mockWebpagePreferences: mockWebpagePreferences, + javaScriptCanOpenWindowsAutomatically: true, + ); + + await controller.setJavaScriptMode(JavaScriptMode.unrestricted); + + verify(mockPreferences.setJavaScriptCanOpenWindowsAutomatically(true)); + }, + ); + + test( + 'setJavaScriptMode does not set javaScriptCanOpenWindowsAutomatically when null', + () async { + final mockPreferences = MockWKPreferences(); + final mockWebpagePreferences = MockWKWebpagePreferences(); + + final WebKitWebViewController controller = createControllerWithMocks( + mockPreferences: mockPreferences, + mockWebpagePreferences: mockWebpagePreferences, + ); + + await controller.setJavaScriptMode(JavaScriptMode.unrestricted); + + verifyNever( + mockPreferences.setJavaScriptCanOpenWindowsAutomatically(any), + ); + }, + ); + test('clearCache', () { final mockWebsiteDataStore = MockWKWebsiteDataStore(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart index 54a4cd6fe77..1934d6b8d50 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart @@ -564,6 +564,17 @@ class MockWKPreferences extends _i1.Mock implements _i2.WKPreferences { ) as _i3.Future); + @override + _i3.Future setJavaScriptCanOpenWindowsAutomatically(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setJavaScriptCanOpenWindowsAutomatically, [ + enabled, + ]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) + as _i3.Future); + @override _i2.WKPreferences pigeon_copy() => (super.noSuchMethod(