import CryptoKit import Foundation import OpenClawKit import SwiftUI #if canImport(WebKit) && (os(iOS) || os(macOS)) import Security import WebKit #if os(iOS) import UIKit #elseif os(macOS) import AppKit import UniformTypeIdentifiers #endif #endif enum OpenClawChatWidgetSurfaceRole: Sendable, Hashable { case node case operatorSurface case legacy } public struct OpenClawChatWidgetResource: Sendable, Equatable { public let url: URL public let tlsFingerprintSHA256: String? let surfaceRole: OpenClawChatWidgetSurfaceRole let attemptedSurfaceRoles: Set public init(url: URL, tlsFingerprintSHA256: String? = nil) { self.url = url self.tlsFingerprintSHA256 = tlsFingerprintSHA256 self.surfaceRole = .legacy self.attemptedSurfaceRoles = [] } init( url: URL, tlsFingerprintSHA256: String?, surfaceRole: OpenClawChatWidgetSurfaceRole, attemptedSurfaceRoles: Set) { self.url = url self.tlsFingerprintSHA256 = tlsFingerprintSHA256 self.surfaceRole = surfaceRole self.attemptedSurfaceRoles = attemptedSurfaceRoles } public static func == (lhs: Self, rhs: Self) -> Bool { lhs.url == rhs.url && lhs.tlsFingerprintSHA256 == rhs.tlsFingerprintSHA256 } } public enum OpenClawChatWidgetURLResolver { private static let documentsPath = "/__openclaw__/canvas/documents" public static func resolve(surfaceURL rawSurfaceURL: String?, target rawTarget: String) -> URL? { guard let target = self.relativeWidgetTarget(rawTarget), var surface = self.capabilitySurface(rawSurfaceURL) else { return nil } var surfacePath = surface.percentEncodedPath while surfacePath.hasSuffix("/") { surfacePath.removeLast() } surface.percentEncodedPath = surfacePath + target.percentEncodedPath surface.percentEncodedQuery = target.percentEncodedQuery surface.fragment = target.fragment return surface.url } public static func supportsTarget(_ rawTarget: String) -> Bool { self.relativeWidgetTarget(rawTarget) != nil } public static func resolveResource( target: String, replacing failedResource: OpenClawChatWidgetResource?, currentSurfaceRoutes: @Sendable () async -> ( node: GatewayCanvasHostRoute?, operatorSurface: GatewayCanvasHostRoute?), refreshNodeSurfaceRoute: @Sendable (GatewayCanvasHostRoute?) async -> GatewayCanvasHostRoute?, refreshOperatorSurfaceRoute: @Sendable (GatewayCanvasHostRoute?) async -> GatewayCanvasHostRoute?) async -> OpenClawChatWidgetResource? { let observed = await currentSurfaceRoutes() guard let failedResource else { return self.resolvePreferred( surfaces: observed, target: target, excluding: nil, blockedRoles: [], attemptedRoles: []) } let blockedRoles = failedResource.attemptedSurfaceRoles if failedResource.surfaceRole == .legacy, blockedRoles.contains(.legacy) { return nil } let attemptedRoles = blockedRoles.union([failedResource.surfaceRole]) if !blockedRoles.contains(.node), let nodeSurface = observed.node, let currentNode = self.resolve( surface: nodeSurface, role: .node, target: target, attemptedRoles: attemptedRoles), self.isReplacement(currentNode, for: failedResource) { return currentNode } if !blockedRoles.contains(.node), let refreshedSurface = await refreshNodeSurfaceRoute(observed.node), let refreshed = resolve( surface: refreshedSurface, role: .node, target: target, attemptedRoles: attemptedRoles), self.isReplacement(refreshed, for: failedResource) { return refreshed } // A nil refresh can mean its route lease lost a reconnect race. Re-read // both roles so a replacement connection and its TLS pin win together. let afterNodeRefresh = await currentSurfaceRoutes() if let replacement = self.resolvePreferred( surfaces: afterNodeRefresh, target: target, excluding: failedResource, blockedRoles: blockedRoles, attemptedRoles: attemptedRoles) { return replacement } if !blockedRoles.contains(.operatorSurface), let refreshedSurface = await refreshOperatorSurfaceRoute(afterNodeRefresh.operatorSurface), let refreshed = resolve( surface: refreshedSurface, role: .operatorSurface, target: target, attemptedRoles: attemptedRoles), self.isReplacement(refreshed, for: failedResource) { return refreshed } return await self.resolvePreferred( surfaces: currentSurfaceRoutes(), target: target, excluding: failedResource, blockedRoles: blockedRoles, attemptedRoles: attemptedRoles) } private static func relativeWidgetTarget(_ rawTarget: String) -> URLComponents? { let target = rawTarget.trimmingCharacters(in: .whitespacesAndNewlines) guard target.hasPrefix("/"), let components = URLComponents(string: target), components.scheme == nil, components.host == nil, components.user == nil, components.password == nil, self.isCanonicalPath(components.percentEncodedPath), components.percentEncodedPath.hasPrefix("\(self.documentsPath)/") else { return nil } return components } private static func capabilitySurface(_ rawSurfaceURL: String?) -> URLComponents? { let raw = rawSurfaceURL?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !raw.isEmpty, let components = URLComponents(string: raw), self.isWebURL(components), components.user == nil, components.password == nil, components.percentEncodedQuery == nil, components.fragment == nil else { return nil } let segments = components.percentEncodedPath.split(separator: "/", omittingEmptySubsequences: true) guard segments.count >= 3, segments[segments.count - 3] == "__openclaw__", segments[segments.count - 2] == "cap", let capability = String(segments[segments.count - 1]).removingPercentEncoding, !capability.isEmpty else { return nil } return components } private static func resolve( surface: GatewayCanvasHostRoute, role: OpenClawChatWidgetSurfaceRole, target: String, attemptedRoles: Set) -> OpenClawChatWidgetResource? { guard let url = resolve(surfaceURL: surface.url, target: target) else { return nil } let resource = OpenClawChatWidgetResource( url: url, tlsFingerprintSHA256: surface.tlsFingerprintSHA256, surfaceRole: role, attemptedSurfaceRoles: attemptedRoles) return resource.hasValidTLSBinding ? resource : nil } private static func resolvePreferred( surfaces: (node: GatewayCanvasHostRoute?, operatorSurface: GatewayCanvasHostRoute?), target: String, excluding failedResource: OpenClawChatWidgetResource?, blockedRoles: Set, attemptedRoles: Set) -> OpenClawChatWidgetResource? { [ (role: OpenClawChatWidgetSurfaceRole.node, surface: surfaces.node), (role: OpenClawChatWidgetSurfaceRole.operatorSurface, surface: surfaces.operatorSurface), ] .lazy .filter { !blockedRoles.contains($0.role) } .compactMap { candidate in candidate.surface.flatMap { self.resolve( surface: $0, role: candidate.role, target: target, attemptedRoles: attemptedRoles) } } .first { self.isReplacement($0, for: failedResource) } } private static func isReplacement( _ candidate: OpenClawChatWidgetResource, for failedResource: OpenClawChatWidgetResource?) -> Bool { guard let failedResource else { return true } // Legacy URL-only callers cannot express trust identity, so retain // their URL-only exclusion while resource-aware callers compare both. if failedResource.surfaceRole == .legacy, failedResource.tlsFingerprintSHA256 == nil { return candidate.url != failedResource.url } return candidate.url != failedResource.url || candidate.tlsFingerprintSHA256 != failedResource.tlsFingerprintSHA256 } private static func isWebURL(_ components: URLComponents) -> Bool { let scheme = components.scheme?.lowercased() return (scheme == "http" || scheme == "https") && components.host?.isEmpty == false } private static func isCanonicalPath(_ path: String) -> Bool { let segments = path.split(separator: "/", omittingEmptySubsequences: false) guard segments.first?.isEmpty == true else { return false } for (index, encodedSegment) in segments.enumerated() { if index == 0 || (index == segments.count - 1 && encodedSegment.isEmpty) { continue } guard !encodedSegment.isEmpty else { return false } guard let segment = self.decodeRepeatedly(String(encodedSegment)) else { return false } if segment == "." || segment == ".." || segment.contains("/") || segment.contains("\\") { return false } } return true } private static func decodeRepeatedly(_ encoded: String) -> String? { var value = encoded for _ in 0..<8 { guard let decoded = value.removingPercentEncoding else { return nil } if decoded == value { return decoded } value = decoded } return nil } } enum ChatInlineWidgetExport { static func filename(title: String?) -> String { var name = title ?? "" name.removeAll { character in character == "/" || character == "\\" || character.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains) } name = name.trimmingCharacters(in: .whitespacesAndNewlines) return "\(name.isEmpty ? "widget" : name).png" } } @MainActor struct ChatInlineWidgetView: View { let preview: OpenClawChatCanvasPreview let resolverReady: Bool let resolveResource: @MainActor @Sendable ( String, OpenClawChatWidgetResource?) async -> OpenClawChatWidgetResource? @State private var resolvedResource: OpenClawChatWidgetResource? @State private var recoveryAttempts = 0 @State private var refreshInFlight = false @State private var unavailable = false @State private var activePath: String? /// A reset can keep the same path while installing a new connection route. /// Its generation prevents older resolver completions from restoring stale trust state. @State private var loadGeneration = UUID() #if canImport(WebKit) && (os(iOS) || os(macOS)) @State private var snapshotRequest: ChatInlineWidgetSnapshotRequest? @State private var exportErrorMessage: String? #if os(iOS) @State private var sharedImage: ChatInlineWidgetSharedImage? #endif private var isPresentingExportError: Binding { Binding( get: { self.exportErrorMessage != nil }, set: { if !$0 { self.exportErrorMessage = nil } }) } #endif var body: some View { VStack(alignment: .leading, spacing: 6) { if let title = self.preview.title?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty { Text(title) .font(OpenClawChatTypography.footnote) .fontWeight(.semibold) .foregroundStyle(OpenClawChatTheme.muted) } #if canImport(WebKit) && (os(iOS) || os(macOS)) if let resolvedResource { self.renderedWidget(resource: resolvedResource) } else if self.unavailable { Text("Widget unavailable") .font(OpenClawChatTypography.footnote) .foregroundStyle(OpenClawChatTheme.muted) } else { ProgressView() .controlSize(.small) .frame(maxWidth: .infinity, minHeight: 44) } #else Text("Widget unavailable") .font(OpenClawChatTypography.footnote) .foregroundStyle(OpenClawChatTheme.muted) #endif } .task(id: LoadID(path: self.preview.inlineWidgetPath, resolverReady: self.resolverReady)) { let path = self.preview.inlineWidgetPath if self.activePath != path { self.reset(path: path) } guard self.resolverReady else { return } self.reset(path: path) guard let path else { self.unavailable = true return } await self.load(path: path, replacing: nil, generation: self.loadGeneration) } #if canImport(WebKit) && (os(iOS) || os(macOS)) .alert("Widget export failed", isPresented: self.isPresentingExportError) { Button(role: .cancel) { self.exportErrorMessage = nil } label: { Text("OK") .font(OpenClawChatTypography.body) } } message: { if let exportErrorMessage { Text(exportErrorMessage) .font(OpenClawChatTypography.body) } } #if os(iOS) .sheet(item: self.$sharedImage) { item in ChatInlineWidgetShareSheet(image: item.image) } #endif #endif } #if canImport(WebKit) && (os(iOS) || os(macOS)) private func renderedWidget(resource: OpenClawChatWidgetResource) -> some View { ChatInlineWidgetWebView( resource: resource, allowsScripts: self.preview.sandbox == "scripts", snapshotRequest: self.snapshotRequest, onFailure: { self.handleLoadFailure(resource: resource) }, onSnapshot: self.handleSnapshot) .id([ resource.url.absoluteString, resource.tlsFingerprintSHA256 ?? "", self.preview.sandbox ?? "", ].joined(separator: "\u{0}")) .frame(height: self.preview.inlineWidgetHeight) .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 10, style: .continuous) .stroke(OpenClawChatTheme.muted.opacity(0.24), lineWidth: 1) } .contentShape(Rectangle()) .contextMenu { Button { self.requestSnapshot(for: .copy) } label: { Text("Copy image") .font(OpenClawChatTypography.body) } Button { self.requestSnapshot(for: .save) } label: { #if os(macOS) Text("Save image…") .font(OpenClawChatTypography.body) #else Text("Save image") .font(OpenClawChatTypography.body) #endif } } } private func requestSnapshot(for action: ChatInlineWidgetSnapshotRequest.Action) { self.snapshotRequest = ChatInlineWidgetSnapshotRequest(action: action) } private func handleSnapshot(_ outcome: ChatInlineWidgetSnapshotOutcome) { switch outcome { case let .failure(request): self.clearSnapshotRequest(ifMatching: request) self.exportErrorMessage = String(localized: "The widget image could not be captured.") case let .success(request, image): self.clearSnapshotRequest(ifMatching: request) switch request.action { case .copy: self.copySnapshot(image) case .save: self.saveSnapshot(image) } } } private func clearSnapshotRequest(ifMatching completedRequest: ChatInlineWidgetSnapshotRequest) { guard self.snapshotRequest?.id == completedRequest.id else { return } self.snapshotRequest = nil } #if os(iOS) private func copySnapshot(_ image: ChatInlineWidgetSnapshotImage) { UIPasteboard.general.image = image } private func saveSnapshot(_ image: ChatInlineWidgetSnapshotImage) { self.sharedImage = ChatInlineWidgetSharedImage(image: image) } #elseif os(macOS) private func copySnapshot(_ image: ChatInlineWidgetSnapshotImage) { let pasteboard = NSPasteboard.general pasteboard.clearContents() guard pasteboard.writeObjects([image]) else { self.exportErrorMessage = String(localized: "The widget image could not be copied.") return } } private func saveSnapshot(_ image: ChatInlineWidgetSnapshotImage) { guard let pngData = image.chatInlineWidgetPNGData else { self.exportErrorMessage = String(localized: "The widget image could not be encoded as PNG.") return } let panel = NSSavePanel() panel.allowedContentTypes = [.png] panel.nameFieldStringValue = ChatInlineWidgetExport.filename(title: self.preview.title) panel.begin { response in guard response == .OK, let url = panel.url else { return } do { try pngData.write(to: url, options: .atomic) } catch { self.exportErrorMessage = String(localized: "The widget image could not be saved.") } } } #endif #endif private struct LoadID: Hashable { let path: String? let resolverReady: Bool } private func reset(path: String?) { self.loadGeneration = UUID() self.activePath = path self.resolvedResource = nil self.recoveryAttempts = 0 self.refreshInFlight = false self.unavailable = false } private func load( path: String, replacing failedResource: OpenClawChatWidgetResource?, generation: UUID) async { let candidate = await self.resolveResource(path, failedResource) guard !Task.isCancelled, self.activePath == path, self.loadGeneration == generation else { return } let resource = candidate?.hasValidTLSBinding == true ? candidate : nil self.resolvedResource = resource self.unavailable = resource == nil } private func handleLoadFailure(resource: OpenClawChatWidgetResource) { guard self.resolvedResource == resource, let path = self.activePath, !self.refreshInFlight else { return } guard self.recoveryAttempts < 3 else { self.resolvedResource = nil self.unavailable = true return } self.recoveryAttempts += 1 self.refreshInFlight = true let generation = self.loadGeneration Task { @MainActor in await self.load(path: path, replacing: resource, generation: generation) guard self.activePath == path, self.loadGeneration == generation else { return } self.refreshInFlight = false } } } extension OpenClawChatWidgetResource { fileprivate var hasValidTLSBinding: Bool { self.tlsFingerprintSHA256 == nil || self.url.scheme?.lowercased() == "https" } } #if canImport(WebKit) && (os(iOS) || os(macOS)) #if os(iOS) private typealias ChatInlineWidgetSnapshotImage = UIImage #elseif os(macOS) private typealias ChatInlineWidgetSnapshotImage = NSImage #endif private struct ChatInlineWidgetSnapshotRequest: Equatable { enum Action: Equatable { case copy case save } let id = UUID() let action: Action } private enum ChatInlineWidgetSnapshotOutcome { case success(ChatInlineWidgetSnapshotRequest, ChatInlineWidgetSnapshotImage) case failure(ChatInlineWidgetSnapshotRequest) } enum ChatInlineWidgetTLSPin { static func normalize(_ raw: String) -> String? { let stripped = raw.replacingOccurrences( of: #"(?i)^sha-?256\s*:?\s*"#, with: "", options: .regularExpression) let normalized = stripped.lowercased().filter(\.isHexDigit) return normalized.count == 64 ? normalized : nil } static func fingerprint(certificateData: Data) -> String { SHA256.hash(data: certificateData).map { String(format: "%02x", $0) }.joined() } static func matches(_ expected: String, trust: SecTrust) -> Bool { guard let expected = normalize(expected), let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate], let certificate = chain.first else { return false } return self.fingerprint(certificateData: SecCertificateCopyData(certificate) as Data) == expected } } struct ChatInlineWidgetContentProcessRecovery { enum Action: Equatable { case reload case fail } private var didReload = false mutating func nextAction() -> Action { guard !self.didReload else { return .fail } self.didReload = true return .reload } mutating func reset() { self.didReload = false } } @MainActor private final class ChatInlineWidgetNavigationDelegate: NSObject, WKNavigationDelegate { var resource: OpenClawChatWidgetResource { didSet { if self.resource != oldValue { self.contentProcessRecovery.reset() } } } let onFailure: @MainActor @Sendable () -> Void var onSnapshot: @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void private var contentProcessRecovery = ChatInlineWidgetContentProcessRecovery() private var lastSnapshotRequestID: UUID? init( resource: OpenClawChatWidgetResource, onFailure: @escaping @MainActor @Sendable () -> Void, onSnapshot: @escaping @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void) { self.resource = resource self.onFailure = onFailure self.onSnapshot = onSnapshot } func captureSnapshot(_ request: ChatInlineWidgetSnapshotRequest?, from webView: WKWebView) { guard let request, request.id != self.lastSnapshotRequestID else { return } self.lastSnapshotRequestID = request.id let configuration = WKSnapshotConfiguration() webView.takeSnapshot(with: configuration) { [weak self] image, _ in guard let self else { return } if let image { self.onSnapshot(.success(request, image)) } else { self.onSnapshot(.failure(request)) } } } func webView( _: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) { if navigationAction.targetFrame?.isMainFrame == false { decisionHandler(.cancel) return } guard navigationAction.request.httpMethod?.caseInsensitiveCompare("GET") == .orderedSame, let url = navigationAction.request.url, self.matchesExpectedDocument(url) else { decisionHandler(.cancel) return } decisionHandler(.allow) } func webView( _: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping @MainActor @Sendable (WKNavigationResponsePolicy) -> Void) { if navigationResponse.isForMainFrame, let response = navigationResponse.response as? HTTPURLResponse, response.statusCode >= 400 { self.onFailure() decisionHandler(.cancel) return } decisionHandler(.allow) } func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation?, withError _: any Error) { self.onFailure() } func webView(_: WKWebView, didFail _: WKNavigation?, withError _: any Error) { self.onFailure() } func webView( _: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping @MainActor @Sendable ( URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let expectedFingerprint = resource.tlsFingerprintSHA256 else { completionHandler(.performDefaultHandling, nil) return } guard self.matchesExpectedProtectionSpace(challenge.protectionSpace), let trust = challenge.protectionSpace.serverTrust, ChatInlineWidgetTLSPin.matches(expectedFingerprint, trust: trust) else { completionHandler(.cancelAuthenticationChallenge, nil) self.onFailure() return } completionHandler(.useCredential, URLCredential(trust: trust)) } func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { // One same-document recovery handles incidental process loss. A second // termination enters the view's bounded capability-refresh/failure path. switch self.contentProcessRecovery.nextAction() { case .reload: webView.load(URLRequest(url: self.resource.url, cachePolicy: .reloadIgnoringLocalCacheData)) case .fail: self.onFailure() } } private func matchesExpectedDocument(_ candidate: URL) -> Bool { guard var expected = URLComponents(url: self.resource.url, resolvingAgainstBaseURL: false), var candidate = URLComponents(url: candidate, resolvingAgainstBaseURL: false) else { return false } expected.fragment = nil candidate.fragment = nil return expected == candidate } private func matchesExpectedProtectionSpace(_ protectionSpace: URLProtectionSpace) -> Bool { guard let expectedHost = self.resource.url.host, protectionSpace.host.caseInsensitiveCompare(expectedHost) == .orderedSame else { return false } let expectedPort = self.resource.url.port ?? (self.resource.url.scheme?.lowercased() == "https" ? 443 : 80) return protectionSpace.port == expectedPort } } @MainActor private func makeChatInlineWidgetWebView( resource: OpenClawChatWidgetResource, allowsScripts: Bool, coordinator: ChatInlineWidgetNavigationDelegate) -> WKWebView { let configuration = WKWebViewConfiguration() configuration.websiteDataStore = .nonPersistent() configuration.defaultWebpagePreferences.allowsContentJavaScript = allowsScripts configuration.preferences.javaScriptCanOpenWindowsAutomatically = false let webView = WKWebView(frame: .zero, configuration: configuration) webView.navigationDelegate = coordinator webView.allowsLinkPreview = false webView.load(URLRequest(url: resource.url, cachePolicy: .reloadIgnoringLocalCacheData)) return webView } #if os(iOS) private struct ChatInlineWidgetWebView: UIViewRepresentable { let resource: OpenClawChatWidgetResource let allowsScripts: Bool let snapshotRequest: ChatInlineWidgetSnapshotRequest? let onFailure: @MainActor @Sendable () -> Void let onSnapshot: @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void func makeCoordinator() -> ChatInlineWidgetNavigationDelegate { ChatInlineWidgetNavigationDelegate( resource: self.resource, onFailure: self.onFailure, onSnapshot: self.onSnapshot) } func makeUIView(context: Context) -> WKWebView { makeChatInlineWidgetWebView( resource: self.resource, allowsScripts: self.allowsScripts, coordinator: context.coordinator) } func updateUIView(_ webView: WKWebView, context: Context) { context.coordinator.onSnapshot = self.onSnapshot if context.coordinator.resource != self.resource { context.coordinator.resource = self.resource webView.load(URLRequest(url: self.resource.url, cachePolicy: .reloadIgnoringLocalCacheData)) } context.coordinator.captureSnapshot(self.snapshotRequest, from: webView) } static func dismantleUIView(_ webView: WKWebView, coordinator: ChatInlineWidgetNavigationDelegate) { webView.stopLoading() webView.navigationDelegate = nil } } #elseif os(macOS) private struct ChatInlineWidgetWebView: NSViewRepresentable { let resource: OpenClawChatWidgetResource let allowsScripts: Bool let snapshotRequest: ChatInlineWidgetSnapshotRequest? let onFailure: @MainActor @Sendable () -> Void let onSnapshot: @MainActor @Sendable (ChatInlineWidgetSnapshotOutcome) -> Void func makeCoordinator() -> ChatInlineWidgetNavigationDelegate { ChatInlineWidgetNavigationDelegate( resource: self.resource, onFailure: self.onFailure, onSnapshot: self.onSnapshot) } func makeNSView(context: Context) -> WKWebView { makeChatInlineWidgetWebView( resource: self.resource, allowsScripts: self.allowsScripts, coordinator: context.coordinator) } func updateNSView(_ webView: WKWebView, context: Context) { context.coordinator.onSnapshot = self.onSnapshot if context.coordinator.resource != self.resource { context.coordinator.resource = self.resource webView.load(URLRequest(url: self.resource.url, cachePolicy: .reloadIgnoringLocalCacheData)) } context.coordinator.captureSnapshot(self.snapshotRequest, from: webView) } static func dismantleNSView(_ webView: WKWebView, coordinator: ChatInlineWidgetNavigationDelegate) { webView.stopLoading() webView.navigationDelegate = nil } } #endif #if os(iOS) private struct ChatInlineWidgetSharedImage: Identifiable { let id = UUID() let image: UIImage } private struct ChatInlineWidgetShareSheet: UIViewControllerRepresentable { let image: UIImage func makeUIViewController(context _: Context) -> UIActivityViewController { UIActivityViewController(activityItems: [self.image], applicationActivities: nil) } func updateUIViewController(_: UIActivityViewController, context _: Context) {} } #elseif os(macOS) extension NSImage { fileprivate var chatInlineWidgetPNGData: Data? { guard let tiffRepresentation, let representation = NSBitmapImageRep(data: tiffRepresentation) else { return nil } return representation.representation(using: .png, properties: [:]) } } #endif #endif