diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index efaab280aef5..c36fa74c5831 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.24.3 + +* Adds support to get failing url from DNS errors on iOS 26+. + ## 3.24.2 * Fixes dartdoc comments that accidentally used HTML. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/FrameInfoProxyAPITests.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/FrameInfoProxyAPITests.swift index 64ba16774b01..c421d53d7746 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/FrameInfoProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/FrameInfoProxyAPITests.swift @@ -13,27 +13,27 @@ class FrameInfoProxyAPITests: XCTestCase { let registrar = TestProxyApiRegistrar() let api = registrar.apiDelegate.pigeonApiWKFrameInfo(registrar) - let instance: TestFrameInfo? = TestFrameInfo() - let value = try? api.pigeonDelegate.isMainFrame(pigeonApi: api, pigeonInstance: instance!) + let instance = TestFrameInfo.instance + let value = try? api.pigeonDelegate.isMainFrame(pigeonApi: api, pigeonInstance: instance) - XCTAssertEqual(value, instance!.isMainFrame) + XCTAssertEqual(value, instance.isMainFrame) } @MainActor func testRequest() { let registrar = TestProxyApiRegistrar() let api = registrar.apiDelegate.pigeonApiWKFrameInfo(registrar) - let instance: TestFrameInfo? = TestFrameInfo() - let value = try? api.pigeonDelegate.request(pigeonApi: api, pigeonInstance: instance!) + let instance = TestFrameInfo.instance + let value = try? api.pigeonDelegate.request(pigeonApi: api, pigeonInstance: instance) - XCTAssertEqual(value?.value, instance!.request) + XCTAssertEqual(value?.value, instance.request) } @MainActor func testNilRequest() { let registrar = TestProxyApiRegistrar() let api = registrar.apiDelegate.pigeonApiWKFrameInfo(registrar) - let instance = TestFrameInfoWithNilRequest() + let instance = TestFrameInfoWithNilRequest.instance let value = try? api.pigeonDelegate.request(pigeonApi: api, pigeonInstance: instance) // On macOS 15.5+, `WKFrameInfo.request` returns with an empty URLRequest. // Previously it would return nil so accept either. @@ -46,6 +46,14 @@ class FrameInfoProxyAPITests: XCTestCase { } class TestFrameInfo: WKFrameInfo { + // Global test instance of `WKFrameInfo`. Using a static instance prevents a crash when + // a `WKFrameInfo` is deallocated during a test on iOS 26+. + static let instance = TestFrameInfo() + + private override init() { + + } + override var isMainFrame: Bool { return true } @@ -56,4 +64,11 @@ class TestFrameInfo: WKFrameInfo { } class TestFrameInfoWithNilRequest: WKFrameInfo { + // Global test instance of `WKFrameInfo` with a nil URLRequest. Using a static instance prevents a + // crash when a `WKFrameInfo` is deallocated during a test on iOS 26+. + static let instance = TestFrameInfoWithNilRequest() + + private override init() { + + } } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationActionProxyAPITests.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationActionProxyAPITests.swift index 3d95643d1cbb..3c6d20210cd2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationActionProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationActionProxyAPITests.swift @@ -40,7 +40,7 @@ class NavigationActionProxyAPITests: XCTestCase { } class TestNavigationAction: WKNavigationAction { - let internalTargetFrame = TestFrameInfo() + let internalTargetFrame = TestFrameInfo.instance override var request: URLRequest { return URLRequest(url: URL(string: "http://google.com")!) 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 081bff28828d..9068f818f0b2 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationDelegateProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/NavigationDelegateProxyAPITests.swift @@ -245,8 +245,7 @@ class TestWebView: WKWebView { } class TestURLAuthenticationChallengeSender: NSObject, URLAuthenticationChallengeSender, - @unchecked - Sendable + @unchecked Sendable { func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UIDelegateProxyAPITests.swift b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UIDelegateProxyAPITests.swift index c5cc1a6f34a5..3f672fbc5e41 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UIDelegateProxyAPITests.swift +++ b/packages/webview_flutter/webview_flutter_wkwebview/darwin/Tests/UIDelegateProxyAPITests.swift @@ -39,7 +39,7 @@ class UIDelegateProxyAPITests: XCTestCase { let instance = UIDelegateImpl(api: api, registrar: registrar) let webView = WKWebView(frame: .zero) let origin = SecurityOriginProxyAPITests.testSecurityOrigin - let frame = TestFrameInfo() + let frame = TestFrameInfo.instance let type: WKMediaCaptureType = .camera var resultDecision: WKPermissionDecision? @@ -64,7 +64,7 @@ class UIDelegateProxyAPITests: XCTestCase { let instance = UIDelegateImpl(api: api, registrar: registrar) let webView = WKWebView(frame: .zero) let message = "myString" - let frame = TestFrameInfo() + let frame = TestFrameInfo.instance instance.webView(webView, runJavaScriptAlertPanelWithMessage: message, initiatedByFrame: frame) { @@ -79,7 +79,7 @@ class UIDelegateProxyAPITests: XCTestCase { let instance = UIDelegateImpl(api: api, registrar: registrar) let webView = WKWebView(frame: .zero) let message = "myString" - let frame = TestFrameInfo() + let frame = TestFrameInfo.instance var confirmedResult: Bool? let callbackExpectation = expectation(description: "Wait for callback.") @@ -103,7 +103,7 @@ class UIDelegateProxyAPITests: XCTestCase { let webView = WKWebView(frame: .zero) let prompt = "myString" let defaultText = "myString3" - let frame = TestFrameInfo() + let frame = TestFrameInfo.instance var inputResult: String? let callbackExpectation = expectation(description: "Wait for callback.") diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/webkit_constants.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/webkit_constants.dart index aef35002c5ea..31d2be16c508 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/webkit_constants.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/webkit_constants.dart @@ -51,6 +51,11 @@ class NSErrorUserInfoKey { /// See https://developer.apple.com/documentation/foundation/nsurlerrorfailingurlstringerrorkey?language=objc. static const String NSURLErrorFailingURLStringError = 'NSErrorFailingURLStringKey'; + + /// The URL which caused a load to fail. + /// + /// See https://developer.apple.com/documentation/foundation/nsurlerrorfailingurlerrorkey. + static const String NSURLErrorFailingURLErrorKey = 'NSErrorFailingURLKey'; } /// The authentication method used by the receiver. 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 6617e5f40598..3dedcc9f2198 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 @@ -1254,17 +1254,22 @@ class WebKitNavigationDelegate extends PlatformNavigationDelegate { ); } }, - didFailProvisionalNavigation: (_, __, NSError error) { + didFailProvisionalNavigation: (_, __, NSError error) async { + var url = + error.userInfo[NSErrorUserInfoKey.NSURLErrorFailingURLStringError] + as String?; + + // On iOS 26+, the error is stored with `NSURLErrorFailingURLErrorKey`. + if (url == null) { + final nativeURL = + error.userInfo[NSErrorUserInfoKey.NSURLErrorFailingURLErrorKey] + as URL?; + url = await nativeURL?.getAbsoluteString(); + } + if (weakThis.target?._onWebResourceError != null) { weakThis.target!._onWebResourceError!( - WebKitWebResourceError._( - error, - isForMainFrame: true, - url: - error.userInfo[NSErrorUserInfoKey - .NSURLErrorFailingURLStringError] - as String?, - ), + WebKitWebResourceError._(error, isForMainFrame: true, url: url), ); } }, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 7330bbae7e60..1b6268e8f829 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.24.2 +version: 3.24.3 environment: sdk: ^3.9.0 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart index 0b2fa67c43a2..a359b78639f9 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_navigation_delegate_test.dart @@ -287,6 +287,55 @@ void main() { expect(callbackError.isForMainFrame, true); }); + test( + 'onWebResourceError can receive DNS errors from didFailProvisionalNavigation', + () async { + PigeonOverrides.wKNavigationDelegate_new = + CapturingNavigationDelegate.new; + final webKitDelegate = WebKitNavigationDelegate( + const WebKitNavigationDelegateCreationParams(), + ); + + late final WebKitWebResourceError callbackError; + void onWebResourceError(WebResourceError error) { + callbackError = error as WebKitWebResourceError; + } + + await webKitDelegate.setOnWebResourceError(onWebResourceError); + + CapturingNavigationDelegate + .lastCreatedDelegate + .didFailProvisionalNavigation!( + WKNavigationDelegate.pigeon_detached( + decidePolicyForNavigationAction: (_, __, ___) async { + return NavigationActionPolicy.cancel; + }, + decidePolicyForNavigationResponse: (_, __, ___) async { + return NavigationResponsePolicy.cancel; + }, + didReceiveAuthenticationChallenge: (_, __, ___) async { + return AuthenticationChallengeResponse.pigeon_detached( + disposition: + UrlSessionAuthChallengeDisposition.performDefaultHandling, + ); + }, + ), + WKWebView.pigeon_detached(), + NSError.pigeon_detached( + code: WKErrorCode.webViewInvalidated, + domain: 'domain', + userInfo: const { + NSErrorUserInfoKey.NSURLErrorFailingURLStringError: + 'www.flutter.dev', + NSErrorUserInfoKey.NSLocalizedDescription: 'my desc', + }, + ), + ); + + expect(callbackError.url, 'www.flutter.dev'); + }, + ); + test( 'onWebResourceError from webViewWebContentProcessDidTerminate', () async {