From a590ec98490c3ea937d010a57bf820e84600ec44 Mon Sep 17 00:00:00 2001 From: Nimrod Gutman Date: Mon, 16 Feb 2026 08:10:48 +0200 Subject: [PATCH] feat(ios): add onboarding reset in gateway advanced settings --- .../Gateway/GatewaySettingsStore.swift | 19 ++++++ apps/ios/Sources/Settings/SettingsTab.swift | 62 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift index fa4d2953d68..3ff57ad2e67 100644 --- a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift +++ b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift @@ -207,6 +207,25 @@ enum GatewaySettingsStore { return .manual(host: host, port: port, useTLS: useTLS, stableID: stableID) } + static func clearLastGatewayConnection(defaults: UserDefaults = .standard) { + defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayTlsDefaultsKey) + defaults.removeObject(forKey: self.lastGatewayStableIDDefaultsKey) + } + + static func deleteGatewayCredentials(instanceId: String) { + let trimmed = instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return } + _ = KeychainStore.delete( + service: self.gatewayService, + account: self.gatewayTokenAccount(instanceId: trimmed)) + _ = KeychainStore.delete( + service: self.gatewayService, + account: self.gatewayPasswordAccount(instanceId: trimmed)) + } + static func loadGatewayClientIdOverride(stableID: String) -> String? { let trimmedID = stableID.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedID.isEmpty else { return nil } diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index 9d731efddf0..ddaf2821b93 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -28,6 +28,12 @@ struct SettingsTab: View { @AppStorage("gateway.manual.tls") private var manualGatewayTLS: Bool = true @AppStorage("gateway.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false + + // Onboarding control (RootCanvas listens to onboarding.requestID and force-opens the wizard). + @AppStorage("onboarding.requestID") private var onboardingRequestID: Int = 0 + @AppStorage("gateway.onboardingComplete") private var onboardingComplete: Bool = false + @AppStorage("gateway.hasConnectedOnce") private var hasConnectedOnce: Bool = false + @State private var connectingGatewayID: String? @State private var localIPAddress: String? @State private var lastLocationModeRaw: String = OpenClawLocationMode.off.rawValue @@ -40,6 +46,9 @@ struct SettingsTab: View { @State private var gatewayExpanded: Bool = true @State private var selectedAgentPickerId: String = "" + @State private var showResetOnboardingAlert: Bool = false + @State private var suppressCredentialPersist: Bool = false + private let gatewayLogger = Logger(subsystem: "ai.openclaw.ios", category: "GatewaySettings") var body: some View { @@ -200,6 +209,10 @@ struct SettingsTab: View { SecureField("Gateway Password", text: self.$gatewayPassword) + Button("Reset Onboarding", role: .destructive) { + self.showResetOnboardingAlert = true + } + VStack(alignment: .leading, spacing: 6) { Text("Debug") .font(.footnote.weight(.semibold)) @@ -311,6 +324,15 @@ struct SettingsTab: View { .accessibilityLabel("Close") } } + .alert("Reset Onboarding?", isPresented: self.$showResetOnboardingAlert) { + Button("Reset", role: .destructive) { + self.resetOnboarding() + } + Button("Cancel", role: .cancel) {} + } message: { + Text( + "This will disconnect, clear saved gateway connection + credentials, and reopen the onboarding wizard.") + } .onAppear { self.localIPAddress = NetworkInterfaces.primaryIPv4Address() self.lastLocationModeRaw = self.locationEnabledModeRaw @@ -340,12 +362,14 @@ struct SettingsTab: View { GatewaySettingsStore.savePreferredGatewayStableID(trimmed) } .onChange(of: self.gatewayToken) { _, newValue in + guard !self.suppressCredentialPersist else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) guard !instanceId.isEmpty else { return } GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId) } .onChange(of: self.gatewayPassword) { _, newValue in + guard !self.suppressCredentialPersist else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) guard !instanceId.isEmpty else { return } @@ -865,6 +889,44 @@ struct SettingsTab: View { SettingsNetworkingHelpers.httpURLString(host: host, port: port, fallback: fallback) } + private func resetOnboarding() { + // Disconnect first so RootCanvas doesn't instantly mark onboarding complete again. + self.appModel.disconnectGateway() + self.connectingGatewayID = nil + self.setupStatusText = nil + self.setupCode = "" + self.gatewayAutoConnect = false + + self.suppressCredentialPersist = true + defer { self.suppressCredentialPersist = false } + + self.gatewayToken = "" + self.gatewayPassword = "" + + let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedInstanceId.isEmpty { + GatewaySettingsStore.deleteGatewayCredentials(instanceId: trimmedInstanceId) + } + + // Reset onboarding state + clear saved gateway connection (the two things RootCanvas checks). + GatewaySettingsStore.clearLastGatewayConnection() + OnboardingStateStore.markIncomplete() + + // RootCanvas also short-circuits onboarding when these are true. + self.onboardingComplete = false + self.hasConnectedOnce = false + + // Clear manual override so it doesn't count as an existing gateway config. + self.manualGatewayEnabled = false + self.manualGatewayHost = "" + + // Force re-present even without app restart. + self.onboardingRequestID += 1 + + // The onboarding wizard is presented from RootCanvas; dismiss Settings so it can show. + self.dismiss() + } + private func gatewayDetailLines(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> [String] { var lines: [String] = [] if let lanHost = gateway.lanHost { lines.append("LAN: \(lanHost)") }