import OpenClawKit import SwiftUI import WebKit struct ScreenWebView: UIViewRepresentable { var controller: ScreenController func makeCoordinator() -> ScreenWebViewCoordinator { ScreenWebViewCoordinator(controller: self.controller) } func makeUIView(context: Context) -> UIView { context.coordinator.makeContainerView() } func updateUIView(_: UIView, context: Context) { context.coordinator.updateController(self.controller) } static func dismantleUIView(_: UIView, coordinator: ScreenWebViewCoordinator) { coordinator.teardown() } } @MainActor final class ScreenWebViewCoordinator: NSObject { private weak var controller: ScreenController? private let navigationDelegate = ScreenNavigationDelegate() private let a2uiActionHandler = CanvasA2UIActionMessageHandler() private let userContentController = WKUserContentController() private(set) var managedWebView: WKWebView? private weak var containerView: UIView? init(controller: ScreenController) { self.controller = controller super.init() self.navigationDelegate.controller = controller self.a2uiActionHandler.controller = controller } func makeContainerView() -> UIView { if let containerView { return containerView } let container = UIView(frame: .zero) container.backgroundColor = .black let webView = Self.makeWebView(userContentController: self.userContentController) webView.navigationDelegate = self.navigationDelegate self.installA2UIHandlers() webView.translatesAutoresizingMaskIntoConstraints = false container.addSubview(webView) NSLayoutConstraint.activate([ webView.leadingAnchor.constraint(equalTo: container.leadingAnchor), webView.trailingAnchor.constraint(equalTo: container.trailingAnchor), webView.topAnchor.constraint(equalTo: container.topAnchor), webView.bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) self.managedWebView = webView self.containerView = container self.controller?.attachWebView(webView) return container } func updateController(_ controller: ScreenController) { let previousController = self.controller let controllerChanged = self.controller !== controller self.controller = controller self.navigationDelegate.controller = controller self.a2uiActionHandler.controller = controller if controllerChanged, let managedWebView { previousController?.detachWebView(managedWebView) controller.attachWebView(managedWebView) } } func teardown() { if let managedWebView { self.controller?.detachWebView(managedWebView) managedWebView.navigationDelegate = nil } self.removeA2UIHandlers() self.navigationDelegate.controller = nil self.a2uiActionHandler.controller = nil self.managedWebView = nil self.containerView = nil } private static func makeWebView(userContentController: WKUserContentController) -> WKWebView { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() config.userContentController = userContentController let webView = WKWebView(frame: .zero, configuration: config) // Canvas scaffold is a fully self-contained HTML page; avoid relying on transparency underlays. webView.isOpaque = true webView.backgroundColor = .black let scrollView = webView.scrollView scrollView.backgroundColor = .black scrollView.contentInsetAdjustmentBehavior = .never scrollView.contentInset = .zero scrollView.scrollIndicatorInsets = .zero scrollView.automaticallyAdjustsScrollIndicatorInsets = false return webView } private func installA2UIHandlers() { for name in CanvasA2UIActionMessageHandler.handlerNames { self.userContentController.add(self.a2uiActionHandler, name: name) } } private func removeA2UIHandlers() { for name in CanvasA2UIActionMessageHandler.handlerNames { self.userContentController.removeScriptMessageHandler(forName: name) } } } // MARK: - Navigation Delegate /// Handles navigation policy to intercept openclaw:// deep links from canvas @MainActor private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { weak var controller: ScreenController? func webView( _: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } // Intercept openclaw:// deep links. if url.scheme?.lowercased() == "openclaw" { decisionHandler(.cancel) self.controller?.onDeepLink?(url) return } decisionHandler(.allow) } func webView( _: WKWebView, didFailProvisionalNavigation _: WKNavigation?, withError error: any Error) { self.controller?.errorText = error.localizedDescription } func webView(_: WKWebView, didFinish _: WKNavigation?) { self.controller?.errorText = nil self.controller?.applyDebugStatusIfNeeded() } func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) { self.controller?.errorText = error.localizedDescription } } private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler { static let messageName = "openclawCanvasA2UIAction" static let handlerNames = [messageName] weak var controller: ScreenController? func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { guard Self.handlerNames.contains(message.name) else { return } guard let controller else { return } guard let url = message.webView?.url else { return } if url.isFileURL { guard controller.isTrustedCanvasUIURL(url) else { return } } else { // For security, only accept actions from local-network pages (e.g. the canvas host). guard controller.isLocalNetworkCanvasURL(url) else { return } } guard let body = ScreenController.parseA2UIActionBody(message.body) else { return } controller.onA2UIAction?(body) } }