mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 18:14:06 +00:00
fix(ios): polish iPad gateway setup
This commit is contained in:
@@ -50,6 +50,11 @@ struct ChatProTab: View {
|
||||
.onChange(of: self.appModel.chatSessionKey) { _, _ in
|
||||
self.syncChatViewModel()
|
||||
}
|
||||
.onChange(of: self.appModel.isOperatorGatewayConnected) { _, connected in
|
||||
guard connected else { return }
|
||||
self.syncChatViewModel()
|
||||
self.viewModel?.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private var header: some View {
|
||||
@@ -151,7 +156,8 @@ struct ChatProTab: View {
|
||||
}
|
||||
|
||||
private var gatewayConnected: Bool {
|
||||
GatewayStatusBuilder.build(appModel: self.appModel) == .connected
|
||||
GatewayStatusBuilder.build(appModel: self.appModel) == .connected &&
|
||||
self.appModel.isOperatorGatewayConnected
|
||||
}
|
||||
|
||||
private var chatUserAccent: Color {
|
||||
|
||||
@@ -45,6 +45,7 @@ struct SettingsProTab: View {
|
||||
@State var gatewayPassword = ""
|
||||
@State var manualGatewayPortText = ""
|
||||
@State var setupStatusText: String?
|
||||
@State var stagedGatewaySetupLink: GatewayConnectDeepLink?
|
||||
@State var pendingManualAuthOverride: GatewayConnectionController.ManualAuthOverride?
|
||||
@State var defaultShareInstruction = ""
|
||||
@State var showGatewayProblemDetails = false
|
||||
@@ -82,6 +83,7 @@ struct SettingsProTab: View {
|
||||
self.previousLocationModeRaw = self.locationModeRaw
|
||||
self.syncSettingsState()
|
||||
self.refreshNotificationSettings()
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
}
|
||||
.onChange(of: self.scenePhase) { _, phase in
|
||||
if phase == .active {
|
||||
@@ -107,9 +109,17 @@ struct SettingsProTab: View {
|
||||
.onChange(of: self.gatewayPassword) { _, newValue in
|
||||
self.persistGatewayPassword(newValue)
|
||||
}
|
||||
.onChange(of: self.setupCode) { _, newValue in
|
||||
if !newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
self.stagedGatewaySetupLink = nil
|
||||
}
|
||||
}
|
||||
.onChange(of: self.defaultShareInstruction) { _, newValue in
|
||||
ShareToAgentSettings.saveDefaultInstruction(newValue)
|
||||
}
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.applyPendingGatewaySetupLinkIfNeeded()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: self.$showGatewayProblemDetails) {
|
||||
if let gatewayProblem = self.appModel.lastGatewayProblem {
|
||||
|
||||
@@ -202,17 +202,29 @@ extension SettingsProTab {
|
||||
await self.connectManual()
|
||||
}
|
||||
|
||||
func applyPendingGatewaySetupLinkIfNeeded() {
|
||||
guard let link = self.appModel.consumePendingGatewaySetupLink() else { return }
|
||||
self.setupCode = ""
|
||||
self.setupStatusText = nil
|
||||
self.stagedGatewaySetupLink = link
|
||||
let security = link.tls ? "TLS" : "plain"
|
||||
self.setupStatusText = "Setup link loaded for \(link.host):\(link.port) (\(security)). Tap Connect to apply."
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func applySetupCode() -> Bool {
|
||||
let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !raw.isEmpty else {
|
||||
let stagedLink = self.stagedGatewaySetupLink
|
||||
guard !raw.isEmpty || stagedLink != nil else {
|
||||
self.setupStatusText = "Paste a setup code to continue."
|
||||
return false
|
||||
}
|
||||
guard let link = GatewayConnectDeepLink.fromSetupInput(raw) else {
|
||||
|
||||
guard let link = raw.isEmpty ? stagedLink : GatewayConnectDeepLink.fromSetupInput(raw) else {
|
||||
self.setupStatusText = "Setup code not recognized or uses an insecure ws:// gateway URL."
|
||||
return false
|
||||
}
|
||||
self.stagedGatewaySetupLink = nil
|
||||
self.applyGatewayLink(link)
|
||||
return true
|
||||
}
|
||||
@@ -299,7 +311,7 @@ extension SettingsProTab {
|
||||
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return false }
|
||||
if Self.isTailnetHostOrIP(trimmed), !Self.hasTailnetIPv4() {
|
||||
self.setupStatusText = "Tailscale is off on this iPhone. Turn it on, then try again."
|
||||
self.setupStatusText = "Tailscale is off on this device. Turn it on, then try again."
|
||||
return false
|
||||
}
|
||||
self.setupStatusText = "Checking gateway reachability..."
|
||||
@@ -510,10 +522,15 @@ extension SettingsProTab {
|
||||
return gatewayStatus
|
||||
}
|
||||
|
||||
var canApplyGatewaySetup: Bool {
|
||||
!self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
|| self.stagedGatewaySetupLink != nil
|
||||
}
|
||||
|
||||
var tailnetWarningText: String? {
|
||||
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !host.isEmpty, Self.isTailnetHostOrIP(host), !Self.hasTailnetIPv4() else { return nil }
|
||||
return "This gateway is on your tailnet. Turn on Tailscale on this iPhone, then tap Connect."
|
||||
return "This gateway is on your tailnet. Turn on Tailscale on this device, then tap Connect."
|
||||
}
|
||||
|
||||
func friendlyGatewayMessage(from raw: String) -> String? {
|
||||
|
||||
@@ -542,7 +542,7 @@ extension SettingsProTab {
|
||||
{
|
||||
Task { await self.applySetupCodeAndConnect() }
|
||||
}
|
||||
.disabled(self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
.disabled(!self.canApplyGatewaySetup)
|
||||
}
|
||||
if let status = self.setupStatusLine {
|
||||
Text(status)
|
||||
|
||||
@@ -111,7 +111,7 @@ struct GatewayProblemBanner: View {
|
||||
case .gateway:
|
||||
"Fix on gateway"
|
||||
case .iphone:
|
||||
"Fix on iPhone"
|
||||
"Fix on this device"
|
||||
case .both:
|
||||
"Check both"
|
||||
case .network:
|
||||
@@ -227,9 +227,9 @@ struct GatewayProblemDetailsSheet: View {
|
||||
case .gateway:
|
||||
"Primary fix: gateway"
|
||||
case .iphone:
|
||||
"Primary fix: this iPhone"
|
||||
"Primary fix: this device"
|
||||
case .both:
|
||||
"Primary fix: check both this iPhone and the gateway"
|
||||
"Primary fix: check both this device and the gateway"
|
||||
case .network:
|
||||
"Primary fix: network or remote access"
|
||||
case .unknown:
|
||||
|
||||
@@ -138,7 +138,9 @@ final class NodeAppModel {
|
||||
var homeCanvasRevision: Int = 0
|
||||
var lastShareEventText: String = "No share events yet."
|
||||
var openChatRequestID: Int = 0
|
||||
var gatewaySetupRequestID: Int = 0
|
||||
private(set) var pendingAgentDeepLinkPrompt: AgentDeepLinkPrompt?
|
||||
private var pendingGatewaySetupLink: GatewayConnectDeepLink?
|
||||
private(set) var pendingExecApprovalPrompt: ExecApprovalPrompt?
|
||||
private(set) var pendingExecApprovalPromptResolving: Bool = false
|
||||
private(set) var pendingExecApprovalPromptErrorText: String?
|
||||
@@ -4134,11 +4136,23 @@ extension NodeAppModel {
|
||||
switch route {
|
||||
case let .agent(link):
|
||||
await self.handleAgentDeepLink(link, originalURL: url)
|
||||
case .gateway, .dashboard:
|
||||
case let .gateway(link):
|
||||
self.stageGatewaySetupLink(link)
|
||||
case .dashboard:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func stageGatewaySetupLink(_ link: GatewayConnectDeepLink) {
|
||||
self.pendingGatewaySetupLink = link
|
||||
self.gatewaySetupRequestID &+= 1
|
||||
}
|
||||
|
||||
func consumePendingGatewaySetupLink() -> GatewayConnectDeepLink? {
|
||||
defer { self.pendingGatewaySetupLink = nil }
|
||||
return self.pendingGatewaySetupLink
|
||||
}
|
||||
|
||||
private func handleAgentDeepLink(_ link: AgentDeepLink, originalURL: URL) async {
|
||||
let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !message.isEmpty else { return }
|
||||
|
||||
@@ -7,7 +7,7 @@ struct OnboardingIntroStep: View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "iphone.gen3")
|
||||
Image(systemName: UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone.gen3")
|
||||
.font(.system(size: 60, weight: .semibold))
|
||||
.foregroundStyle(.tint)
|
||||
.padding(.bottom, 18)
|
||||
@@ -17,7 +17,7 @@ struct OnboardingIntroStep: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("Turn this iPhone into a secure OpenClaw node for chat, voice, camera, and device tools.")
|
||||
Text("Turn this device into a secure OpenClaw node for chat, voice, camera, and device tools.")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -114,7 +114,7 @@ struct OnboardingWelcomeStep: View {
|
||||
.foregroundStyle(.secondary)
|
||||
Text("/pair qr")
|
||||
.font(.system(.footnote, design: .monospaced).weight(.semibold))
|
||||
Text("Then scan the QR code here to connect this iPhone.")
|
||||
Text("Then scan the QR code here to connect this device.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -669,8 +669,8 @@ extension OpenClawApp {
|
||||
switch route {
|
||||
case .agent, .dashboard:
|
||||
await self.appModel.handleDeepLink(url: url)
|
||||
case .gateway:
|
||||
break
|
||||
case let .gateway(link):
|
||||
self.appModel.stageGatewaySetupLink(link)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ struct RootTabs: View {
|
||||
@State private var didAutoOpenSettings: Bool = false
|
||||
@State private var didApplyInitialAppearance: Bool = false
|
||||
@State private var didApplyInitialChatSession: Bool = false
|
||||
@State private var handledGatewaySetupRequestID: Int = 0
|
||||
|
||||
private enum AppTab: Hashable {
|
||||
case control
|
||||
@@ -237,6 +238,7 @@ struct RootTabs: View {
|
||||
.onAppear { self.updateCanvasState() }
|
||||
.onAppear { self.evaluateOnboardingPresentation(force: false) }
|
||||
.onAppear { self.maybeAutoOpenSettings() }
|
||||
.onAppear { self.maybeOpenSettingsForGatewaySetup() }
|
||||
.onAppear { self.maybeShowQuickSetup() }
|
||||
.onAppear { self.applyInitialAppearanceIfNeeded() }
|
||||
.onAppear { self.applyInitialChatSessionIfNeeded() }
|
||||
@@ -296,6 +298,9 @@ struct RootTabs: View {
|
||||
.onChange(of: self.appModel.openChatRequestID) { _, _ in
|
||||
self.selectedTab = .chat
|
||||
}
|
||||
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
|
||||
self.maybeOpenSettingsForGatewaySetup()
|
||||
}
|
||||
}
|
||||
|
||||
private func rootPresentation(_ content: some View) -> some View {
|
||||
@@ -560,6 +565,16 @@ struct RootTabs: View {
|
||||
self.selectedTab = .settings
|
||||
}
|
||||
|
||||
private func maybeOpenSettingsForGatewaySetup() {
|
||||
let requestID = self.appModel.gatewaySetupRequestID
|
||||
guard requestID != 0, requestID != self.handledGatewaySetupRequestID else { return }
|
||||
self.handledGatewaySetupRequestID = requestID
|
||||
self.showOnboarding = false
|
||||
self.presentedSheet = nil
|
||||
self.didAutoOpenSettings = true
|
||||
self.selectedTab = .settings
|
||||
}
|
||||
|
||||
private func applyInitialChatSessionIfNeeded() {
|
||||
guard !self.didApplyInitialChatSession else { return }
|
||||
self.didApplyInitialChatSession = true
|
||||
|
||||
@@ -147,8 +147,8 @@ struct TalkPermissionPromptView: View {
|
||||
case .upgradeRequested:
|
||||
"Approve this request on your gateway. Talk will start automatically when approval lands."
|
||||
default:
|
||||
"This iPhone needs gateway approval before Talk can use realtime voice. Audio will go directly from " +
|
||||
"this phone to the voice provider."
|
||||
"This device needs gateway approval before Talk can use realtime voice. Audio will go directly from " +
|
||||
"this device to the voice provider."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway token required",
|
||||
message: authError.userMessageOverride
|
||||
?? "This gateway requires an auth token, but this iPhone did not send one.",
|
||||
?? "This gateway requires an auth token, but this device did not send one.",
|
||||
actionLabel: authError.actionLabel ?? "Open Settings",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
@@ -229,7 +229,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway token is out of date",
|
||||
message: authError.userMessageOverride
|
||||
?? "The token on this iPhone does not match the gateway token.",
|
||||
?? "The token on this device does not match the gateway token.",
|
||||
actionLabel: authError
|
||||
.actionLabel ?? (authError.canRetryWithDeviceToken ? "Retry once" : "Update gateway token"),
|
||||
actionCommand: authError.actionCommand,
|
||||
@@ -262,7 +262,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway password required",
|
||||
message: authError.userMessageOverride
|
||||
?? "This gateway requires a password, but this iPhone did not send one.",
|
||||
?? "This gateway requires a password, but this device did not send one.",
|
||||
actionLabel: authError.actionLabel ?? "Open Settings",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
@@ -278,7 +278,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "Gateway password is out of date",
|
||||
message: authError.userMessageOverride
|
||||
?? "The saved password on this iPhone does not match the gateway password.",
|
||||
?? "The saved password on this device does not match the gateway password.",
|
||||
actionLabel: authError.actionLabel ?? "Update password",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
@@ -322,7 +322,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
return self.problem(
|
||||
kind: .deviceTokenMismatch,
|
||||
owner: .both,
|
||||
title: authError.titleOverride ?? "This iPhone's saved device token is no longer valid",
|
||||
title: authError.titleOverride ?? "This device's saved device token is no longer valid",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway rejected the stored device token for this role.",
|
||||
actionLabel: authError.actionLabel ?? "Repair pairing",
|
||||
@@ -355,7 +355,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
title: authError.titleOverride ?? "Secure device identity is required",
|
||||
message: authError.userMessageOverride
|
||||
??
|
||||
"This connection must include a signed device identity before the gateway can bind permissions to this iPhone.",
|
||||
"This connection must include a signed device identity before the gateway can bind permissions to this device.",
|
||||
actionLabel: authError.actionLabel ?? "Retry from the app",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/platforms/ios"),
|
||||
@@ -369,7 +369,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .iphone,
|
||||
title: authError.titleOverride ?? "Secure handshake expired",
|
||||
message: authError.userMessageOverride ?? "The device signature is too old to use.",
|
||||
actionLabel: authError.actionLabel ?? "Check iPhone time",
|
||||
actionLabel: authError.actionLabel ?? "Check device time",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(
|
||||
authError.docsURLString,
|
||||
@@ -415,8 +415,8 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .iphone,
|
||||
title: authError.titleOverride ?? "This device identity could not be verified",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway could not verify the identity this iPhone presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
|
||||
?? "The gateway could not verify the identity this device presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this device",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
requestId: authError.requestId,
|
||||
@@ -429,8 +429,8 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .iphone,
|
||||
title: authError.titleOverride ?? "This device identity could not be verified",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway could not verify the public key this iPhone presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
|
||||
?? "The gateway could not verify the public key this device presented.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this device",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
requestId: authError.requestId,
|
||||
@@ -444,7 +444,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
title: authError.titleOverride ?? "This device identity could not be verified",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway rejected the device identity because the device ID did not match.",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this iPhone",
|
||||
actionLabel: authError.actionLabel ?? "Re-pair this device",
|
||||
actionCommand: authError.actionCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
requestId: authError.requestId,
|
||||
@@ -745,7 +745,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
title: authError.titleOverride ?? "Additional approval required",
|
||||
message: authError.userMessageOverride
|
||||
??
|
||||
"This iPhone is already paired, but it is requesting a new role that was not previously approved.",
|
||||
"This device is already paired, but it is requesting a new role that was not previously approved.",
|
||||
actionLabel: authError.actionLabel ?? "Approve on gateway",
|
||||
actionCommand: authError.actionCommand ?? pairingCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
@@ -759,7 +759,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
owner: .gateway,
|
||||
title: authError.titleOverride ?? "Additional permissions required",
|
||||
message: authError.userMessageOverride
|
||||
?? "This iPhone is already paired, but it is requesting new permissions that require approval.",
|
||||
?? "This device is already paired, but it is requesting new permissions that require approval.",
|
||||
actionLabel: authError.actionLabel ?? "Approve on gateway",
|
||||
actionCommand: authError.actionCommand ?? pairingCommand,
|
||||
docsURL: self.docsURL(authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/pairing"),
|
||||
@@ -786,7 +786,7 @@ public enum GatewayConnectionProblemMapper {
|
||||
return self.problem(
|
||||
kind: .pairingRequired,
|
||||
owner: .gateway,
|
||||
title: authError.titleOverride ?? "This iPhone is not approved yet",
|
||||
title: authError.titleOverride ?? "This device is not approved yet",
|
||||
message: authError.userMessageOverride
|
||||
?? "The gateway received the connection request, but this device must be approved first.",
|
||||
actionLabel: authError.actionLabel ?? "Approve on gateway",
|
||||
|
||||
Reference in New Issue
Block a user