From 2aab4627025b86dd621b0f28ff0ec9298f86d0cd Mon Sep 17 00:00:00 2001 From: Lorenzo Pichilli Date: Wed, 10 Feb 2021 00:15:10 +0100 Subject: [PATCH] Added useOnNavigationResponse iOS-specific WebView option, Added iosOnNavigationResponse iOS-specific WebView event, Added new iOS-specific attributes to ShouldOverrideUrlLoadingRequest and CreateWindowRequest classes --- CHANGELOG.md | 4 +- README.md | 1 + .../InAppWebViewChromeClient.java | 8 + .../InAppWebView/InAppWebViewClient.java | 9 + ios/Classes/InAppWebView.swift | 119 +++++- ios/Classes/InAppWebViewOptions.swift | 1 + lib/src/headless_in_app_webview.dart | 6 + lib/src/in_app_browser.dart | 10 + lib/src/in_app_webview.dart | 6 + lib/src/in_app_webview_controller.dart | 65 ++- lib/src/types.dart | 382 +++++++++++++++++- lib/src/webview.dart | 12 + lib/src/webview_options.dart | 24 +- 13 files changed, 612 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 057a524a..7f408550 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,14 @@ - Added `initialUserScripts` WebView option - Added `addUserScript`, `addUserScripts`, `removeUserScript`, `removeUserScripts`, `removeAllUserScripts`, `callAsyncJavaScript` WebView methods - Added `contentWorld` argument to `evaluateJavascript` WebView method -- Added `isDirectionalLockEnabled`, `mediaType`, `pageZoom`, `limitsNavigationsToAppBoundDomains` iOS-specific WebView options +- Added `isDirectionalLockEnabled`, `mediaType`, `pageZoom`, `limitsNavigationsToAppBoundDomains`, `useOnNavigationResponse` iOS-specific WebView options - Added `handlesURLScheme`, `createPdf`, `createWebArchiveData` iOS-specific WebView methods +- Added `iosOnNavigationResponse` iOS-specific WebView events - Added `iosAnimated` optional argument to `zoomBy` WebView method - Added `screenshotConfiguration` optional argument to `takeScreenshot` WebView method - Added `scriptHtmlTagAttributes` optional argument to `injectJavascriptFileFromUrl` WebView method - Added `cssLinkHtmlTagAttributes` optional argument to `injectCSSFileFromUrl` WebView method +- Added new iOS-specific attributes to `ShouldOverrideUrlLoadingRequest` and `CreateWindowRequest` classes - Updated integration tests - Merge "Upgraded appcompat to 1.2.0-rc-02" [#465](https://github.com/pichillilorenzo/flutter_inappwebview/pull/465) (thanks to [andreidiaconu](https://github.com/andreidiaconu)) - Merge "Added missing field 'headers' which returned by WebResourceResponse.toMap()" [#490](https://github.com/pichillilorenzo/flutter_inappwebview/pull/490) (thanks to [Doflatango](https://github.com/Doflatango)) diff --git a/README.md b/README.md index 998878df..97f46a5c 100755 --- a/README.md +++ b/README.md @@ -662,6 +662,7 @@ Instead, on the `onLoadStop` WebView event, you can use `callHandler` directly: * `selectionGranularity`: The level of granularity with which the user can interactively select content in the web view. * `sharedCookiesEnabled`: Set `true` if shared cookies from `HTTPCookieStorage.shared` should used for every load request in the WebView. * `suppressesIncrementalRendering`: Set to `true` if you want the WebView suppresses content rendering until it is fully loaded into memory. The default value is `false`. +* `useOnNavigationResponse`: Set to `true` to be able to listen at the `iosOnNavigationResponse` event. The default value is `false`. #### `InAppWebView` Events diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewChromeClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewChromeClient.java index 1a8a88d3..231a0d72 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewChromeClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewChromeClient.java @@ -586,6 +586,14 @@ public class InAppWebViewChromeClient extends WebChromeClient implements PluginR obj.put("androidIsUserGesture", isUserGesture); obj.put("iosWKNavigationType", null); obj.put("iosIsForMainFrame", null); + obj.put("iosAllowsCellularAccess", null); + obj.put("iosAllowsConstrainedNetworkAccess", null); + obj.put("iosAllowsExpensiveNetworkAccess", null); + obj.put("iosCachePolicy", null); + obj.put("iosHttpShouldHandleCookies", null); + obj.put("iosHttpShouldUsePipelining", null); + obj.put("iosNetworkServiceType", null); + obj.put("iosTimeoutInterval", null); windowWebViewMessages.put(windowId, resultMsg); diff --git a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java index 065e02b0..591a0638 100755 --- a/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java +++ b/android/src/main/java/com/pichillilorenzo/flutter_inappwebview/InAppWebView/InAppWebViewClient.java @@ -120,6 +120,15 @@ public class InAppWebViewClient extends WebViewClient { obj.put("androidHasGesture", hasGesture); obj.put("androidIsRedirect", isRedirect); obj.put("iosWKNavigationType", null); + obj.put("iosAllowsCellularAccess", null); + obj.put("iosAllowsConstrainedNetworkAccess", null); + obj.put("iosAllowsExpensiveNetworkAccess", null); + obj.put("iosCachePolicy", null); + obj.put("iosHttpShouldHandleCookies", null); + obj.put("iosHttpShouldUsePipelining", null); + obj.put("iosNetworkServiceType", null); + obj.put("iosTimeoutInterval", null); + channel.invokeMethod("shouldOverrideUrlLoading", obj, new MethodChannel.Result() { @Override public void success(Object response) { diff --git a/ios/Classes/InAppWebView.swift b/ios/Classes/InAppWebView.swift index a69b973f..072077ff 100755 --- a/ios/Classes/InAppWebView.swift +++ b/ios/Classes/InAppWebView.swift @@ -2402,17 +2402,24 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi return result; } + @available(iOS 13.0, *) + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + preferences: WKWebpagePreferences, + decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { + self.webView(webView, decidePolicyFor: navigationAction, decisionHandler: {(navigationActionPolicy) -> Void in + decisionHandler(navigationActionPolicy, preferences) + }) + } + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if let url = navigationAction.request.url { + if navigationAction.request.url != nil { - if activateShouldOverrideUrlLoading && (options?.useShouldOverrideUrlLoading)! { - - let isForMainFrame = navigationAction.targetFrame?.isMainFrame ?? false - - shouldOverrideUrlLoading(url: url, method: navigationAction.request.httpMethod, headers: navigationAction.request.allHTTPHeaderFields, isForMainFrame: isForMainFrame, navigationType: navigationAction.navigationType, result: { (result) -> Void in + if activateShouldOverrideUrlLoading, let useShouldOverrideUrlLoading = options?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading { + shouldOverrideUrlLoading(navigationAction: navigationAction, result: { (result) -> Void in if result is FlutterError { print((result as! FlutterError).message ?? "") decisionHandler(.allow) @@ -2475,18 +2482,55 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } } - if (options?.useOnDownloadStart)! { + let useOnNavigationResponse = options?.useOnNavigationResponse + + if useOnNavigationResponse != nil, useOnNavigationResponse! { + onNavigationResponse(navigationResponse: navigationResponse, result: { (result) -> Void in + if result is FlutterError { + print((result as! FlutterError).message ?? "") + decisionHandler(.allow) + return + } + else if (result as? NSObject) == FlutterMethodNotImplemented { + decisionHandler(.allow) + return + } + else { + var response: [String: Any] + if let r = result { + response = r as! [String: Any] + var action = response["action"] as? Int + action = action != nil ? action : 0; + switch action { + case 1: + decisionHandler(.allow) + break + default: + decisionHandler(.cancel) + } + return; + } + decisionHandler(.allow) + } + }) + } + + if let useOnDownloadStart = options?.useOnDownloadStart, useOnDownloadStart { let mimeType = navigationResponse.response.mimeType if let url = navigationResponse.response.url, navigationResponse.isForMainFrame { if mimeType != nil && !mimeType!.starts(with: "text/") { onDownloadStart(url: url.absoluteString) - decisionHandler(.cancel) + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.cancel) + } return } } } - decisionHandler(.allow) + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.allow) + } } public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { @@ -2758,7 +2802,6 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi } return identityAndTrust; } - func createAlertDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, completionHandler: @escaping () -> Void) { let title = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message @@ -3008,13 +3051,28 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi InAppWebView.windowWebViews[windowId] = webViewTransport windowWebView.stopLoading() + var iosAllowsConstrainedNetworkAccess: Bool? = nil + var iosAllowsExpensiveNetworkAccess: Bool? = nil + if #available(iOS 13.0, *) { + iosAllowsConstrainedNetworkAccess = navigationAction.request.allowsConstrainedNetworkAccess + iosAllowsExpensiveNetworkAccess = navigationAction.request.allowsExpensiveNetworkAccess + } + let arguments: [String: Any?] = [ "url": navigationAction.request.url?.absoluteString, "windowId": windowId, "androidIsDialog": nil, "androidIsUserGesture": nil, "iosWKNavigationType": navigationAction.navigationType.rawValue, - "iosIsForMainFrame": navigationAction.targetFrame?.isMainFrame ?? false + "iosIsForMainFrame": navigationAction.targetFrame?.isMainFrame ?? false, + "iosAllowsCellularAccess": navigationAction.request.allowsCellularAccess, + "iosAllowsConstrainedNetworkAccess": iosAllowsConstrainedNetworkAccess, + "iosAllowsExpensiveNetworkAccess": iosAllowsExpensiveNetworkAccess, + "iosCachePolicy": navigationAction.request.cachePolicy.rawValue, + "iosHttpShouldHandleCookies": navigationAction.request.httpShouldHandleCookies, + "iosHttpShouldUsePipelining": navigationAction.request.httpShouldUsePipelining, + "iosNetworkServiceType": navigationAction.request.networkServiceType.rawValue, + "iosTimeoutInterval": navigationAction.request.timeoutInterval, ] channel?.invokeMethod("onCreateWindow", arguments: arguments, result: { (result) -> Void in if result is FlutterError { @@ -3218,19 +3276,46 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi channel?.invokeMethod("onLoadResourceCustomScheme", arguments: arguments, result: result) } - public func shouldOverrideUrlLoading(url: URL, method: String?, headers: [String: String]?, isForMainFrame: Bool, navigationType: WKNavigationType, result: FlutterResult?) { + public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, result: FlutterResult?) { + var iosAllowsConstrainedNetworkAccess: Bool? = nil + var iosAllowsExpensiveNetworkAccess: Bool? = nil + if #available(iOS 13.0, *) { + iosAllowsConstrainedNetworkAccess = navigationAction.request.allowsConstrainedNetworkAccess + iosAllowsExpensiveNetworkAccess = navigationAction.request.allowsExpensiveNetworkAccess + } let arguments: [String: Any?] = [ - "url": url.absoluteString, - "method": method, - "headers": headers, - "isForMainFrame": isForMainFrame, + "url": navigationAction.request.url?.absoluteString, + "method": navigationAction.request.httpMethod, + "headers": navigationAction.request.allHTTPHeaderFields, + "isForMainFrame": navigationAction.targetFrame?.isMainFrame ?? false, "androidHasGesture": nil, "androidIsRedirect": nil, - "iosWKNavigationType": navigationType.rawValue + "iosWKNavigationType": navigationAction.navigationType.rawValue, + "iosAllowsCellularAccess": navigationAction.request.allowsCellularAccess, + "iosAllowsConstrainedNetworkAccess": iosAllowsConstrainedNetworkAccess, + "iosAllowsExpensiveNetworkAccess": iosAllowsExpensiveNetworkAccess, + "iosCachePolicy": navigationAction.request.cachePolicy.rawValue, + "iosHttpShouldHandleCookies": navigationAction.request.httpShouldHandleCookies, + "iosHttpShouldUsePipelining": navigationAction.request.httpShouldUsePipelining, + "iosNetworkServiceType": navigationAction.request.networkServiceType.rawValue, + "iosTimeoutInterval": navigationAction.request.timeoutInterval, ] channel?.invokeMethod("shouldOverrideUrlLoading", arguments: arguments, result: result) } + public func onNavigationResponse(navigationResponse: WKNavigationResponse, result: FlutterResult?) { + let arguments: [String: Any?] = [ + "url": navigationResponse.response.url?.absoluteString, + "isForMainFrame": navigationResponse.isForMainFrame, + "canShowMIMEType": navigationResponse.canShowMIMEType, + "expectedContentLength": navigationResponse.response.expectedContentLength, + "mimeType": navigationResponse.response.mimeType, + "suggestedFilename": navigationResponse.response.suggestedFilename, + "textEncodingName": navigationResponse.response.textEncodingName, + ] + channel?.invokeMethod("onNavigationResponse", arguments: arguments, result: result) + } + public func onReceivedHttpAuthRequest(challenge: URLAuthenticationChallenge, result: FlutterResult?) { let arguments: [String: Any?] = [ "host": challenge.protectionSpace.host, diff --git a/ios/Classes/InAppWebViewOptions.swift b/ios/Classes/InAppWebViewOptions.swift index 239123c9..9ac27598 100755 --- a/ios/Classes/InAppWebViewOptions.swift +++ b/ios/Classes/InAppWebViewOptions.swift @@ -65,6 +65,7 @@ public class InAppWebViewOptions: Options { var mediaType: String? = nil var pageZoom = 1.0 var limitsNavigationsToAppBoundDomains = false + var useOnNavigationResponse = false override init(){ super.init() diff --git a/lib/src/headless_in_app_webview.dart b/lib/src/headless_in_app_webview.dart index c1b85b73..aebc558e 100644 --- a/lib/src/headless_in_app_webview.dart +++ b/lib/src/headless_in_app_webview.dart @@ -77,6 +77,7 @@ class HeadlessInAppWebView implements WebView { this.androidOnReceivedLoginRequest, this.iosOnWebContentProcessDidTerminate, this.iosOnDidReceiveServerRedirectForProvisionalNavigation, + this.iosOnNavigationResponse, this.initialUrl, this.initialFile, this.initialData, @@ -190,6 +191,11 @@ class HeadlessInAppWebView implements WebView { final void Function(InAppWebViewController controller)? iosOnWebContentProcessDidTerminate; + @override + final Future Function(InAppWebViewController controller, + IOSNavigationResponse navigationResponse)? + iosOnNavigationResponse; + @override final Future Function( InAppWebViewController controller, AjaxRequest ajaxRequest)? diff --git a/lib/src/in_app_browser.dart b/lib/src/in_app_browser.dart index 5433e8b0..79b32f28 100755 --- a/lib/src/in_app_browser.dart +++ b/lib/src/in_app_browser.dart @@ -753,6 +753,16 @@ class InAppBrowser { ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455627-webview void iosOnDidReceiveServerRedirectForProvisionalNavigation() {} + ///Called when a web view asks for permission to navigate to new content after the response to the navigation request is known. + /// + ///[navigationResponse] represents the navigation response. + /// + ///**NOTE**: available only on iOS. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview + Future? + iosOnNavigationResponse(IOSNavigationResponse navigationResponse) {} + void throwIsAlreadyOpened({String message = ''}) { if (this.isOpened()) { throw Exception([ diff --git a/lib/src/in_app_webview.dart b/lib/src/in_app_webview.dart index 93912144..77a66bb8 100755 --- a/lib/src/in_app_webview.dart +++ b/lib/src/in_app_webview.dart @@ -89,6 +89,7 @@ class InAppWebView extends StatefulWidget implements WebView { this.androidOnReceivedLoginRequest, this.iosOnWebContentProcessDidTerminate, this.iosOnDidReceiveServerRedirectForProvisionalNavigation, + this.iosOnNavigationResponse, this.gestureRecognizers, }) : super(key: key); @@ -151,6 +152,11 @@ class InAppWebView extends StatefulWidget implements WebView { final void Function(InAppWebViewController controller)? iosOnWebContentProcessDidTerminate; + @override + final Future Function(InAppWebViewController controller, + IOSNavigationResponse navigationResponse)? + iosOnNavigationResponse; + @override final Future Function( InAppWebViewController controller, AjaxRequest ajaxRequest)? diff --git a/lib/src/in_app_webview_controller.dart b/lib/src/in_app_webview_controller.dart index 002b27f7..b4441e7a 100644 --- a/lib/src/in_app_webview_controller.dart +++ b/lib/src/in_app_webview_controller.dart @@ -146,6 +146,14 @@ class InAppWebViewController { bool? androidHasGesture = call.arguments["androidHasGesture"]; bool? androidIsRedirect = call.arguments["androidIsRedirect"]; int? iosWKNavigationType = call.arguments["iosWKNavigationType"]; + bool? iosAllowsCellularAccess = call.arguments["iosAllowsCellularAccess"]; + bool? iosAllowsConstrainedNetworkAccess = call.arguments["iosAllowsConstrainedNetworkAccess"]; + bool? iosAllowsExpensiveNetworkAccess = call.arguments["iosAllowsExpensiveNetworkAccess"]; + int? iosCachePolicy = call.arguments["iosCachePolicy"]; + bool? iosHttpShouldHandleCookies = call.arguments["iosHttpShouldHandleCookies"]; + bool? iosHttpShouldUsePipelining = call.arguments["iosHttpShouldUsePipelining"]; + int? iosNetworkServiceType = call.arguments["iosNetworkServiceType"]; + double? iosTimeoutInterval = call.arguments["iosTimeoutInterval"]; ShouldOverrideUrlLoadingRequest shouldOverrideUrlLoadingRequest = ShouldOverrideUrlLoadingRequest( @@ -156,7 +164,15 @@ class InAppWebViewController { androidHasGesture: androidHasGesture, androidIsRedirect: androidIsRedirect, iosWKNavigationType: - IOSWKNavigationType.fromValue(iosWKNavigationType)); + IOSWKNavigationType.fromValue(iosWKNavigationType), + iosAllowsCellularAccess: iosAllowsCellularAccess, + iosAllowsConstrainedNetworkAccess: iosAllowsConstrainedNetworkAccess, + iosAllowsExpensiveNetworkAccess: iosAllowsExpensiveNetworkAccess, + iosCachePolicy: IOSURLRequestCachePolicy.fromValue(iosCachePolicy), + iosHttpShouldHandleCookies: iosHttpShouldHandleCookies, + iosHttpShouldUsePipelining: iosHttpShouldUsePipelining, + iosNetworkServiceType: IOSURLRequestNetworkServiceType.fromValue(iosNetworkServiceType), + iosTimeoutInterval: iosTimeoutInterval); if (_webview != null && _webview!.shouldOverrideUrlLoading != null) return (await _webview!.shouldOverrideUrlLoading!( @@ -221,6 +237,14 @@ class InAppWebViewController { bool? androidIsUserGesture = call.arguments["androidIsUserGesture"]; int? iosWKNavigationType = call.arguments["iosWKNavigationType"]; bool? iosIsForMainFrame = call.arguments["iosIsForMainFrame"]; + bool? iosAllowsCellularAccess = call.arguments["iosAllowsCellularAccess"]; + bool? iosAllowsConstrainedNetworkAccess = call.arguments["iosAllowsConstrainedNetworkAccess"]; + bool? iosAllowsExpensiveNetworkAccess = call.arguments["iosAllowsExpensiveNetworkAccess"]; + int? iosCachePolicy = call.arguments["iosCachePolicy"]; + bool? iosHttpShouldHandleCookies = call.arguments["iosHttpShouldHandleCookies"]; + bool? iosHttpShouldUsePipelining = call.arguments["iosHttpShouldUsePipelining"]; + int? iosNetworkServiceType = call.arguments["iosNetworkServiceType"]; + double? iosTimeoutInterval = call.arguments["iosTimeoutInterval"]; CreateWindowRequest createWindowRequest = CreateWindowRequest( url: url, @@ -229,7 +253,15 @@ class InAppWebViewController { androidIsUserGesture: androidIsUserGesture, iosWKNavigationType: IOSWKNavigationType.fromValue(iosWKNavigationType), - iosIsForMainFrame: iosIsForMainFrame); + iosIsForMainFrame: iosIsForMainFrame, + iosAllowsCellularAccess: iosAllowsCellularAccess, + iosAllowsConstrainedNetworkAccess: iosAllowsConstrainedNetworkAccess, + iosAllowsExpensiveNetworkAccess: iosAllowsExpensiveNetworkAccess, + iosCachePolicy: IOSURLRequestCachePolicy.fromValue(iosCachePolicy), + iosHttpShouldHandleCookies: iosHttpShouldHandleCookies, + iosHttpShouldUsePipelining: iosHttpShouldUsePipelining, + iosNetworkServiceType: IOSURLRequestNetworkServiceType.fromValue(iosNetworkServiceType), + iosTimeoutInterval: iosTimeoutInterval); bool? result = false; @@ -614,6 +646,35 @@ class InAppWebViewController { else if (_inAppBrowser != null) _inAppBrowser!.iosOnDidReceiveServerRedirectForProvisionalNavigation(); break; + case "onNavigationResponse": + String? url = call.arguments["url"]; + bool isForMainFrame = call.arguments["isForMainFrame"]; + bool canShowMIMEType = call.arguments["canShowMIMEType"]; + int expectedContentLength = call.arguments["expectedContentLength"]; + String? mimeType = call.arguments["mimeType"]; + String? suggestedFilename = call.arguments["suggestedFilename"]; + String? textEncodingName = call.arguments["textEncodingName"]; + + IOSNavigationResponse iosOnNavigationResponse = + IOSNavigationResponse( + url: url, + isForMainFrame: isForMainFrame, + canShowMIMEType: canShowMIMEType, + expectedContentLength: expectedContentLength, + mimeType: mimeType, + suggestedFilename: suggestedFilename, + textEncodingName: textEncodingName + ); + + if (_webview != null && _webview!.iosOnNavigationResponse != null) + return (await _webview!.iosOnNavigationResponse!( + this, iosOnNavigationResponse)) + ?.toMap(); + else if (_inAppBrowser != null) + return (await _inAppBrowser! + .iosOnNavigationResponse(iosOnNavigationResponse)) + ?.toMap(); + break; case "onLongPressHitTestResult": Map? hitTestResultMap = call.arguments["hitTestResult"]; diff --git a/lib/src/types.dart b/lib/src/types.dart index ac9f6081..0132c3bf 100755 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -3157,6 +3157,181 @@ class IOSWKNavigationType { int get hashCode => _value.hashCode; } +///An iOS-specific Class that represents the constants used to specify interaction with the cached responses. +class IOSURLRequestCachePolicy { + final int _value; + + const IOSURLRequestCachePolicy._internal(this._value); + + static final Set values = [ + IOSURLRequestCachePolicy.USE_PROTOCOL_CACHE_POLICY, + IOSURLRequestCachePolicy.RELOAD_IGNORING_LOCAL_CACHE_DATA, + IOSURLRequestCachePolicy.RELOAD_IGNORING_LOCAL_AND_REMOTE_CACHE_DATA, + IOSURLRequestCachePolicy.RETURN_CACHE_DATA_ELSE_LOAD, + IOSURLRequestCachePolicy.RETURN_CACHE_DATA_DONT_LOAD, + IOSURLRequestCachePolicy.RELOAD_REVALIDATING_CACHE_DATA, + ].toSet(); + + static IOSURLRequestCachePolicy? fromValue(int? value) { + if (value != null) { + try { + return IOSURLRequestCachePolicy.values.firstWhere( + (element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + int toValue() => _value; + + @override + String toString() { + switch (_value) { + case 1: + return "RELOAD_IGNORING_LOCAL_CACHE_DATA"; + case 2: + return "RETURN_CACHE_DATA_ELSE_LOAD"; + case 3: + return "RETURN_CACHE_DATA_DONT_LOAD"; + case 4: + return "RELOAD_IGNORING_LOCAL_AND_REMOTE_CACHE_DATA"; + case 5: + return "RELOAD_REVALIDATING_CACHE_DATA"; + case 0: + default: + return "USE_PROTOCOL_CACHE_POLICY"; + } + } + + ///Use the caching logic defined in the protocol implementation, if any, for a particular URL load request. + ///This is the default policy for URL load requests. + static const USE_PROTOCOL_CACHE_POLICY = const IOSURLRequestCachePolicy._internal(0); + + ///The URL load should be loaded only from the originating source. + ///This policy specifies that no existing cache data should be used to satisfy a URL load request. + /// + ///**NOTE**: Always use this policy if you are making HTTP or HTTPS byte-range requests. + static const RELOAD_IGNORING_LOCAL_CACHE_DATA = const IOSURLRequestCachePolicy._internal(1); + + ///Use existing cache data, regardless or age or expiration date, loading from originating source only if there is no cached data. + static const RETURN_CACHE_DATA_ELSE_LOAD = const IOSURLRequestCachePolicy._internal(2); + + ///Use existing cache data, regardless or age or expiration date, and fail if no cached data is available. + /// + ///If there is no existing data in the cache corresponding to a URL load request, + ///no attempt is made to load the data from the originating source, and the load is considered to have failed. + ///This constant specifies a behavior that is similar to an “offline” mode. + static const RETURN_CACHE_DATA_DONT_LOAD = const IOSURLRequestCachePolicy._internal(3); + + ///Ignore local cache data, and instruct proxies and other intermediates to disregard their caches so far as the protocol allows. + /// + ///**NOTE**: Versions earlier than macOS 15, iOS 13, watchOS 6, and tvOS 13 don’t implement this constant. + static const RELOAD_IGNORING_LOCAL_AND_REMOTE_CACHE_DATA = const IOSURLRequestCachePolicy._internal(4); + + ///Use cache data if the origin source can validate it; otherwise, load from the origin. + /// + ///**NOTE**: Versions earlier than macOS 15, iOS 13, watchOS 6, and tvOS 13 don’t implement this constant. + static const RELOAD_REVALIDATING_CACHE_DATA = const IOSURLRequestCachePolicy._internal(5); + + bool operator ==(value) => value == _value; + + @override + int get hashCode => _value.hashCode; +} + +///An iOS-specific Class that represents the constants that specify how a request uses network resources. +class IOSURLRequestNetworkServiceType { + final int _value; + + const IOSURLRequestNetworkServiceType._internal(this._value); + + static final Set values = [ + IOSURLRequestNetworkServiceType.DEFAULT, + IOSURLRequestNetworkServiceType.VIDEO, + IOSURLRequestNetworkServiceType.BACKGROUND, + IOSURLRequestNetworkServiceType.VOICE, + IOSURLRequestNetworkServiceType.RESPONSIVE_DATA, + IOSURLRequestNetworkServiceType.AV_STREAMING, + IOSURLRequestNetworkServiceType.RESPONSIVE_AV, + IOSURLRequestNetworkServiceType.CALL_SIGNALING, + ].toSet(); + + static IOSURLRequestNetworkServiceType? fromValue(int? value) { + if (value != null) { + try { + return IOSURLRequestNetworkServiceType.values.firstWhere( + (element) => element.toValue() == value); + } catch (e) { + return null; + } + } + return null; + } + + int toValue() => _value; + + @override + String toString() { + switch (_value) { + case 2: + return "VIDEO"; + case 3: + return "BACKGROUND"; + case 4: + return "VOICE"; + case 6: + return "RESPONSIVE_DATA"; + case 8: + return "AV_STREAMING"; + case 9: + return "RESPONSIVE_AV"; + case 11: + return "CALL_SIGNALING"; + case 0: + default: + return "DEFAULT"; + } + } + + ///A service type for standard network traffic. + static const DEFAULT = const IOSURLRequestNetworkServiceType._internal(0); + + ///A service type for video traffic. + static const VIDEO = const IOSURLRequestNetworkServiceType._internal(2); + + ///A service type for background traffic. + /// + ///You should specify this type if your app is performing a download that was not requested by the user—for example, + ///prefetching content so that it will be available when the user chooses to view it. + static const BACKGROUND = const IOSURLRequestNetworkServiceType._internal(3); + + ///A service type for voice traffic. + static const VOICE = const IOSURLRequestNetworkServiceType._internal(4); + + ///A service type for data that the user is actively waiting for. + /// + ///Use this service type for interactive situations where the user is anticipating a quick response, like instant messaging or completing a purchase. + static const RESPONSIVE_DATA = const IOSURLRequestNetworkServiceType._internal(6); + + ///A service type for streaming audio/video data. + static const AV_STREAMING = const IOSURLRequestNetworkServiceType._internal(8); + + ///A service type for responsive (time-sensitive) audio/video data. + static const RESPONSIVE_AV = const IOSURLRequestNetworkServiceType._internal(9); + + ///A service type for call signaling. + /// + ///Use this service type with network traffic that establishes, maintains, or tears down a VoIP call. + static const CALL_SIGNALING = const IOSURLRequestNetworkServiceType._internal(11); + + bool operator ==(value) => value == _value; + + @override + int get hashCode => _value.hashCode; +} + ///Class that represents the navigation request used by the [WebView.shouldOverrideUrlLoading] event. class ShouldOverrideUrlLoadingRequest { ///Represents the url of the navigation request. @@ -3189,6 +3364,46 @@ class ShouldOverrideUrlLoadingRequest { ///**NOTE**: available only on iOS. IOSWKNavigationType? iosWKNavigationType; + ///A Boolean value indicating whether the request is allowed to use the built-in cellular radios to satisfy the request. + /// + ///**NOTE**: available only on iOS. + bool? iosAllowsCellularAccess; + + ///A Boolean value that indicates whether the request may use the network when the user has specified Low Data Mode. + /// + ///**NOTE**: available only on iOS 13.0+. + bool? iosAllowsConstrainedNetworkAccess; + + ///A Boolean value that indicates whether connections may use a network interface that the system considers expensive. + /// + ///**NOTE**: available only on iOS 13.0+. + bool? iosAllowsExpensiveNetworkAccess; + + ///The request’s cache policy. + /// + ///**NOTE**: available only on iOS. + IOSURLRequestCachePolicy? iosCachePolicy; + + ///A Boolean value indicating whether cookies will be sent with and set for this request. + /// + ///**NOTE**: available only on iOS. + bool? iosHttpShouldHandleCookies; + + ///A Boolean value indicating whether the request should transmit before the previous response is received. + /// + ///**NOTE**: available only on iOS. + bool? iosHttpShouldUsePipelining; + + ///The service type associated with this request. + /// + ///**NOTE**: available only on iOS. + IOSURLRequestNetworkServiceType? iosNetworkServiceType; + + ///The timeout interval of the request. + /// + ///**NOTE**: available only on iOS. + double? iosTimeoutInterval; + ShouldOverrideUrlLoadingRequest( {required this.url, this.method, @@ -3196,7 +3411,15 @@ class ShouldOverrideUrlLoadingRequest { required this.isForMainFrame, this.androidHasGesture, this.androidIsRedirect, - this.iosWKNavigationType}); + this.iosWKNavigationType, + this.iosAllowsCellularAccess, + this.iosAllowsConstrainedNetworkAccess, + this.iosAllowsExpensiveNetworkAccess, + this.iosCachePolicy, + this.iosHttpShouldHandleCookies, + this.iosHttpShouldUsePipelining, + this.iosNetworkServiceType, + this.iosTimeoutInterval}); Map toMap() { return { @@ -3206,7 +3429,15 @@ class ShouldOverrideUrlLoadingRequest { "isForMainFrame": isForMainFrame, "androidHasGesture": androidHasGesture, "androidIsRedirect": androidIsRedirect, - "iosWKNavigationType": iosWKNavigationType?.toValue() + "iosWKNavigationType": iosWKNavigationType?.toValue(), + "iosAllowsCellularAccess": iosAllowsCellularAccess, + "iosAllowsConstrainedNetworkAccess": iosAllowsConstrainedNetworkAccess, + "iosAllowsExpensiveNetworkAccess": iosAllowsExpensiveNetworkAccess, + "iosCachePolicy": iosCachePolicy?.toValue(), + "iosHttpShouldHandleCookies": iosHttpShouldHandleCookies, + "iosHttpShouldUsePipelining": iosHttpShouldUsePipelining, + "iosNetworkServiceType": iosNetworkServiceType?.toValue(), + "iosTimeoutInterval": iosTimeoutInterval, }; } @@ -3250,21 +3481,78 @@ class CreateWindowRequest { ///**NOTE**: available only on iOS. bool? iosIsForMainFrame; + ///A Boolean value indicating whether the request is allowed to use the built-in cellular radios to satisfy the request. + /// + ///**NOTE**: available only on iOS. + bool? iosAllowsCellularAccess; + + ///A Boolean value that indicates whether the request may use the network when the user has specified Low Data Mode. + /// + ///**NOTE**: available only on iOS 13.0+. + bool? iosAllowsConstrainedNetworkAccess; + + ///A Boolean value that indicates whether connections may use a network interface that the system considers expensive. + /// + ///**NOTE**: available only on iOS 13.0+. + bool? iosAllowsExpensiveNetworkAccess; + + ///The request’s cache policy. + /// + ///**NOTE**: available only on iOS. + IOSURLRequestCachePolicy? iosCachePolicy; + + ///A Boolean value indicating whether cookies will be sent with and set for this request. + /// + ///**NOTE**: available only on iOS. + bool? iosHttpShouldHandleCookies; + + ///A Boolean value indicating whether the request should transmit before the previous response is received. + /// + ///**NOTE**: available only on iOS. + bool? iosHttpShouldUsePipelining; + + ///The service type associated with this request. + /// + ///**NOTE**: available only on iOS. + IOSURLRequestNetworkServiceType? iosNetworkServiceType; + + ///The timeout interval of the request. + /// + ///**NOTE**: available only on iOS. + double? iosTimeoutInterval; + CreateWindowRequest( {this.url, required this.windowId, this.androidIsDialog, this.androidIsUserGesture, this.iosWKNavigationType, - this.iosIsForMainFrame}); + this.iosIsForMainFrame, + this.iosAllowsCellularAccess, + this.iosAllowsConstrainedNetworkAccess, + this.iosAllowsExpensiveNetworkAccess, + this.iosCachePolicy, + this.iosHttpShouldHandleCookies, + this.iosHttpShouldUsePipelining, + this.iosNetworkServiceType, + this.iosTimeoutInterval}); Map toMap() { return { + "url": url, + "windowId": windowId, "androidIsDialog": androidIsDialog, "androidIsUserGesture": androidIsUserGesture, "iosWKNavigationType": iosWKNavigationType?.toValue(), - "url": url, - "windowId": windowId + "iosIsForMainFrame": iosIsForMainFrame, + "iosAllowsCellularAccess": iosAllowsCellularAccess, + "iosAllowsConstrainedNetworkAccess": iosAllowsConstrainedNetworkAccess, + "iosAllowsExpensiveNetworkAccess": iosAllowsExpensiveNetworkAccess, + "iosCachePolicy": iosCachePolicy?.toValue(), + "iosHttpShouldHandleCookies": iosHttpShouldHandleCookies, + "iosHttpShouldUsePipelining": iosHttpShouldUsePipelining, + "iosNetworkServiceType": iosNetworkServiceType?.toValue(), + "iosTimeoutInterval": iosTimeoutInterval, }; } @@ -5161,4 +5449,86 @@ class CSSLinkHtmlTagAttributes { String toString() { return toMap().toString(); } -} \ No newline at end of file +} + +///An iOS-specific Class that represents the navigation response used by the [WebView.iosOnNavigationResponse] event. +class IOSNavigationResponse { + + ///The URL for the response. + String? url; + + ///A Boolean value that indicates whether the response targets the web view’s main frame. + bool isForMainFrame; + + ///A Boolean value that indicates whether WebKit is capable of displaying the response’s MIME type natively. + bool canShowMIMEType; + + ///The expected length of the response’s content. + int expectedContentLength; + + ///The MIME type of the response. + String? mimeType; + + ///A suggested filename for the response data. + String? suggestedFilename; + + ///The name of the text encoding provided by the response’s originating source. + String? textEncodingName; + + IOSNavigationResponse({this.url, + required this.isForMainFrame, + required this.canShowMIMEType, + required this.expectedContentLength, + this.mimeType, + this.suggestedFilename, + this.textEncodingName + }); + + Map toMap() { + return { + "url": this.url, + "isForMainFrame": this.isForMainFrame, + "canShowMIMEType": this.canShowMIMEType, + "expectedContentLength": this.expectedContentLength, + "mimeType": this.mimeType, + "suggestedFilename": this.suggestedFilename, + "textEncodingName": this.textEncodingName, + }; + } + + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return toMap().toString(); + } +} + +///Class that is used by [WebView.iosOnNavigationResponse] event. +///It represents the policy to pass back to the decision handler. +class IOSNavigationResponseAction { + final int _value; + + const IOSNavigationResponseAction._internal(this._value); + + int toValue() => _value; + + ///Cancel the navigation. + static const CANCEL = const IOSNavigationResponseAction._internal(0); + + ///Allow the navigation to continue. + static const ALLOW = const IOSNavigationResponseAction._internal(1); + + bool operator ==(value) => value == _value; + + @override + int get hashCode => _value.hashCode; + + Map toMap() { + return { + "action": _value, + }; + } +} diff --git a/lib/src/webview.dart b/lib/src/webview.dart index 0fe96262..251250a1 100644 --- a/lib/src/webview.dart +++ b/lib/src/webview.dart @@ -604,6 +604,17 @@ abstract class WebView { final void Function(InAppWebViewController controller)? iosOnDidReceiveServerRedirectForProvisionalNavigation; + ///Called when a web view asks for permission to navigate to new content after the response to the navigation request is known. + /// + ///[navigationResponse] represents the navigation response. + /// + ///**NOTE**: available only on iOS. + /// + ///**Official iOS API**: https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455643-webview + final Future Function(InAppWebViewController controller, + IOSNavigationResponse navigationResponse)? + iosOnNavigationResponse; + ///Initial url that will be loaded. final String? initialUrl; @@ -680,6 +691,7 @@ abstract class WebView { this.androidOnReceivedLoginRequest, this.iosOnWebContentProcessDidTerminate, this.iosOnDidReceiveServerRedirectForProvisionalNavigation, + this.iosOnNavigationResponse, this.initialUrl, this.initialFile, this.initialData, diff --git a/lib/src/webview_options.dart b/lib/src/webview_options.dart index 117c124c..8408b571 100755 --- a/lib/src/webview_options.dart +++ b/lib/src/webview_options.dart @@ -80,13 +80,13 @@ class ChromeSafariBrowserOptions { ///This class represents all the cross-platform WebView options available. class InAppWebViewOptions implements WebViewOptions, BrowserOptions, AndroidOptions, IosOptions { - ///Set to `true` to be able to listen at the [shouldOverrideUrlLoading] event. The default value is `false`. + ///Set to `true` to be able to listen at the [WebView.shouldOverrideUrlLoading] event. The default value is `false`. bool useShouldOverrideUrlLoading; - ///Set to `true` to be able to listen at the [onLoadResource] event. The default value is `false`. + ///Set to `true` to be able to listen at the [WebView.onLoadResource] event. The default value is `false`. bool useOnLoadResource; - ///Set to `true` to be able to listen at the [onDownloadStart] event. The default value is `false`. + ///Set to `true` to be able to listen at the [WebView.onDownloadStart] event. The default value is `false`. bool useOnDownloadStart; ///Set to `true` to have all the browser's cache cleared before the new WebView is opened. The default value is `false`. @@ -122,7 +122,7 @@ class InAppWebViewOptions ///Define whether the horizontal scrollbar should be drawn or not. The default value is `true`. bool horizontalScrollBarEnabled; - ///List of custom schemes that the WebView must handle. Use the [onLoadResourceCustomScheme] event to intercept resource requests with custom scheme. + ///List of custom schemes that the WebView must handle. Use the [WebView.onLoadResourceCustomScheme] event to intercept resource requests with custom scheme. /// ///**NOTE**: available on iOS 11.0+. List resourceCustomSchemes; @@ -137,10 +137,10 @@ class InAppWebViewOptions ///**NOTE**: available on iOS 13.0+. UserPreferredContentMode? preferredContentMode; - ///Set to `true` to be able to listen at the [shouldInterceptAjaxRequest] event. The default value is `false`. + ///Set to `true` to be able to listen at the [WebView.shouldInterceptAjaxRequest] event. The default value is `false`. bool useShouldInterceptAjaxRequest; - ///Set to `true` to be able to listen at the [shouldInterceptFetchRequest] event. The default value is `false`. + ///Set to `true` to be able to listen at the [WebView.shouldInterceptFetchRequest] event. The default value is `false`. bool useShouldInterceptFetchRequest; ///Set to `true` to open a browser window with incognito mode. The default value is `false`. @@ -474,10 +474,10 @@ class AndroidInAppWebViewOptions bool hardwareAcceleration; ///Sets whether the WebView supports multiple windows. - ///If set to `true`, [onCreateWindow] event must be implemented by the host application. The default value is `false`. + ///If set to `true`, [WebView.onCreateWindow] event must be implemented by the host application. The default value is `false`. bool supportMultipleWindows; - ///Regular expression used by [shouldOverrideUrlLoading] event to cancel navigation requests for frames that are not the main frame. + ///Regular expression used by [WebView.shouldOverrideUrlLoading] event to cancel navigation requests for frames that are not the main frame. ///If the url request of a subframe matches the regular expression, then the request of that subframe is canceled. String? regexToCancelSubFramesLoading; @@ -866,6 +866,9 @@ class IOSInAppWebViewOptions ///**NOTE**: available on iOS 14.0+. bool limitsNavigationsToAppBoundDomains; + ///Set to `true` to be able to listen at the [WebView.iosOnNavigationResponse] event. The default value is `false`. + bool useOnNavigationResponse; + IOSInAppWebViewOptions( {this.disallowOverScroll = false, this.enableViewportScale = false, @@ -894,7 +897,8 @@ class IOSInAppWebViewOptions this.isDirectionalLockEnabled = false, this.mediaType, this.pageZoom = 1.0, - this.limitsNavigationsToAppBoundDomains = false}); + this.limitsNavigationsToAppBoundDomains = false, + this.useOnNavigationResponse = false}); @override Map toMap() { @@ -934,6 +938,7 @@ class IOSInAppWebViewOptions "mediaType": mediaType, "pageZoom": pageZoom, "limitsNavigationsToAppBoundDomains": limitsNavigationsToAppBoundDomains, + "useOnNavigationResponse": useOnNavigationResponse, }; } @@ -987,6 +992,7 @@ class IOSInAppWebViewOptions options.mediaType = map["mediaType"]; options.pageZoom = map["pageZoom"]; options.limitsNavigationsToAppBoundDomains = map["limitsNavigationsToAppBoundDomains"]; + options.useOnNavigationResponse = map["useOnNavigationResponse"]; return options; }