mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 23:40:45 +00:00
355 lines
13 KiB
Swift
355 lines
13 KiB
Swift
import Foundation
|
||
import SwiftUI
|
||
|
||
struct GatewayOnboardingView: View {
|
||
var body: some View {
|
||
NavigationStack {
|
||
List {
|
||
Section {
|
||
Text("Connect to your gateway to get started.")
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
|
||
Section {
|
||
NavigationLink("Auto detect") {
|
||
AutoDetectStep()
|
||
}
|
||
NavigationLink("Manual entry") {
|
||
ManualEntryStep()
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("Connect Gateway")
|
||
}
|
||
.gatewayTrustPromptAlert()
|
||
}
|
||
}
|
||
|
||
private struct AutoDetectStep: View {
|
||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
|
||
@AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = ""
|
||
@AppStorage("gateway.lastDiscoveredStableID") private var lastDiscoveredGatewayStableID: String = ""
|
||
|
||
@State private var connectingGatewayID: String?
|
||
@State private var connectStatusText: String?
|
||
|
||
var body: some View {
|
||
Form {
|
||
Section {
|
||
Text("We’ll scan for gateways on your network and connect automatically when we find one.")
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
|
||
Section("Connection status") {
|
||
ConnectionStatusBox(
|
||
statusLines: self.connectionStatusLines(),
|
||
secondaryLine: self.connectStatusText)
|
||
}
|
||
|
||
Section {
|
||
Button("Retry") {
|
||
self.resetConnectionState()
|
||
self.triggerAutoConnect()
|
||
}
|
||
.disabled(self.connectingGatewayID != nil)
|
||
}
|
||
}
|
||
.navigationTitle("Auto detect")
|
||
.onAppear { self.triggerAutoConnect() }
|
||
.onChange(of: self.gatewayController.gateways) { _, _ in
|
||
self.triggerAutoConnect()
|
||
}
|
||
}
|
||
|
||
private func triggerAutoConnect() {
|
||
guard self.appModel.gatewayServerName == nil else { return }
|
||
guard self.connectingGatewayID == nil else { return }
|
||
guard let candidate = self.autoCandidate() else { return }
|
||
|
||
self.connectingGatewayID = candidate.id
|
||
Task {
|
||
defer { self.connectingGatewayID = nil }
|
||
await self.gatewayController.connect(candidate)
|
||
}
|
||
}
|
||
|
||
private func autoCandidate() -> GatewayDiscoveryModel.DiscoveredGateway? {
|
||
let preferred = self.preferredGatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let lastDiscovered = self.lastDiscoveredGatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
|
||
if !preferred.isEmpty,
|
||
let match = self.gatewayController.gateways.first(where: { $0.stableID == preferred })
|
||
{
|
||
return match
|
||
}
|
||
if !lastDiscovered.isEmpty,
|
||
let match = self.gatewayController.gateways.first(where: { $0.stableID == lastDiscovered })
|
||
{
|
||
return match
|
||
}
|
||
if self.gatewayController.gateways.count == 1 {
|
||
return self.gatewayController.gateways.first
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func connectionStatusLines() -> [String] {
|
||
ConnectionStatusBox.defaultLines(appModel: self.appModel, gatewayController: self.gatewayController)
|
||
}
|
||
|
||
private func resetConnectionState() {
|
||
self.appModel.disconnectGateway()
|
||
self.connectStatusText = nil
|
||
self.connectingGatewayID = nil
|
||
}
|
||
}
|
||
|
||
private struct ManualEntryStep: View {
|
||
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
|
||
@Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController
|
||
|
||
@State private var setupCode: String = ""
|
||
@State private var setupStatusText: String?
|
||
@State private var manualHost: String = ""
|
||
@State private var manualPortText: String = ""
|
||
@State private var manualUseTLS: Bool = true
|
||
@State private var manualToken: String = ""
|
||
@State private var manualPassword: String = ""
|
||
|
||
@State private var connectingGatewayID: String?
|
||
@State private var connectStatusText: String?
|
||
|
||
var body: some View {
|
||
Form {
|
||
Section("Setup code") {
|
||
Text("Use /pair in your bot to get a setup code.")
|
||
.font(.footnote)
|
||
.foregroundStyle(.secondary)
|
||
|
||
TextField("Paste setup code", text: self.$setupCode)
|
||
.textInputAutocapitalization(.never)
|
||
.autocorrectionDisabled()
|
||
|
||
Button("Apply setup code") {
|
||
self.applySetupCode()
|
||
}
|
||
.disabled(self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||
|
||
if let setupStatusText, !setupStatusText.isEmpty {
|
||
Text(setupStatusText)
|
||
.font(.footnote)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
Section {
|
||
TextField("Host", text: self.$manualHost)
|
||
.textInputAutocapitalization(.never)
|
||
.autocorrectionDisabled()
|
||
|
||
TextField("Port", text: self.$manualPortText)
|
||
.keyboardType(.numberPad)
|
||
|
||
Toggle("Use TLS", isOn: self.$manualUseTLS)
|
||
|
||
TextField("Gateway token", text: self.$manualToken)
|
||
.textInputAutocapitalization(.never)
|
||
.autocorrectionDisabled()
|
||
|
||
SecureField("Gateway password", text: self.$manualPassword)
|
||
.textInputAutocapitalization(.never)
|
||
.autocorrectionDisabled()
|
||
}
|
||
|
||
Section("Connection status") {
|
||
ConnectionStatusBox(
|
||
statusLines: self.connectionStatusLines(),
|
||
secondaryLine: self.connectStatusText)
|
||
}
|
||
|
||
Section {
|
||
Button {
|
||
Task { await self.connectManual() }
|
||
} label: {
|
||
if self.connectingGatewayID == "manual" {
|
||
HStack(spacing: 8) {
|
||
ProgressView()
|
||
.progressViewStyle(.circular)
|
||
Text("Connecting…")
|
||
}
|
||
} else {
|
||
Text("Connect")
|
||
}
|
||
}
|
||
.disabled(self.connectingGatewayID != nil)
|
||
|
||
Button("Retry") {
|
||
self.resetConnectionState()
|
||
self.resetManualForm()
|
||
}
|
||
.disabled(self.connectingGatewayID != nil)
|
||
}
|
||
}
|
||
.navigationTitle("Manual entry")
|
||
}
|
||
|
||
private func connectManual() async {
|
||
let host = self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !host.isEmpty else {
|
||
self.connectStatusText = "Failed: host required"
|
||
return
|
||
}
|
||
|
||
if let port = self.manualPortValue(), !(1...65535).contains(port) {
|
||
self.connectStatusText = "Failed: invalid port"
|
||
return
|
||
}
|
||
|
||
let defaults = UserDefaults.standard
|
||
defaults.set(true, forKey: "gateway.manual.enabled")
|
||
defaults.set(host, forKey: "gateway.manual.host")
|
||
defaults.set(self.manualPortValue() ?? 0, forKey: "gateway.manual.port")
|
||
defaults.set(self.manualUseTLS, forKey: "gateway.manual.tls")
|
||
|
||
if let instanceId = defaults.string(forKey: "node.instanceId")?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||
!instanceId.isEmpty
|
||
{
|
||
let trimmedToken = self.manualToken.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let trimmedPassword = self.manualPassword.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
if !trimmedToken.isEmpty {
|
||
GatewaySettingsStore.saveGatewayToken(trimmedToken, instanceId: instanceId)
|
||
}
|
||
GatewaySettingsStore.saveGatewayPassword(trimmedPassword, instanceId: instanceId)
|
||
}
|
||
|
||
self.connectingGatewayID = "manual"
|
||
defer { self.connectingGatewayID = nil }
|
||
await self.gatewayController.connectManual(
|
||
host: host,
|
||
port: self.manualPortValue() ?? 0,
|
||
useTLS: self.manualUseTLS)
|
||
}
|
||
|
||
private func manualPortValue() -> Int? {
|
||
let trimmed = self.manualPortText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !trimmed.isEmpty else { return nil }
|
||
return Int(trimmed.filter { $0.isNumber })
|
||
}
|
||
|
||
private func connectionStatusLines() -> [String] {
|
||
ConnectionStatusBox.defaultLines(appModel: self.appModel, gatewayController: self.gatewayController)
|
||
}
|
||
|
||
private func resetConnectionState() {
|
||
self.appModel.disconnectGateway()
|
||
self.connectStatusText = nil
|
||
self.connectingGatewayID = nil
|
||
}
|
||
|
||
private func resetManualForm() {
|
||
self.setupCode = ""
|
||
self.setupStatusText = nil
|
||
self.manualHost = ""
|
||
self.manualPortText = ""
|
||
self.manualUseTLS = true
|
||
self.manualToken = ""
|
||
self.manualPassword = ""
|
||
}
|
||
|
||
private func applySetupCode() {
|
||
let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
guard !raw.isEmpty else {
|
||
self.setupStatusText = "Paste a setup code to continue."
|
||
return
|
||
}
|
||
|
||
guard let payload = GatewaySetupCode.decode(raw: raw) else {
|
||
self.setupStatusText = "Setup code not recognized."
|
||
return
|
||
}
|
||
|
||
if let urlString = payload.url, let url = URL(string: urlString) {
|
||
self.applyURL(url)
|
||
} else if let host = payload.host, !host.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||
self.manualHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
if let port = payload.port {
|
||
self.manualPortText = String(port)
|
||
} else {
|
||
self.manualPortText = ""
|
||
}
|
||
if let tls = payload.tls {
|
||
self.manualUseTLS = tls
|
||
}
|
||
} else if let url = URL(string: raw), url.scheme != nil {
|
||
self.applyURL(url)
|
||
} else {
|
||
self.setupStatusText = "Setup code missing URL or host."
|
||
return
|
||
}
|
||
|
||
if let token = payload.token, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||
self.manualToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
}
|
||
if let password = payload.password, !password.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||
self.manualPassword = password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
}
|
||
|
||
self.setupStatusText = "Setup code applied."
|
||
}
|
||
|
||
private func applyURL(_ url: URL) {
|
||
guard let host = url.host, !host.isEmpty else { return }
|
||
self.manualHost = host
|
||
if let port = url.port {
|
||
self.manualPortText = String(port)
|
||
} else {
|
||
self.manualPortText = ""
|
||
}
|
||
let scheme = (url.scheme ?? "").lowercased()
|
||
if scheme == "wss" || scheme == "https" {
|
||
self.manualUseTLS = true
|
||
} else if scheme == "ws" || scheme == "http" {
|
||
self.manualUseTLS = false
|
||
}
|
||
}
|
||
|
||
// (GatewaySetupCode) decode raw setup codes.
|
||
}
|
||
|
||
private struct ConnectionStatusBox: View {
|
||
let statusLines: [String]
|
||
let secondaryLine: String?
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
ForEach(self.statusLines, id: \.self) { line in
|
||
Text(line)
|
||
.font(.system(size: 12, weight: .regular, design: .monospaced))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
if let secondaryLine, !secondaryLine.isEmpty {
|
||
Text(secondaryLine)
|
||
.font(.footnote)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(10)
|
||
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||
}
|
||
|
||
static func defaultLines(
|
||
appModel: NodeAppModel,
|
||
gatewayController: GatewayConnectionController
|
||
) -> [String] {
|
||
var lines: [String] = [
|
||
"gateway: \(appModel.gatewayStatusText)",
|
||
"discovery: \(gatewayController.discoveryStatusText)",
|
||
]
|
||
lines.append("server: \(appModel.gatewayServerName ?? "—")")
|
||
lines.append("address: \(appModel.gatewayRemoteAddress ?? "—")")
|
||
return lines
|
||
}
|
||
}
|