import Shared import SwiftUI import UIKit import WebKit // MARK: - WebView extension WebViewController { func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { updateFrontendConnectionState(state: FrontEndConnectionState.disconnected.rawValue) webViewExternalMessageHandler.stopImprovScanIfNeeded() } func webView( _ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { let result = server.info.connection.evaluate(challenge) completionHandler(result.0, result.1) } func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { if navigationAction.targetFrame == nil { guard let url = navigationAction.request.url else { Current.Log.error("Received navigation action without URL for new window") return nil } openURLInBrowser(url, self) } return nil } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { refreshControl.endRefreshing() if let err = error as? URLError { if err.code != .cancelled { Current.Log.error("Failure during nav: \(err)") } if !error.isCancelled { showEmptyState() showSwiftMessage(error: error) } } } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { refreshControl.endRefreshing() let nsError = error as NSError let shouldShowError: Bool // Handle URLError if let urlError = error as? URLError { shouldShowError = urlError.code != .cancelled if shouldShowError { Current.Log.error("Failure during content load: \(error)") } } // Handle WebKitErrorDomain errors (e.g., Code 101 - invalid URL) else if nsError.domain == "WebKitErrorDomain" { shouldShowError = !nsError.isCancelled Current.Log.error("WebKit error during content load: \(error)") } else { shouldShowError = !error.isCancelled if shouldShowError { Current.Log.error("Failure during content load: \(error)") } } if shouldShowError { showEmptyState() showSwiftMessage(error: error) } } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { refreshControl.endRefreshing() // in case the view appears again, don't reload initialURL = nil updateWebViewSettings(reason: .load) } func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) { if #available(iOS 17.0, *) { let viewModel = DownloadManagerViewModel() let downloadManager = DownloadManagerView(viewModel: viewModel) let downloadController = UIHostingController(rootView: downloadManager) presentOverlayController(controller: downloadController, animated: true) download.delegate = viewModel } } func webView( _ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void ) { lastNavigationWasServerError = false guard navigationResponse.isForMainFrame else { // we don't need to modify the response if it's for a sub-frame decisionHandler(.allow) return } guard let httpResponse = navigationResponse.response as? HTTPURLResponse, httpResponse.statusCode >= 400 else { // not an error response, we don't need to inspect at all decisionHandler(.allow) return } lastNavigationWasServerError = true // error response, let's inspect if it's restoring a page or normal navigation if navigationResponse.response.url != initialURL { // just a normal loading error decisionHandler(.allow) } else { // first: clear that saved url, it's bad initialURL = nil // it's for the restored page, let's load the default url if let webviewURL = server.info.connection.webviewURL() { decisionHandler(.cancel) load(request: URLRequest(url: webviewURL)) } else { // we don't have anything we can do about this decisionHandler(.allow) } } } // WKUIDelegate func webView( _ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void ) { let style: UIAlertController.Style = { switch webView.traitCollection.userInterfaceIdiom { case .carPlay, .phone, .tv: return .actionSheet case .mac: return .alert case .pad, .unspecified, .vision: // without a touch to tell us where, an action sheet in the middle of the screen isn't great return .alert @unknown default: return .alert } }() let alertController = UIAlertController(title: nil, message: message, preferredStyle: style) alertController.addAction(UIAlertAction(title: L10n.Alerts.Confirm.ok, style: .default, handler: { _ in completionHandler(true) })) alertController.addAction(UIAlertAction(title: L10n.Alerts.Confirm.cancel, style: .cancel, handler: { _ in completionHandler(false) })) if presentedViewController != nil { Current.Log.error("attempted to present an alert when already presenting, bailing") completionHandler(false) } else { present(alertController, animated: true, completion: nil) } } func webView( _ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void ) { let alertController = UIAlertController(title: nil, message: prompt, preferredStyle: .alert) alertController.addTextField { textField in textField.text = defaultText } alertController.addAction(UIAlertAction(title: L10n.Alerts.Prompt.ok, style: .default, handler: { _ in if let text = alertController.textFields?.first?.text { completionHandler(text) } else { completionHandler(defaultText) } })) alertController.addAction(UIAlertAction(title: L10n.Alerts.Prompt.cancel, style: .cancel, handler: { _ in completionHandler(nil) })) if presentedViewController != nil { Current.Log.error("attempted to present an alert when already presenting, bailing") completionHandler(nil) } else { present(alertController, animated: true, completion: nil) } } func webView( _ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void ) { let alertController = UIAlertController(title: nil, message: message, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: L10n.Alerts.Alert.ok, style: .default, handler: { _ in completionHandler() })) alertController.popoverPresentationController?.sourceView = self.webView if presentedViewController != nil { Current.Log.error("attempted to present an alert when already presenting, bailing") completionHandler() } else { present(alertController, animated: true, completion: nil) } } func webView( _ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void ) { decisionHandler(.grant) } } extension WebViewController: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { // Prevent scrollView from scrolling past the top or bottom if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height { scrollView.contentOffset.y = scrollView.contentSize.height - scrollView.bounds.height } } } extension WebViewController: UIGestureRecognizerDelegate { func gestureRecognizer( _ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer ) -> Bool { true } }