diff --git a/AUTHORS b/AUTHORS index 253669739d88..e4d0b0b5763c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -86,3 +86,4 @@ LeanCode Piotr Denert Marcin Chudy Paweł Jakubowski +Mateusz Jabłoński diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index bae44ba8f51a..4bb10f010213 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.15.0 + +* Adds NavigationDelegate.onCreateWindow for target=_blank / window.open. + ## 4.14.1 * Adds documentation for `NavigationDelegate` callback parameters. diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml index b82d30fb132e..55d783ab3ebb 100644 --- a/packages/webview_flutter/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -35,3 +35,9 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_android: {path: ../../../../packages/webview_flutter/webview_flutter_android} + webview_flutter_platform_interface: {path: ../../../../packages/webview_flutter/webview_flutter_platform_interface} + webview_flutter_wkwebview: {path: ../../../../packages/webview_flutter/webview_flutter_wkwebview} 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 a29eb0f4e1d4..bfdfe2352eef 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart @@ -39,6 +39,9 @@ class NavigationDelegate { /// /// {@template webview_fluttter.NavigationDelegate.constructor} /// **`onNavigationRequest`:** invoked when a navigation request is pending. + /// **`onCreateWindow`:** invoked when the page opens a new window + /// (`target=_blank` / `window.open`). The host should handle the URL; the + /// WebView will not load it when this callback is set. /// **`onPageStarted`:** invoked when a page starts loading. /// **`onPageFinished`:** invoked when a page finishes loading. /// **`onProgress`:** invoked when page loading progress changes. @@ -52,6 +55,7 @@ class NavigationDelegate { /// {@endtemplate} NavigationDelegate({ FutureOr Function(NavigationRequest request)? onNavigationRequest, + void Function(String url)? onCreateWindow, void Function(String url)? onPageStarted, void Function(String url)? onPageFinished, void Function(int progress)? onProgress, @@ -63,6 +67,7 @@ class NavigationDelegate { }) : this.fromPlatformCreationParams( const PlatformNavigationDelegateCreationParams(), onNavigationRequest: onNavigationRequest, + onCreateWindow: onCreateWindow, onPageStarted: onPageStarted, onPageFinished: onPageFinished, onProgress: onProgress, @@ -107,6 +112,7 @@ class NavigationDelegate { NavigationDelegate.fromPlatformCreationParams( PlatformNavigationDelegateCreationParams params, { FutureOr Function(NavigationRequest request)? onNavigationRequest, + void Function(String url)? onCreateWindow, void Function(String url)? onPageStarted, void Function(String url)? onPageFinished, void Function(int progress)? onProgress, @@ -118,6 +124,7 @@ class NavigationDelegate { }) : this.fromPlatform( PlatformNavigationDelegate(params), onNavigationRequest: onNavigationRequest, + onCreateWindow: onCreateWindow, onPageStarted: onPageStarted, onPageFinished: onPageFinished, onProgress: onProgress, @@ -134,6 +141,7 @@ class NavigationDelegate { NavigationDelegate.fromPlatform( this.platform, { this.onNavigationRequest, + this.onCreateWindow, this.onPageStarted, this.onPageFinished, this.onProgress, @@ -146,6 +154,9 @@ class NavigationDelegate { if (onNavigationRequest != null) { platform.setOnNavigationRequest(onNavigationRequest!); } + if (onCreateWindow != null) { + platform.setOnCreateWindow(onCreateWindow!); + } if (onPageStarted != null) { platform.setOnPageStarted(onPageStarted!); } @@ -189,6 +200,12 @@ class NavigationDelegate { /// See [NavigationDecision]. final NavigationRequestCallback? onNavigationRequest; + /// Invoked when the page requests a new window (`target=_blank` / `window.open`). + /// + /// When set, the URL is not loaded in the WebView; the host should handle it + /// (e.g. open the system browser). + final CreateWindowCallback? onCreateWindow; + /// Invoked when a page has started loading. final PageEventCallback? onPageStarted; diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart index 9e1d8082ffb2..4f8b33733714 100644 --- a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -4,6 +4,7 @@ export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' show + CreateWindowCallback, HttpAuthRequest, HttpResponseError, HttpResponseErrorCallback, diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index a8bfde8791cd..03bd2b51deef 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.14.1 +version: 4.15.0 environment: sdk: ^3.10.0 @@ -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.14.0 + webview_flutter_platform_interface: ^2.16.0 + webview_flutter_wkwebview: ^3.27.0 dev_dependencies: build_runner: ^2.1.5 @@ -36,3 +36,9 @@ topics: - html - webview - webview-flutter +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_android: {path: ../../../packages/webview_flutter/webview_flutter_android} + webview_flutter_platform_interface: {path: ../../../packages/webview_flutter/webview_flutter_platform_interface} + webview_flutter_wkwebview: {path: ../../../packages/webview_flutter/webview_flutter_wkwebview} 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 6071a892af4e..2932420e9781 100644 --- a/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter/test/navigation_delegate_test.dart @@ -26,6 +26,16 @@ void main() { verify(delegate.platform.setOnNavigationRequest(onNavigationRequest)); }); + test('onCreateWindow', () async { + WebViewPlatform.instance = TestWebViewPlatform(); + + void onCreateWindow(String url) {} + + final delegate = NavigationDelegate(onCreateWindow: onCreateWindow); + + verify(delegate.platform.setOnCreateWindow(onCreateWindow)); + }); + test('onPageStarted', () async { WebViewPlatform.instance = TestWebViewPlatform(); 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 28982b7550aa..28381bebc2d3 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 @@ -148,6 +148,15 @@ class MockPlatformNavigationDelegate extends _i1.Mock implements _i3.PlatformNav ) as _i8.Future); + @override + _i8.Future setOnCreateWindow(_i3.CreateWindowCallback? onCreateWindow) => + (super.noSuchMethod( + Invocation.method(#setOnCreateWindow, [onCreateWindow]), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) + as _i8.Future); + @override _i8.Future setOnPageStarted(_i3.PageEventCallback? onPageStarted) => (super.noSuchMethod( diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 3ece3a82e1df..816027c0ac96 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.14.0 + +* Adds NavigationDelegate.onCreateWindow for target=_blank / window.open. + ## 4.13.0 * Adds new method for accessing a native `WebView` from a `FlutterPluginBinding`. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt index e5b528d49079..caec5e5bde17 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/AndroidWebkitLibrary.g.kt @@ -3566,6 +3566,45 @@ abstract class PigeonApiWebViewClient( } } + /** Notifies the host that the page requested a new window (`target=_blank` / `window.open`). */ + fun onCreateWindow( + pigeon_instanceArg: android.webkit.WebViewClient, + urlArg: String, + callback: (Result) -> Unit + ) { + if (pigeonRegistrar.ignoreCallsToDart) { + callback( + Result.failure( + AndroidWebKitError("ignore-calls-error", "Calls to Dart are being ignored.", ""))) + return + } else if (!pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) { + callback( + Result.failure( + AndroidWebKitError( + "missing-instance-error", + "Callback to `WebViewClient.onCreateWindow` failed because native instance was not in the instance manager.", + ""))) + return + } + val binaryMessenger = pigeonRegistrar.binaryMessenger + val codec = pigeonRegistrar.codec + val channelName = "dev.flutter.pigeon.webview_flutter_android.WebViewClient.onCreateWindow" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(pigeon_instanceArg, urlArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback( + Result.failure( + AndroidWebKitError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(AndroidWebkitLibraryPigeonUtils.createConnectionError(channelName))) + } + } + } + /** Notify the host application to update its visited links database. */ fun doUpdateVisitedHistory( pigeon_instanceArg: android.webkit.WebViewClient, diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientProxyApi.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientProxyApi.java index 184661e251a1..bb16253b7c15 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientProxyApi.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientProxyApi.java @@ -274,13 +274,18 @@ boolean onCreateWindow( return false; } + // Forward new-window navigations (`target=_blank` / `window.open`) to Dart + // via onCreateWindow — same role as WebChromeClient.onCreateWindow. final WebViewClient windowWebViewClient = new WebViewClient() { @Override public boolean shouldOverrideUrlLoading( @NonNull WebView windowWebView, @NonNull WebResourceRequest request) { - if (!webViewClient.shouldOverrideUrlLoading(view, request)) { - view.loadUrl(request.getUrl().toString()); + final String url = request.getUrl().toString(); + if (webViewClient instanceof WebViewClientProxyApi.WebViewClientImpl) { + ((WebViewClientProxyApi.WebViewClientImpl) webViewClient).notifyCreateWindow(url); + } else if (!webViewClient.shouldOverrideUrlLoading(view, request)) { + view.loadUrl(url); } return true; } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientProxyApi.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientProxyApi.java index 5690b962c9fd..fd000918eec3 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientProxyApi.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewClientProxyApi.java @@ -35,6 +35,11 @@ public WebViewClientImpl(@NonNull WebViewClientProxyApi api) { this.api = api; } + /** Notifies Dart that the page requested a new window (`target=_blank` / `window.open`). */ + public void notifyCreateWindow(@NonNull String url) { + api.getPigeonRegistrar().runOnMainThread(() -> api.onCreateWindow(this, url, reply -> null)); + } + @Override public void onPageStarted(@NonNull WebView view, @NonNull String url, @NonNull Bitmap favicon) { api.getPigeonRegistrar() diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index 2fe120907732..2fa44f89fbb0 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -89,6 +89,42 @@ public void onCreateWindow() { verify(mockWebView).loadUrl("https://www.google.com"); } + @Test + public void onCreateWindowWithWebViewClientImplNotifiesDart() { + final WebViewClientProxyApi mockWebViewClientApi = mock(WebViewClientProxyApi.class); + when(mockWebViewClientApi.getPigeonRegistrar()).thenReturn(new TestProxyApiRegistrar()); + final WebViewClientProxyApi.WebViewClientImpl webViewClient = + new WebViewClientProxyApi.WebViewClientImpl(mockWebViewClientApi); + + final WebView mockOnCreateWindowWebView = mock(WebView.class); + final Message message = new Message(); + message.obj = mock(WebViewTransport.class); + + final WebChromeClientProxyApi mockApi = mock(WebChromeClientProxyApi.class); + final WebChromeClientImpl instance = new WebChromeClientImpl(mockApi); + + final WebView mockWebView = mock(WebView.class); + instance.setWebViewClient(webViewClient); + assertTrue(instance.onCreateWindow(mockWebView, message, mockOnCreateWindowWebView)); + + final ArgumentCaptor webViewClientCaptor = + ArgumentCaptor.forClass(WebViewClient.class); + verify(mockOnCreateWindowWebView).setWebViewClient(webViewClientCaptor.capture()); + final WebViewClient onCreateWindowWebViewClient = webViewClientCaptor.getValue(); + assertNotNull(onCreateWindowWebViewClient); + + final WebResourceRequest mockRequest = mock(WebResourceRequest.class); + when(mockRequest.getUrl()).thenReturn(mock(Uri.class)); + when(mockRequest.getUrl().toString()).thenReturn("https://www.google.com"); + + assertTrue( + onCreateWindowWebViewClient.shouldOverrideUrlLoading( + mockOnCreateWindowWebView, mockRequest)); + verify(mockWebViewClientApi) + .onCreateWindow(eq(webViewClient), eq("https://www.google.com"), any()); + verify(mockWebView, never()).loadUrl(any()); + } + @Test public void onPermissionRequest() { final WebChromeClientProxyApi mockApi = mock(WebChromeClientProxyApi.class); diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java index 6ad5d70a8702..e2a59ca5089b 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java @@ -37,6 +37,18 @@ public void onPageStarted() { verify(mockApi).onPageStarted(eq(instance), eq(webView), eq(url), any()); } + @Test + public void onCreateWindow() { + final WebViewClientProxyApi mockApi = mock(WebViewClientProxyApi.class); + when(mockApi.getPigeonRegistrar()).thenReturn(new TestProxyApiRegistrar()); + + final WebViewClientImpl instance = new WebViewClientImpl(mockApi); + final String url = "https://www.google.com"; + instance.notifyCreateWindow(url); + + verify(mockApi).onCreateWindow(eq(instance), eq(url), any()); + } + @Test public void onReceivedError() { final WebViewClientProxyApi mockApi = mock(WebViewClientProxyApi.class); diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test_legacy.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test_legacy.dart index d123e3abedeb..1890898af903 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test_legacy.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test_legacy.dart @@ -101,26 +101,28 @@ Future main() async { ); }); - testWidgets('withWeakRefenceTo allows encapsulating class to be garbage collected', ( - WidgetTester tester, - ) async { - final gcCompleter = Completer(); - final instanceManager = android.PigeonInstanceManager( - onWeakReferenceRemoved: gcCompleter.complete, - ); + testWidgets( + 'withWeakRefenceTo allows encapsulating class to be garbage collected', + (WidgetTester tester) async { + final gcCompleter = Completer(); + final instanceManager = android.PigeonInstanceManager( + onWeakReferenceRemoved: gcCompleter.complete, + ); - ClassWithCallbackClass? instance = ClassWithCallbackClass(); - instanceManager.addHostCreatedInstance(instance.callbackClass, 0); - instance = null; + ClassWithCallbackClass? instance = ClassWithCallbackClass(); + instanceManager.addHostCreatedInstance(instance.callbackClass, 0); + instance = null; - // Force garbage collection. - await IntegrationTestWidgetsFlutterBinding.instance.watchPerformance(() async { - await tester.pumpAndSettle(); - }); + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance.watchPerformance(() async { + await tester.pumpAndSettle(); + }); - final int gcIdentifier = await gcCompleter.future; - expect(gcIdentifier, 0); - }, timeout: const Timeout(Duration(seconds: 10))); + final int gcIdentifier = await gcCompleter.future; + expect(gcIdentifier, 0); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); // TODO(bparrishMines): This test is skipped because of // https://github.com/flutter/flutter/issues/123327 diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 42c9aa76eebb..729628d932da 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -33,3 +33,7 @@ flutter: - assets/sample_video.mp4 - assets/www/index.html - assets/www/styles/style.css +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_platform_interface: {path: ../../../../packages/webview_flutter/webview_flutter_platform_interface} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart index 5b05123527d4..c13fa18aba01 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webkit.g.dart @@ -100,6 +100,7 @@ class PigeonOverrides { void Function(WebViewClient pigeon_instance, WebView webView, WebResourceRequest request)? requestLoading, void Function(WebViewClient pigeon_instance, WebView webView, String url)? urlLoading, + void Function(WebViewClient pigeon_instance, String url)? onCreateWindow, void Function(WebViewClient pigeon_instance, WebView webView, String url, bool isReload)? doUpdateVisitedHistory, void Function( @@ -2713,6 +2714,7 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { void Function(WebViewClient pigeon_instance, WebView webView, WebResourceRequest request)? requestLoading, void Function(WebViewClient pigeon_instance, WebView webView, String url)? urlLoading, + void Function(WebViewClient pigeon_instance, String url)? onCreateWindow, void Function(WebViewClient pigeon_instance, WebView webView, String url, bool isReload)? doUpdateVisitedHistory, void Function( @@ -2761,6 +2763,7 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { onReceivedRequestErrorCompat: onReceivedRequestErrorCompat, requestLoading: requestLoading, urlLoading: urlLoading, + onCreateWindow: onCreateWindow, doUpdateVisitedHistory: doUpdateVisitedHistory, onReceivedHttpAuthRequest: onReceivedHttpAuthRequest, onFormResubmission: onFormResubmission, @@ -2782,6 +2785,7 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { onReceivedRequestErrorCompat: onReceivedRequestErrorCompat, requestLoading: requestLoading, urlLoading: urlLoading, + onCreateWindow: onCreateWindow, doUpdateVisitedHistory: doUpdateVisitedHistory, onReceivedHttpAuthRequest: onReceivedHttpAuthRequest, onFormResubmission: onFormResubmission, @@ -2805,6 +2809,7 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { this.onReceivedRequestErrorCompat, this.requestLoading, this.urlLoading, + this.onCreateWindow, this.doUpdateVisitedHistory, this.onReceivedHttpAuthRequest, this.onFormResubmission, @@ -2850,6 +2855,7 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { this.onReceivedRequestErrorCompat, this.requestLoading, this.urlLoading, + this.onCreateWindow, this.doUpdateVisitedHistory, this.onReceivedHttpAuthRequest, this.onFormResubmission, @@ -3033,6 +3039,28 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { /// release the associated Native object manually. final void Function(WebViewClient pigeon_instance, WebView webView, String url)? urlLoading; + /// Notifies the host that the page requested a new window + /// (`target=_blank` / `window.open`). + /// + /// For the associated Native object to be automatically garbage collected, + /// it is required that the implementation of this `Function` doesn't have a + /// strong reference to the encapsulating class instance. When this `Function` + /// references a non-local variable, it is strongly recommended to access it + /// with a `WeakReference`: + /// + /// ```dart + /// final WeakReference weakMyVariable = WeakReference(myVariable); + /// final WebViewClient instance = WebViewClient( + /// onCreateWindow: (WebViewClient pigeon_instance, ...) { + /// print(weakMyVariable?.target); + /// }, + /// ); + /// ``` + /// + /// Alternatively, [PigeonInstanceManager.removeWeakReference] can be used to + /// release the associated Native object manually. + final void Function(WebViewClient pigeon_instance, String url)? onCreateWindow; + /// Notify the host application to update its visited links database. /// /// For the associated Native object to be automatically garbage collected, @@ -3294,6 +3322,7 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { void Function(WebViewClient pigeon_instance, WebView webView, WebResourceRequest request)? requestLoading, void Function(WebViewClient pigeon_instance, WebView webView, String url)? urlLoading, + void Function(WebViewClient pigeon_instance, String url)? onCreateWindow, void Function(WebViewClient pigeon_instance, WebView webView, String url, bool isReload)? doUpdateVisitedHistory, void Function( @@ -3596,6 +3625,36 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { } } + { + final pigeonVar_channel = BasicMessageChannel( + 'dev.flutter.pigeon.webview_flutter_android.WebViewClient.onCreateWindow', + pigeonChannelCodec, + binaryMessenger: binaryMessenger, + ); + if (pigeon_clearHandlers) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final WebViewClient arg_pigeon_instance = args[0]! as WebViewClient; + final String arg_url = args[1]! as String; + try { + (onCreateWindow ?? arg_pigeon_instance.onCreateWindow)?.call( + arg_pigeon_instance, + arg_url, + ); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString()), + ); + } + }); + } + } + { final pigeonVar_channel = BasicMessageChannel( 'dev.flutter.pigeon.webview_flutter_android.WebViewClient.doUpdateVisitedHistory', @@ -3941,6 +4000,7 @@ class WebViewClient extends PigeonInternalProxyApiBaseClass { onReceivedRequestErrorCompat: onReceivedRequestErrorCompat, requestLoading: requestLoading, urlLoading: urlLoading, + onCreateWindow: onCreateWindow, doUpdateVisitedHistory: doUpdateVisitedHistory, onReceivedHttpAuthRequest: onReceivedHttpAuthRequest, onFormResubmission: onFormResubmission, 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 70d8a0aac5ff..77e199d44c59 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 @@ -1452,6 +1452,15 @@ class AndroidNavigationDelegate extends PlatformNavigationDelegate { isForMainFrame: request.isForMainFrame, ); }, + onCreateWindow: (_, String url) { + final CreateWindowCallback? callback = weakThis.target?._onCreateWindow; + if (callback != null) { + callback(url); + } else { + // Upstream default: load new-window URLs in the same WebView. + weakThis.target?._handleNavigation(url, isForMainFrame: true); + } + }, urlLoading: (_, android_webview.WebView webView, String url) { weakThis.target?._handleNavigation(url, isForMainFrame: true); }, @@ -1560,6 +1569,7 @@ class AndroidNavigationDelegate extends PlatformNavigationDelegate { ProgressCallback? _onProgress; WebResourceErrorCallback? _onWebResourceError; NavigationRequestCallback? _onNavigationRequest; + CreateWindowCallback? _onCreateWindow; LoadRequestCallback? _onLoadRequest; UrlChangeCallback? _onUrlChange; HttpAuthRequestCallback? _onHttpAuthRequest; @@ -1605,6 +1615,11 @@ class AndroidNavigationDelegate extends PlatformNavigationDelegate { return _webViewClient.setSynchronousReturnValueForShouldOverrideUrlLoading(true); } + @override + Future setOnCreateWindow(CreateWindowCallback onCreateWindow) async { + _onCreateWindow = onCreateWindow; + } + @override Future setOnPageStarted(PageEventCallback onPageStarted) async { _onPageStarted = onPageStarted; diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart index d4f758224ca3..3ddb26aa9202 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webkit.dart @@ -490,6 +490,10 @@ abstract class WebViewClient { /// be loaded in the current WebView. late void Function(WebView webView, String url)? urlLoading; + /// Notifies the host that the page requested a new window + /// (`target=_blank` / `window.open`). + late void Function(String url)? onCreateWindow; + /// Notify the host application to update its visited links database. late void Function(WebView webView, String url, bool isReload)? doUpdateVisitedHistory; diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 0aa602bf8976..0887a3a100b0 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.13.0 +version: 4.14.0 environment: sdk: ^3.12.0 @@ -21,7 +21,7 @@ dependencies: flutter: sdk: flutter meta: ^1.10.0 - webview_flutter_platform_interface: ^2.15.1 + webview_flutter_platform_interface: ^2.16.0 dev_dependencies: build_runner: ^2.1.4 @@ -34,3 +34,7 @@ topics: - html - webview - webview-flutter +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_platform_interface: {path: ../../../packages/webview_flutter/webview_flutter_platform_interface} diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 2e6873c26b36..c903c4df0dfc 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -42,6 +42,20 @@ void main() { expect(callbackUrl, 'https://www.google.com'); }); + test('onCreateWindow', () { + final androidNavigationDelegate = AndroidNavigationDelegate(_buildCreationParams()); + + late final String callbackUrl; + androidNavigationDelegate.setOnCreateWindow((String url) => callbackUrl = url); + + CapturingWebViewClient.lastCreatedDelegate.onCreateWindow!( + CapturingWebViewClient(), + 'https://www.google.com', + ); + + expect(callbackUrl, 'https://www.google.com'); + }); + test('onPageStarted', () { final androidNavigationDelegate = AndroidNavigationDelegate(_buildCreationParams()); @@ -660,6 +674,7 @@ class CapturingWebViewClient extends android_webview.WebViewClient { super.onReceivedRequestError, super.requestLoading, super.urlLoading, + super.onCreateWindow, super.onFormResubmission, super.onLoadResource, super.onPageCommitVisible, 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 12cb351323f7..58fd7f051591 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 @@ -198,6 +198,7 @@ void main() { )? requestLoading, void Function(android_webview.WebViewClient, android_webview.WebView, String)? urlLoading, + void Function(android_webview.WebViewClient, String)? onCreateWindow, void Function(android_webview.WebViewClient, android_webview.WebView, String, bool)? doUpdateVisitedHistory, void Function( @@ -2295,6 +2296,7 @@ class TestWebViewClient extends android_webview.WebViewClient { super.onReceivedRequestErrorCompat, super.requestLoading, super.urlLoading, + super.onCreateWindow, super.doUpdateVisitedHistory, super.onReceivedHttpAuthRequest, super.onFormResubmission, diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 8325f522befa..e5cbbc9cb8c1 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.16.0 +* Adds NavigationDelegate.onCreateWindow for target=_blank / window.open. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.15.1 diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart index 3c7405c572bc..c19d80795915 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_navigation_delegate.dart @@ -15,6 +15,14 @@ import 'webview_platform.dart' show WebViewPlatform; typedef NavigationRequestCallback = FutureOr Function(NavigationRequest navigationRequest); +/// Signature for callbacks when the page requests a new window +/// (`target=_blank` / `window.open`). +/// +/// Corresponds to platform APIs like iOS `WKUIDelegate.createWebViewWith` and +/// Android `WebChromeClient.onCreateWindow`. The host should open [url] +/// externally (or otherwise handle it); the WebView will not load it. +typedef CreateWindowCallback = void Function(String url); + /// Signature for callbacks that report page events triggered by the native web view. typedef PageEventCallback = void Function(String url); @@ -82,6 +90,13 @@ abstract class PlatformNavigationDelegate extends PlatformInterface { throw UnimplementedError('setOnNavigationRequest is not implemented on the current platform.'); } + /// Invoked when the page requests a new window (`target=_blank` / `window.open`). + /// + /// See [PlatformWebViewController.setPlatformNavigationDelegate]. + Future setOnCreateWindow(CreateWindowCallback onCreateWindow) { + throw UnimplementedError('setOnCreateWindow is not implemented on the current platform.'); + } + /// Invoked when a page has started loading. /// /// See [PlatformWebViewController.setPlatformNavigationDelegate]. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 2d79e0281639..3aaac01d3045 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/webview_flutt issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.15.1 +version: 2.16.0 environment: sdk: ^3.10.0 diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_navigation_delegate_test.dart index 7f94537d6ff7..bd5ced32f26c 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/test/platform_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/platform_navigation_delegate_test.dart @@ -63,6 +63,14 @@ void main() { ); }); + test('Default implementation of setOnCreateWindow should throw unimplemented error', () { + final PlatformNavigationDelegate callbackDelegate = ExtendsPlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + ); + + expect(() => callbackDelegate.setOnCreateWindow((String url) {}), throwsUnimplementedError); + }); + test('Default implementation of setOnPageStarted should throw unimplemented error', () { final PlatformNavigationDelegate callbackDelegate = ExtendsPlatformNavigationDelegate( const PlatformNavigationDelegateCreationParams(), diff --git a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml index 15a46df8cf66..4818255b428e 100644 --- a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml @@ -29,3 +29,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_platform_interface: {path: ../../../../packages/webview_flutter/webview_flutter_platform_interface} diff --git a/packages/webview_flutter/webview_flutter_web/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/pubspec.yaml index d1e13da8162d..116a779f3936 100644 --- a/packages/webview_flutter/webview_flutter_web/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/pubspec.yaml @@ -34,3 +34,7 @@ topics: - html - webview - webview-flutter +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_platform_interface: {path: ../../../packages/webview_flutter/webview_flutter_platform_interface} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index f5a9fd6ef336..50781aae1a50 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.27.0 + +* Adds NavigationDelegate.onCreateWindow for target=_blank / window.open. + ## 3.26.0 * Adds new method for accessing a native `WKWebView` from a `FlutterPluginRegistrar`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart index 7e1f8823b37e..37208ffdfa1c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/legacy/webview_flutter_test.dart @@ -76,22 +76,24 @@ Future main() async { expect(currentUrl, primaryUrl); }); - testWidgets('withWeakRefenceTo allows encapsulating class to be garbage collected', ( - WidgetTester tester, - ) async { - final gcCompleter = Completer(); - final instanceManager = PigeonInstanceManager(onWeakReferenceRemoved: gcCompleter.complete); + testWidgets( + 'withWeakRefenceTo allows encapsulating class to be garbage collected', + (WidgetTester tester) async { + final gcCompleter = Completer(); + final instanceManager = PigeonInstanceManager(onWeakReferenceRemoved: gcCompleter.complete); - ClassWithCallbackClass? instance = ClassWithCallbackClass(); - instanceManager.addDartCreatedInstance(instance.callbackClass); - instance = null; + ClassWithCallbackClass? instance = ClassWithCallbackClass(); + instanceManager.addDartCreatedInstance(instance.callbackClass); + instance = null; - // Force garbage collection. - await forceGC(); + // Force garbage collection. + await forceGC(); - final int gcIdentifier = await gcCompleter.future; - expect(gcIdentifier, 0); - }, timeout: const Timeout(Duration(seconds: 10))); + final int gcIdentifier = await gcCompleter.future; + expect(gcIdentifier, 0); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); testWidgets('loadUrl', (WidgetTester tester) async { final controllerCompleter = Completer(); @@ -455,104 +457,112 @@ Future main() async { skip: Platform.isIOS, ); - testWidgets('Video plays inline when allowsInlineMediaPlayback is true', ( - WidgetTester tester, - ) async { - final String videoTestBase64 = await getTestVideoBase64(); - final controllerCompleter = Completer(); - final pageLoaded = Completer(); - final videoPlaying = Completer(); + testWidgets( + 'Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + final String videoTestBase64 = await getTestVideoBase64(); + final controllerCompleter = Completer(); + final pageLoaded = Completer(); + final videoPlaying = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'VideoTestTime', - onMessageReceived: (JavascriptMessage message) { - final double currentTime = double.parse(message.message); - // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1 && !videoPlaying.isCompleted) { - videoPlaying.complete(null); - } - }, - ), - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - allowsInlineMediaPlayback: true, + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; - // Pump once to trigger the video play. - await tester.pump(); + // Pump once to trigger the video play. + await tester.pump(); - // Makes sure we get the correct event that indicates the video is actually playing. - await videoPlaying.future; + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; - final String fullScreen = await controller.runJavascriptReturningResult('isFullScreen();'); - expect(fullScreen, _webviewBool(false)); - }, skip: Platform.isMacOS || skipOnIosFor154676); + final String fullScreen = await controller.runJavascriptReturningResult( + 'isFullScreen();', + ); + expect(fullScreen, _webviewBool(false)); + }, + skip: Platform.isMacOS || skipOnIosFor154676, + ); - testWidgets('Video plays full screen when allowsInlineMediaPlayback is false', ( - WidgetTester tester, - ) async { - final String videoTestBase64 = await getTestVideoBase64(); - final controllerCompleter = Completer(); - final pageLoaded = Completer(); - final videoPlaying = Completer(); + testWidgets( + 'Video plays full screen when allowsInlineMediaPlayback is false', + (WidgetTester tester) async { + final String videoTestBase64 = await getTestVideoBase64(); + final controllerCompleter = Completer(); + final pageLoaded = Completer(); + final videoPlaying = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - javascriptChannels: { - JavascriptChannel( - name: 'VideoTestTime', - onMessageReceived: (JavascriptMessage message) { - final double currentTime = double.parse(message.message); - // Let it play for at least 1 second to make sure the related video's properties are set. - if (currentTime > 1 && !videoPlaying.isCompleted) { - videoPlaying.complete(null); - } - }, - ), - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1 && !videoPlaying.isCompleted) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; - // Pump once to trigger the video play. - await tester.pump(); + // Pump once to trigger the video play. + await tester.pump(); - // Makes sure we get the correct event that indicates the video is actually playing. - await videoPlaying.future; + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; - final String fullScreen = await controller.runJavascriptReturningResult('isFullScreen();'); - expect(fullScreen, _webviewBool(true)); - }, skip: Platform.isMacOS || skipOnIosFor154676); + final String fullScreen = await controller.runJavascriptReturningResult( + 'isFullScreen();', + ); + expect(fullScreen, _webviewBool(true)); + }, + skip: Platform.isMacOS || skipOnIosFor154676, + ); }, // allowsInlineMediaPlayback has no effect on macOS. skip: Platform.isMacOS, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index 23e262f4af67..61e4e5b52eae 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -35,3 +35,7 @@ flutter: - assets/www/styles/style.css # Test certificate used to create a test native `SecTrust`. - assets/test_cert.der +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_platform_interface: {path: ../../../../packages/webview_flutter/webview_flutter_platform_interface} 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 ffff87a1f43f..a3c8402bc98b 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 @@ -175,11 +175,28 @@ class WebKitWebViewController extends PlatformWebViewController { WKWebView webView, WKWebViewConfiguration configuration, WKNavigationAction navigationAction, - ) { + ) async { + // New-window requests (`target=_blank` / `window.open`) map to + // onCreateWindow — same role as WKUIDelegate.createWebViewWith. final bool isForMainFrame = navigationAction.targetFrame?.isMainFrame ?? false; - if (!isForMainFrame) { - PlatformWebView.fromNativeWebView(webView).load(navigationAction.request); + if (isForMainFrame) { + return; + } + + final String url = await navigationAction.request.getUrl() ?? ''; + if (url.isEmpty) { + return; + } + + final CreateWindowCallback? onCreateWindow = + weakThis.target?._currentNavigationDelegate?._onCreateWindow; + if (onCreateWindow != null) { + onCreateWindow(url); + return; } + + // Upstream default when no host callback is registered. + await PlatformWebView.fromNativeWebView(webView).load(navigationAction.request); }, requestMediaCapturePermission: ( @@ -1232,6 +1249,7 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { ProgressCallback? _onProgress; WebResourceErrorCallback? _onWebResourceError; NavigationRequestCallback? _onNavigationRequest; + CreateWindowCallback? _onCreateWindow; UrlChangeCallback? _onUrlChange; HttpAuthRequestCallback? _onHttpAuthRequest; SslAuthErrorCallback? _onSslAuthError; @@ -1266,6 +1284,11 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { _onNavigationRequest = onNavigationRequest; } + @override + Future setOnCreateWindow(CreateWindowCallback onCreateWindow) async { + _onCreateWindow = onCreateWindow; + } + @override Future setOnUrlChange(UrlChangeCallback onUrlChange) async { _onUrlChange = onUrlChange; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 5a1c07a9c271..cc964e27d1b0 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.26.0 +version: 3.27.0 environment: sdk: ^3.12.0 @@ -26,7 +26,7 @@ dependencies: sdk: flutter meta: ^1.10.0 path: ^1.8.0 - webview_flutter_platform_interface: ^2.15.1 + webview_flutter_platform_interface: ^2.16.0 dev_dependencies: build_runner: ^2.1.5 @@ -39,3 +39,7 @@ topics: - html - webview - webview-flutter +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + webview_flutter_platform_interface: {path: ../../../packages/webview_flutter/webview_flutter_platform_interface} 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 757704666dfb..b365f9c5d9e3 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 @@ -1140,7 +1140,7 @@ void main() { expect(callbackProgress, 0); }); - test('Requests to open a new window loads request in same window', () { + test('Requests to open a new window loads request in same window', () async { // Reset last created delegate. CapturingUIDelegate.lastCreatedDelegate = CapturingUIDelegate( requestMediaCapturePermission: (_, _, _, _, _) async { @@ -1157,6 +1157,7 @@ void main() { final mockWebView = MockUIViewWKWebView(); final mockRequest = MockURLRequest(); + when(mockRequest.getUrl()).thenAnswer((_) => Future.value('https://www.google.com')); CapturingUIDelegate.lastCreatedDelegate.onCreateWebView!( CapturingUIDelegate.lastCreatedDelegate, @@ -1168,10 +1169,52 @@ void main() { navigationType: NavigationType.linkActivated, ), ); + await pumpEventQueue(); verify(mockWebView.load(mockRequest)); }); + test('Requests to open a new window call onCreateWindow when set', () async { + CapturingUIDelegate.lastCreatedDelegate = CapturingUIDelegate( + requestMediaCapturePermission: (_, _, _, _, _) async { + return PermissionDecision.deny; + }, + runJavaScriptConfirmPanel: (_, _, _, _) async { + return false; + }, + ); + + final WebKitWebViewController controller = createControllerWithMocks(); + + PigeonOverrides.wKNavigationDelegate_new = CapturingNavigationDelegate.new; + final navigationDelegate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + ); + + late final String callbackUrl; + await navigationDelegate.setOnCreateWindow((String url) => callbackUrl = url); + await controller.setPlatformNavigationDelegate(navigationDelegate); + + final mockWebView = MockUIViewWKWebView(); + final mockRequest = MockURLRequest(); + when(mockRequest.getUrl()).thenAnswer((_) => Future.value('https://www.google.com')); + + CapturingUIDelegate.lastCreatedDelegate.onCreateWebView!( + CapturingUIDelegate.lastCreatedDelegate, + mockWebView, + MockWKWebViewConfiguration(), + WKNavigationAction.pigeon_detached( + request: mockRequest, + targetFrame: WKFrameInfo.pigeon_detached(isMainFrame: false, request: MockURLRequest()), + navigationType: NavigationType.linkActivated, + ), + ); + await pumpEventQueue(); + + expect(callbackUrl, 'https://www.google.com'); + verifyNever(mockWebView.load(any)); + }); + test( 'setPlatformNavigationDelegate onProgress can be changed by the WebKitNavigationDelegate', () async {