import OpenClawKit import SwiftUI struct GatewayQuickSetupSheet: View { @Environment(NodeAppModel.self) private var appModel @Environment(GatewayConnectionController.self) private var gatewayController @Environment(\.dismiss) private var dismiss @AppStorage("onboarding.quickSetupDismissed") private var quickSetupDismissed: Bool = false @State private var connecting: Bool = false @State private var connectError: String? @State private var showGatewayProblemDetails: Bool = false var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 16) { Text("Connect to a Gateway?") .font(OpenClawType.title2) if let gatewayProblem = self.appModel.lastGatewayProblem { GatewayProblemBanner( problem: gatewayProblem, primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem), onPrimaryAction: { Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) } }, onShowDetails: { self.showGatewayProblemDetails = true }) } if let candidate = self.bestCandidate { GatewayQuickSetupCandidatePanel( name: candidate.name, debugID: candidate.debugID, discoveryStatusText: self.gatewayController.discoveryStatusText, gatewayDisplayStatusText: self.appModel.gatewayDisplayStatusText, nodeStatusText: self.appModel.nodeStatusText, operatorStatusText: self.appModel.operatorStatusText) Button { self.connectError = nil self.connecting = true Task { let err = await self.gatewayController.connectWithDiagnostics(candidate) await MainActor.run { self.connecting = false self.connectError = err } } } label: { Group { if self.connecting { HStack(spacing: 8) { ProgressView().progressViewStyle(.circular) Text("Connecting…") .font(OpenClawType.headline) } } else { Text("Connect") .font(OpenClawType.headline) } } } .font(OpenClawType.headline) .openClawPrimaryButton() .disabled(self.connecting) if let connectError { Text(connectError) .font(OpenClawType.footnote) .foregroundStyle(.secondary) .textSelection(.enabled) } Button { self.dismiss() } label: { Text("Not now") .font(OpenClawType.headline) } .font(OpenClawType.headline) .openClawSecondaryButton() .disabled(self.connecting) self.fullRowToggle("Don’t show this again", isOn: self.$quickSetupDismissed) .padding(.top, 4) } else { Text("No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.") .font(OpenClawType.subhead) .foregroundStyle(.secondary) } Spacer() } .font(OpenClawType.body) .padding() .navigationTitle("Quick Setup") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { Text("Quick Setup") .font(OpenClawType.headline) } ToolbarItem(placement: .topBarTrailing) { Button { self.quickSetupDismissed = true self.dismiss() } label: { Text("Close") .font(OpenClawType.subheadSemiBold) } .font(OpenClawType.subheadSemiBold) } } } .sheet(isPresented: self.$showGatewayProblemDetails) { if let gatewayProblem = self.appModel.lastGatewayProblem { GatewayProblemDetailsSheet( problem: gatewayProblem, primaryActionTitle: self.gatewayProblemPrimaryActionTitle(gatewayProblem), onPrimaryAction: { Task { await self.handleGatewayProblemPrimaryAction(gatewayProblem) } }) } } } private var bestCandidate: GatewayDiscoveryModel.DiscoveredGateway? { // Prefer whatever discovery says is first; the list is already name-sorted. self.gatewayController.gateways.first } private func fullRowToggle(_ title: LocalizedStringKey, isOn: Binding) -> some View { Toggle(isOn: isOn) { Text(title) .font(OpenClawType.subhead) } .font(OpenClawType.subhead) .contentShape(Rectangle()) .overlay { // Keep Toggle semantics for accessibility while making the full visual row tappable. Button { isOn.wrappedValue.toggle() } label: { Rectangle() .fill(.clear) .contentShape(Rectangle()) } .font(OpenClawType.subhead) .buttonStyle(.plain) .accessibilityHidden(true) } } private func gatewayProblemPrimaryActionTitle(_ problem: GatewayConnectionProblem) -> String? { GatewayProblemPrimaryAction.title(for: problem, retryTitle: "Connect") } private func handleGatewayProblemPrimaryAction(_ problem: GatewayConnectionProblem) async { if problem.canTrustRotatedCertificate { _ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem) return } if GatewayProblemPrimaryAction.openProtocolMismatchHelpIfNeeded(problem) { return } guard problem.retryable else { return } guard let candidate = self.bestCandidate else { return } self.connectError = nil self.connecting = true let err = await self.gatewayController.connectWithDiagnostics(candidate) self.connecting = false self.connectError = err } } private struct GatewayQuickSetupCandidatePanel: View { private static let readableMonospaceWidth: CGFloat = 72 * 8 let name: String let debugID: String let discoveryStatusText: String let gatewayDisplayStatusText: String let nodeStatusText: String let operatorStatusText: String var body: some View { VStack(alignment: .leading, spacing: 6) { Text(verbatim: self.name) .font(OpenClawType.monoHeadline) .foregroundStyle(.primary) Text(verbatim: self.debugID) .font(OpenClawType.monoFootnote) .foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 2) { // Use verbatim strings so Bonjour-provided values can't be interpreted as // localized format strings (which can crash with Objective-C exceptions). Text(verbatim: "Discovery: \(self.discoveryStatusText)") .font(OpenClawType.monoFootnote) Text(verbatim: "Status: \(self.gatewayDisplayStatusText)") .font(OpenClawType.monoFootnote) Text(verbatim: "Node: \(self.nodeStatusText)") .font(OpenClawType.monoFootnote) Text(verbatim: "Operator: \(self.operatorStatusText)") .font(OpenClawType.monoFootnote) } .font(OpenClawType.monoFootnote) .foregroundStyle(.secondary) } .frame(maxWidth: Self.readableMonospaceWidth, alignment: .leading) .padding(.vertical, 14) .padding(.horizontal, 16) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) .background(OpenClawBrand.obsidian) .clipShape(RoundedRectangle(cornerRadius: OpenClawRadius.lg)) } }