Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
854
apps/ios/Sources/Design/SettingsProTabActions.swift
Normal file
854
apps/ios/Sources/Design/SettingsProTabActions.swift
Normal file
@@ -0,0 +1,854 @@
|
||||
import CoreLocation
|
||||
import OpenClawKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
extension SettingsProTab {
|
||||
func detailStatusCard(
|
||||
icon: String,
|
||||
title: String,
|
||||
detail: String,
|
||||
value: String,
|
||||
color: Color) -> some View
|
||||
{
|
||||
Section {
|
||||
HStack(spacing: 12) {
|
||||
SettingsIcon(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(OpenClawType.headline)
|
||||
Text(detail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(value)
|
||||
.font(OpenClawType.subheadMedium)
|
||||
.foregroundStyle(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var diagnosticChecksCard: some View {
|
||||
Section("Checks") {
|
||||
self.diagnosticCheckRow(
|
||||
icon: "stethoscope",
|
||||
title: "Last Run",
|
||||
detail: self.diagnosticsLastRunText,
|
||||
value: self.diagnosticsRunValue,
|
||||
color: self.diagnosticsRunColor)
|
||||
self.diagnosticCheckRow(
|
||||
icon: "antenna.radiowaves.left.and.right",
|
||||
title: "Gateway Link",
|
||||
detail: self.gatewayStatusDetail,
|
||||
value: self.gatewayStatusValue,
|
||||
color: self.gatewayStatusColor)
|
||||
self.diagnosticCheckRow(
|
||||
icon: "dot.radiowaves.left.and.right",
|
||||
title: "Discovery",
|
||||
detail: self.gatewayController.discoveryStatusText,
|
||||
value: "\(self.gatewayController.gateways.count)",
|
||||
color: self.gatewayController.gateways.isEmpty ? .secondary : OpenClawBrand.accent)
|
||||
self.diagnosticCheckRow(
|
||||
icon: "waveform",
|
||||
title: "Talk Config",
|
||||
detail: self.gatewayTalkConfigDetail,
|
||||
value: self.gatewayTalkConfigValue,
|
||||
color: self.gatewayTalkConfigColor)
|
||||
self.diagnosticCheckRow(
|
||||
icon: "bell",
|
||||
title: "Notifications",
|
||||
detail: "Approval and event alert channel",
|
||||
value: self.notificationStatusText,
|
||||
color: self.notificationStatus.color)
|
||||
self.diagnosticCheckRow(
|
||||
icon: "rectangle.on.rectangle",
|
||||
title: "Screen Capture",
|
||||
detail: "Live foreground capture state",
|
||||
value: self.appModel.screenRecordActive ? "live" : "idle",
|
||||
color: self.appModel.screenRecordActive ? OpenClawBrand.ok : .secondary)
|
||||
self.diagnosticCheckRow(
|
||||
icon: "mic",
|
||||
title: "Voice Wake",
|
||||
detail: self.appModel.voiceWake.statusText,
|
||||
value: self.voiceWakeEnabled ? "on" : "off",
|
||||
color: self.voiceWakeEnabled ? OpenClawBrand.ok : .secondary)
|
||||
}
|
||||
}
|
||||
|
||||
func diagnosticCheckRow(
|
||||
icon: String,
|
||||
title: String,
|
||||
detail: String,
|
||||
value: String,
|
||||
color: Color) -> some View
|
||||
{
|
||||
HStack(spacing: 12) {
|
||||
SettingsIcon(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text(value)
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
func detailListCard(@ViewBuilder content: () -> some View) -> some View {
|
||||
Section {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
func detailRow(_ label: String, value: String) -> some View {
|
||||
LabeledContent(label) {
|
||||
Text(value)
|
||||
.font(OpenClawType.subhead)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
|
||||
func reconnectGateway() async {
|
||||
guard !self.appModel.isAppleReviewDemoModeEnabled else { return }
|
||||
guard !self.isReconnectingGateway else { return }
|
||||
self.isReconnectingGateway = true
|
||||
defer { self.isReconnectingGateway = false }
|
||||
await self.gatewayController.connectLastKnown()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func runDiagnostics() async {
|
||||
guard !self.isRefreshingGateway else { return }
|
||||
self.isRefreshingGateway = true
|
||||
defer { self.isRefreshingGateway = false }
|
||||
|
||||
if !self.appModel.isAppleReviewDemoModeEnabled {
|
||||
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
|
||||
self.gatewayController.restartDiscovery()
|
||||
await self.appModel.refreshGatewayOverviewIfConnected()
|
||||
}
|
||||
let notificationSettings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
self.applyNotificationStatus(notificationSettings.authorizationStatus)
|
||||
self.registerForRemoteNotificationsIfEnrollmentReady()
|
||||
|
||||
let issueCount = SettingsDiagnostics.issueCount(
|
||||
gatewayConnected: self.gatewayDiagnosticConnected,
|
||||
discoveredGatewayCount: self.gatewayController.gateways.count,
|
||||
talkConfigLoaded: self.gatewayDiagnosticTalkConfigLoaded,
|
||||
notificationsAllowed: self.notificationStatus == .allowed)
|
||||
self.diagnosticsIssueCount = issueCount
|
||||
self.diagnosticsLastRunText = SettingsDiagnostics.timestamp(Date())
|
||||
}
|
||||
|
||||
func syncSettingsState() {
|
||||
self.manualGatewayPortText = self.manualGatewayPort > 0 ? String(self.manualGatewayPort) : ""
|
||||
self.selectedAgentPickerId = self.appModel.selectedAgentId ?? ""
|
||||
self.defaultShareInstruction = ShareToAgentSettings.loadDefaultInstruction()
|
||||
self.refreshLocationPermissionSummary()
|
||||
let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedInstanceId.isEmpty else { return }
|
||||
self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? ""
|
||||
self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? ""
|
||||
}
|
||||
|
||||
func refreshLocationPermissionSummary(desiredMode modeOverride: OpenClawLocationMode? = nil) {
|
||||
let mode = modeOverride ?? OpenClawLocationMode(rawValue: self.locationModeRaw) ?? .off
|
||||
let manager = CLLocationManager()
|
||||
self.locationPermissionRefreshID &+= 1
|
||||
let refreshID = self.locationPermissionRefreshID
|
||||
let currentSummary = self.locationPermissionSummary
|
||||
self.locationPermissionSummary = LocationPermissionSummary(
|
||||
desiredMode: mode,
|
||||
locationServicesEnabled: currentSummary.locationServicesEnabled,
|
||||
authorizationStatus: manager.authorizationStatus,
|
||||
accuracyAuthorization: manager.accuracyAuthorization)
|
||||
Task {
|
||||
let locationServicesEnabled = await Self.locationServicesEnabled()
|
||||
guard refreshID == self.locationPermissionRefreshID else { return }
|
||||
let latestManager = CLLocationManager()
|
||||
let latestMode = modeOverride ?? OpenClawLocationMode(rawValue: self.locationModeRaw) ?? .off
|
||||
self.locationPermissionSummary = LocationPermissionSummary(
|
||||
desiredMode: latestMode,
|
||||
locationServicesEnabled: locationServicesEnabled,
|
||||
authorizationStatus: latestManager.authorizationStatus,
|
||||
accuracyAuthorization: latestManager.accuracyAuthorization)
|
||||
}
|
||||
}
|
||||
|
||||
private static func locationServicesEnabled() async -> Bool {
|
||||
await Task.detached(priority: .utility) {
|
||||
CLLocationManager.locationServicesEnabled()
|
||||
}.value
|
||||
}
|
||||
|
||||
func syncAfterOnboardingReset() {
|
||||
self.connectingGatewayID = nil
|
||||
self.setupStatusText = nil
|
||||
self.stagedGatewaySetupLink = nil
|
||||
self.pendingManualAuthOverride = nil
|
||||
self.syncSettingsState()
|
||||
}
|
||||
|
||||
func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async {
|
||||
self.connectingGatewayID = gateway.id
|
||||
defer { self.connectingGatewayID = nil }
|
||||
self.manualGatewayEnabled = false
|
||||
GatewaySettingsStore.savePreferredGatewayStableID(gateway.stableID)
|
||||
GatewaySettingsStore.saveLastDiscoveredGatewayStableID(gateway.stableID)
|
||||
if let err = await self.gatewayController.connectWithDiagnostics(gateway) {
|
||||
self.setupStatusText = err
|
||||
}
|
||||
}
|
||||
|
||||
func applySetupCodeAndConnect() async {
|
||||
self.setupStatusText = nil
|
||||
guard self.applySetupCode() else { return }
|
||||
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard self.resolvedManualPort(host: host) != nil else {
|
||||
self.setupStatusText = "Failed: invalid port"
|
||||
return
|
||||
}
|
||||
guard await self.preflightGateway(host: host) else { return }
|
||||
self.setupStatusText = "Setup code applied. Connecting..."
|
||||
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)
|
||||
let stagedLink = self.stagedGatewaySetupLink
|
||||
guard !raw.isEmpty || stagedLink != nil else {
|
||||
self.setupStatusText = "Paste a setup code to continue."
|
||||
return false
|
||||
}
|
||||
|
||||
if AppleReviewDemoMode.isSetupCode(raw) {
|
||||
self.stagedGatewaySetupLink = nil
|
||||
self.setupCode = ""
|
||||
self.setupStatusText = "Apple Review demo mode enabled."
|
||||
self.appModel.enterAppleReviewDemoMode()
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func applyGatewayLink(_ link: GatewayConnectDeepLink) {
|
||||
self.manualGatewayHost = link.host
|
||||
self.manualGatewayPort = link.port
|
||||
self.manualGatewayPortText = String(link.port)
|
||||
self.manualGatewayTLS = link.tls
|
||||
let instanceId = GatewaySettingsStore.currentInstanceID()
|
||||
let setupAuth = GatewayConnectionController.ManualAuthOverride.setupAuth(from: link)
|
||||
if setupAuth.hasBootstrapToken {
|
||||
GatewayOnboardingReset.prepareForBootstrapPairing(appModel: self.appModel, instanceId: instanceId)
|
||||
}
|
||||
if !instanceId.isEmpty {
|
||||
GatewaySettingsStore.saveGatewayBootstrapToken(setupAuth.bootstrapToken, instanceId: instanceId)
|
||||
}
|
||||
if setupAuth.shouldApplyTokenField {
|
||||
self.gatewayToken = setupAuth.token
|
||||
if !instanceId.isEmpty {
|
||||
GatewaySettingsStore.saveGatewayToken(setupAuth.token, instanceId: instanceId)
|
||||
}
|
||||
}
|
||||
if setupAuth.shouldApplyPasswordField {
|
||||
self.gatewayPassword = setupAuth.password
|
||||
if !instanceId.isEmpty {
|
||||
GatewaySettingsStore.saveGatewayPassword(setupAuth.password, instanceId: instanceId)
|
||||
}
|
||||
}
|
||||
self.pendingManualAuthOverride = setupAuth.manualAuthOverride
|
||||
}
|
||||
|
||||
func openGatewayQRScanner() {
|
||||
self.appModel.disconnectGateway()
|
||||
self.connectingGatewayID = nil
|
||||
self.setupStatusText = "Opening QR scanner..."
|
||||
self.showQRScanner = true
|
||||
}
|
||||
|
||||
func handleScannedGatewayLink(_ link: GatewayConnectDeepLink) {
|
||||
self.showQRScanner = false
|
||||
self.setupCode = ""
|
||||
self.applyGatewayLink(link)
|
||||
self.setupStatusText = "QR loaded. Connecting to \(link.host):\(link.port)..."
|
||||
Task { await self.connectAfterScannedGatewayLink() }
|
||||
}
|
||||
|
||||
func handleScannedSetupCode(_ code: String) {
|
||||
guard AppleReviewDemoMode.isSetupCode(code) else { return }
|
||||
self.showQRScanner = false
|
||||
self.setupCode = ""
|
||||
self.stagedGatewaySetupLink = nil
|
||||
self.setupStatusText = "Apple Review demo mode enabled."
|
||||
self.appModel.enterAppleReviewDemoMode()
|
||||
}
|
||||
|
||||
func connectAfterScannedGatewayLink() async {
|
||||
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard self.resolvedManualPort(host: host) != nil else {
|
||||
self.setupStatusText = "Failed: invalid port"
|
||||
return
|
||||
}
|
||||
guard await self.preflightGateway(host: host) else { return }
|
||||
await self.connectManual()
|
||||
}
|
||||
|
||||
func connectManual() async {
|
||||
let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !host.isEmpty else {
|
||||
self.setupStatusText = "Failed: host required"
|
||||
return
|
||||
}
|
||||
guard self.manualPortIsValid else {
|
||||
self.setupStatusText = "Failed: invalid port"
|
||||
return
|
||||
}
|
||||
self.connectingGatewayID = "manual"
|
||||
self.manualGatewayEnabled = true
|
||||
defer { self.connectingGatewayID = nil }
|
||||
let authOverride = GatewayConnectionController.ManualAuthOverride.currentManualInput(
|
||||
token: self.gatewayToken,
|
||||
pendingOverride: self.pendingManualAuthOverride,
|
||||
password: self.gatewayPassword)
|
||||
self.pendingManualAuthOverride = nil
|
||||
await self.gatewayController.connectManual(
|
||||
host: host,
|
||||
port: self.manualGatewayPort,
|
||||
useTLS: self.manualGatewayTLS,
|
||||
authOverride: authOverride)
|
||||
}
|
||||
|
||||
func preflightGateway(host: String) async -> Bool {
|
||||
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 device. Turn it on, then try again."
|
||||
return false
|
||||
}
|
||||
self.gatewayController.requestLocalNetworkAccess(reason: "settings_preflight")
|
||||
return true
|
||||
}
|
||||
|
||||
func resetOnboarding() {
|
||||
self.connectingGatewayID = nil
|
||||
self.setupStatusText = nil
|
||||
self.setupCode = ""
|
||||
self.gatewayAutoConnect = false
|
||||
self.suppressCredentialPersist = true
|
||||
defer { self.suppressCredentialPersist = false }
|
||||
self.gatewayToken = ""
|
||||
self.gatewayPassword = ""
|
||||
GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: self.instanceId)
|
||||
self.onboardingComplete = false
|
||||
self.hasConnectedOnce = false
|
||||
self.manualGatewayEnabled = false
|
||||
self.manualGatewayHost = ""
|
||||
self.onboardingRequestID += 1
|
||||
}
|
||||
|
||||
func handleLocationModeChange(_ newValue: String) {
|
||||
guard !self.isChangingLocationMode else { return }
|
||||
guard newValue != self.previousLocationModeRaw else { return }
|
||||
guard let mode = OpenClawLocationMode(rawValue: newValue) else { return }
|
||||
let previous = self.previousLocationModeRaw
|
||||
Task {
|
||||
await self.applyLocationMode(mode, rawValue: newValue, previous: previous)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func applyLocationMode(
|
||||
_ mode: OpenClawLocationMode,
|
||||
rawValue: String,
|
||||
previous: String) async
|
||||
{
|
||||
self.isChangingLocationMode = true
|
||||
self.locationStatusText = nil
|
||||
self.refreshLocationPermissionSummary(desiredMode: mode)
|
||||
defer { self.isChangingLocationMode = false }
|
||||
|
||||
if mode == .off {
|
||||
_ = await self.appModel.requestLocationPermissions(mode: mode)
|
||||
self.previousLocationModeRaw = rawValue
|
||||
self.refreshLocationPermissionSummary(desiredMode: mode)
|
||||
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
|
||||
return
|
||||
}
|
||||
|
||||
let granted = await self.appModel.requestLocationPermissions(mode: mode)
|
||||
self.refreshLocationPermissionSummary(desiredMode: mode)
|
||||
if granted {
|
||||
self.previousLocationModeRaw = rawValue
|
||||
self.gatewayController.refreshActiveGatewayRegistrationFromSettings()
|
||||
} else {
|
||||
self.locationModeRaw = previous
|
||||
self.previousLocationModeRaw = previous
|
||||
self.locationStatusText = "Location permission was not granted."
|
||||
self.refreshLocationPermissionSummary(
|
||||
desiredMode: OpenClawLocationMode(rawValue: previous) ?? .off)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshNotificationSettings() {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
let status = settings.authorizationStatus
|
||||
Task { @MainActor in
|
||||
self.applyNotificationStatus(status)
|
||||
self.registerForRemoteNotificationsIfEnrollmentReady()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleNotificationAction() {
|
||||
if self.notificationStatus.shouldOpenNotificationSettings {
|
||||
self.openNotificationSettings()
|
||||
return
|
||||
}
|
||||
guard self.notificationStatus == .notSet else { return }
|
||||
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
self.showNotificationRelayDisclosure = true
|
||||
return
|
||||
}
|
||||
self.requestNotificationAuthorizationFromSettings()
|
||||
}
|
||||
|
||||
func requestNotificationAuthorizationFromSettings() {
|
||||
guard !self.isRequestingNotificationAuthorization else { return }
|
||||
PushEnrollmentConsent.markDisclosureAccepted()
|
||||
self.isRequestingNotificationAuthorization = true
|
||||
Task {
|
||||
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
|
||||
.alert,
|
||||
.badge,
|
||||
.sound,
|
||||
])) ?? false
|
||||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||||
await MainActor.run {
|
||||
self.isRequestingNotificationAuthorization = false
|
||||
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
|
||||
guard granted else { return }
|
||||
self.registerForRemoteNotificationsIfEnrollmentReady()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func registerForRemoteNotificationsIfEnrollmentReady() {
|
||||
guard PushEnrollmentConsent.disclosureAccepted else { return }
|
||||
guard self.notificationStatus.allowsNotifications else { return }
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func applyNotificationStatus(_ status: UNAuthorizationStatus) {
|
||||
self.notificationStatus = SettingsNotificationStatus(status)
|
||||
}
|
||||
|
||||
func persistGatewayToken(_ value: String) {
|
||||
guard !self.suppressCredentialPersist else { return }
|
||||
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !instanceId.isEmpty else { return }
|
||||
GatewaySettingsStore.saveGatewayToken(
|
||||
value.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
instanceId: instanceId)
|
||||
}
|
||||
|
||||
func persistGatewayPassword(_ value: String) {
|
||||
guard !self.suppressCredentialPersist else { return }
|
||||
let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !instanceId.isEmpty else { return }
|
||||
GatewaySettingsStore.saveGatewayPassword(
|
||||
value.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
instanceId: instanceId)
|
||||
}
|
||||
|
||||
func openNotificationSettings() {
|
||||
guard let url = URL(string: UIApplication.openNotificationSettingsURLString) else { return }
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
func title(for route: SettingsRoute) -> String {
|
||||
switch route {
|
||||
case .gateway: "Gateway"
|
||||
case .approvals: "Approvals"
|
||||
case .permissions: "Permissions"
|
||||
case .channels: "Channels"
|
||||
case .voice: "Voice & Talk"
|
||||
case .diagnostics: "Diagnostics"
|
||||
case .privacy: "Privacy"
|
||||
case .notifications: "Notifications"
|
||||
case .licenses: "Licenses"
|
||||
case .about: "About"
|
||||
}
|
||||
}
|
||||
|
||||
var manualPortBinding: Binding<String> {
|
||||
Binding(
|
||||
get: { self.manualGatewayPortText },
|
||||
set: { newValue in
|
||||
let filtered = newValue.filter(\.isNumber)
|
||||
self.manualGatewayPortText = filtered
|
||||
self.manualGatewayPort = Int(filtered) ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
var manualPortIsValid: Bool {
|
||||
if self.manualGatewayPortText.isEmpty { return true }
|
||||
return self.manualGatewayPort >= 1 && self.manualGatewayPort <= 65535
|
||||
}
|
||||
|
||||
func resolvedManualPort(host: String) -> Int? {
|
||||
if self.manualGatewayPort > 0 {
|
||||
return self.manualGatewayPort <= 65535 ? self.manualGatewayPort : nil
|
||||
}
|
||||
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
if self.manualGatewayTLS, trimmed.lowercased().hasSuffix(".ts.net") {
|
||||
return 443
|
||||
}
|
||||
return 18789
|
||||
}
|
||||
|
||||
var setupStatusLine: String? {
|
||||
if let problem = self.appModel.lastGatewayProblem {
|
||||
return problem.message
|
||||
}
|
||||
let trimmedSetup = self.setupStatusText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let friendly = self.friendlyGatewayMessage(from: gatewayStatus) { return friendly }
|
||||
if let friendly = self.friendlyGatewayMessage(from: trimmedSetup) { return friendly }
|
||||
if self.isTransientSetupStatus(trimmedSetup),
|
||||
!gatewayStatus.isEmpty,
|
||||
gatewayStatus != "Offline"
|
||||
{
|
||||
return gatewayStatus
|
||||
}
|
||||
if !trimmedSetup.isEmpty { return trimmedSetup }
|
||||
if gatewayStatus.isEmpty || gatewayStatus == "Offline" { return nil }
|
||||
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 device, then tap Connect."
|
||||
}
|
||||
|
||||
func friendlyGatewayMessage(from raw: String) -> String? {
|
||||
let lower = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
if lower.contains("pairing required") {
|
||||
return "Pairing required. Run /pair approve in your OpenClaw chat, then connect again."
|
||||
}
|
||||
if lower.contains("device nonce required") || lower.contains("device nonce mismatch") {
|
||||
return "Secure handshake failed. Check Tailscale, then connect again."
|
||||
}
|
||||
if lower.contains("tls fingerprint verification timed out")
|
||||
|| lower.contains("no tls endpoint detected")
|
||||
{
|
||||
return raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
if lower.contains("timed out") {
|
||||
return "Connection timed out. Make sure Tailscale is connected, then try again."
|
||||
}
|
||||
if lower.contains("unauthorized role") {
|
||||
return "Connected, but some controls are restricted for nodes. This is expected."
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isTransientSetupStatus(_ raw: String) -> Bool {
|
||||
let lower = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
return lower == "setup code applied. connecting..."
|
||||
|| lower.hasPrefix("qr loaded. connecting to ")
|
||||
|| lower == "checking gateway reachability..."
|
||||
}
|
||||
|
||||
var shouldShowRealtimeVoicePicker: Bool {
|
||||
let providerSelection = TalkModeProviderSelection.resolved(self.talkProviderSelectionRaw)
|
||||
return providerSelection == .openAIRealtime || self.appModel.talkMode.gatewayTalkUsesRealtime
|
||||
}
|
||||
|
||||
var talkProviderSelectionBinding: Binding<String> {
|
||||
Binding(
|
||||
get: { self.talkProviderSelectionRaw },
|
||||
set: { newValue in
|
||||
let selection = TalkModeProviderSelection.resolved(newValue)
|
||||
self.talkProviderSelectionRaw = selection.rawValue
|
||||
self.appModel.setTalkProviderSelection(selection.rawValue)
|
||||
})
|
||||
}
|
||||
|
||||
var talkRealtimeVoiceSelectionBinding: Binding<String> {
|
||||
Binding(
|
||||
get: { self.talkRealtimeVoiceSelectionRaw },
|
||||
set: { newValue in
|
||||
let voice = TalkModeRealtimeVoiceSelection.resolvedOverride(newValue) ?? ""
|
||||
self.talkRealtimeVoiceSelectionRaw = voice
|
||||
self.appModel.setTalkRealtimeVoiceSelection(voice)
|
||||
})
|
||||
}
|
||||
|
||||
var talkSpeakerphoneBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { self.talkSpeakerphoneEnabled },
|
||||
set: { newValue in
|
||||
self.talkSpeakerphoneEnabled = newValue
|
||||
self.appModel.setTalkSpeakerphoneEnabled(newValue)
|
||||
})
|
||||
}
|
||||
|
||||
var talkApiKeyStatus: String {
|
||||
guard self.appModel.talkMode.gatewayTalkConfigLoaded else { return "Not loaded" }
|
||||
return self.appModel.talkMode.gatewayTalkApiKeyConfigured ? "Configured" : "Not configured"
|
||||
}
|
||||
|
||||
var gatewayTalkActiveVoiceDetail: String {
|
||||
let title = self.appModel.talkMode.gatewayTalkActiveModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let subtitle = (self.appModel.talkMode.gatewayTalkActiveModeSubtitle ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if title.isEmpty { return "Not active" }
|
||||
if subtitle.isEmpty { return title }
|
||||
return "\(title) • \(subtitle)"
|
||||
}
|
||||
|
||||
var gatewayTalkLastIssueDetail: String? {
|
||||
let detail = (self.appModel.talkMode.gatewayTalkLastIssueText ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return detail.isEmpty ? nil : detail
|
||||
}
|
||||
|
||||
func gatewayDetailLines(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> [String] {
|
||||
var lines: [String] = []
|
||||
if let lanHost = gateway.lanHost { lines.append("LAN: \(lanHost)") }
|
||||
if let tailnet = gateway.tailnetDns { lines.append("Tailnet: \(tailnet)") }
|
||||
let gw = gateway.gatewayPort.map(String.init)
|
||||
let canvas = gateway.canvasPort.map(String.init)
|
||||
if gw != nil || canvas != nil {
|
||||
lines.append("Ports: gateway \(gw ?? "-") / canvas \(canvas ?? "-")")
|
||||
}
|
||||
return lines.isEmpty ? [gateway.debugID] : lines
|
||||
}
|
||||
|
||||
var gatewayConnected: Bool {
|
||||
!self.appModel.isAppleReviewDemoModeEnabled &&
|
||||
GatewayStatusBuilder.build(appModel: self.appModel) == .connected
|
||||
}
|
||||
|
||||
var gatewayStatusDetail: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "Apple Review demo mode" }
|
||||
return self.gatewayConnected ? "Connected" : self.appModel.gatewayDisplayStatusText
|
||||
}
|
||||
|
||||
var gatewayStatusValue: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
|
||||
return self.gatewayConnected ? "online" : "offline"
|
||||
}
|
||||
|
||||
var gatewayStatusColor: Color {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return OpenClawBrand.accent }
|
||||
return self.gatewayConnected ? OpenClawBrand.ok : .secondary
|
||||
}
|
||||
|
||||
var gatewayDiagnosticConnected: Bool {
|
||||
self.appModel.isAppleReviewDemoModeEnabled || self.gatewayConnected
|
||||
}
|
||||
|
||||
var gatewayDiagnosticTalkConfigLoaded: Bool {
|
||||
self.appModel.isAppleReviewDemoModeEnabled || self.appModel.talkMode.gatewayTalkConfigLoaded
|
||||
}
|
||||
|
||||
var approvalEmptyDetail: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled {
|
||||
return "Live gateway requests are disabled in demo mode."
|
||||
}
|
||||
if self.notificationsNeedAttention {
|
||||
return "Foreground approvals still appear while OpenClaw is connected."
|
||||
}
|
||||
return self.gatewayConnected ? "Gateway requests will appear here." : "Connect to the gateway."
|
||||
}
|
||||
|
||||
var gatewayTalkConfigDetail: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "Demo mode only" }
|
||||
return self.appModel.talkMode.gatewayTalkTransportLabel
|
||||
}
|
||||
|
||||
var gatewayTalkConfigValue: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
|
||||
return self.appModel.talkMode.gatewayTalkConfigLoaded ? "loaded" : "missing"
|
||||
}
|
||||
|
||||
var gatewayTalkConfigColor: Color {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return .secondary }
|
||||
return self.appModel.talkMode.gatewayTalkConfigLoaded ? OpenClawBrand.ok : .secondary
|
||||
}
|
||||
|
||||
var gatewayAddress: String {
|
||||
self.appModel.gatewayRemoteAddress ?? "Waiting for gateway"
|
||||
}
|
||||
|
||||
var gatewayServer: String {
|
||||
self.appModel.gatewayServerName ?? "OpenClaw Gateway"
|
||||
}
|
||||
|
||||
var pendingApproval: NodeAppModel.ExecApprovalPrompt? {
|
||||
self.appModel.pendingExecApprovalPrompt
|
||||
}
|
||||
|
||||
var notificationsNeedAttention: Bool {
|
||||
switch self.notificationStatus {
|
||||
case .allowed, .checking:
|
||||
false
|
||||
case .notAllowed, .notSet, .unknown:
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
var approvalItems: [SettingsApprovalItem] {
|
||||
guard let pendingApproval else { return [] }
|
||||
return [
|
||||
SettingsApprovalItem(
|
||||
id: "pending-real",
|
||||
icon: "terminal.fill",
|
||||
title: pendingApproval.commandPreview ?? "Review gateway action",
|
||||
detail: "Agent: \(self.appModel.activeAgentName)",
|
||||
priority: self.appModel.pendingExecApprovalPromptResolving ? "Resolving" : "High",
|
||||
color: OpenClawBrand.danger),
|
||||
SettingsApprovalItem(
|
||||
id: "pending-context",
|
||||
icon: "doc.text.fill",
|
||||
title: pendingApproval.allowsAllowAlways ? "Permission can be saved" : "One-time approval",
|
||||
detail: "Gateway request",
|
||||
priority: pendingApproval.allowsAllowAlways ? "Medium" : "Review",
|
||||
color: OpenClawBrand.warn),
|
||||
]
|
||||
}
|
||||
|
||||
var voiceDetail: String {
|
||||
if self.talkEnabled, self.voiceWakeEnabled { return "Talk + Wake" }
|
||||
if self.talkEnabled { return "Talk on" }
|
||||
if self.voiceWakeEnabled { return "Wake on" }
|
||||
return "Off"
|
||||
}
|
||||
|
||||
var diagnosticsHealthValue: String {
|
||||
if self.appModel.isAppleReviewDemoModeEnabled { return "demo" }
|
||||
if self.gatewayConnected { return "ready" }
|
||||
if self.gatewayController.gateways.isEmpty { return "check" }
|
||||
return "partial"
|
||||
}
|
||||
|
||||
var diagnosticsRunValue: String {
|
||||
guard let diagnosticsIssueCount else { return "pending" }
|
||||
return diagnosticsIssueCount == 0 ? "pass" : "\(diagnosticsIssueCount)"
|
||||
}
|
||||
|
||||
var diagnosticsRunColor: Color {
|
||||
guard let diagnosticsIssueCount else { return .secondary }
|
||||
return diagnosticsIssueCount == 0 ? OpenClawBrand.ok : OpenClawBrand.warn
|
||||
}
|
||||
|
||||
var privacyDetail: String {
|
||||
let location = OpenClawLocationMode(rawValue: self.locationModeRaw) ?? .off
|
||||
return switch (location, self.locationPermissionSummary.effectiveMode) {
|
||||
case (.off, _):
|
||||
"Location off"
|
||||
case (.whileUsing, .whileUsing):
|
||||
"Location While Using"
|
||||
case (.whileUsing, .off):
|
||||
"Location While Using, effective Off"
|
||||
case (.whileUsing, .always):
|
||||
"Location While Using, effective Always"
|
||||
case (.always, .always):
|
||||
"Location Always"
|
||||
case (.always, .whileUsing):
|
||||
"Location Always, effective While Using"
|
||||
case (.always, .off):
|
||||
"Location Always, effective Off"
|
||||
}
|
||||
}
|
||||
|
||||
var locationPermissionDetailText: String {
|
||||
if self.isChangingLocationMode {
|
||||
return "Requesting iOS location permission…"
|
||||
}
|
||||
return self.locationPermissionSummary.detailText
|
||||
}
|
||||
|
||||
var locationPermissionWarningText: String? {
|
||||
guard let locationStatusText else { return nil }
|
||||
guard locationStatusText != self.locationPermissionSummary.detailText else { return nil }
|
||||
return locationStatusText
|
||||
}
|
||||
|
||||
var notificationStatusText: String {
|
||||
self.notificationStatus.text
|
||||
}
|
||||
|
||||
var notificationActionText: String {
|
||||
self.notificationStatus.actionTitle
|
||||
}
|
||||
|
||||
var notificationStatusDetail: String {
|
||||
switch self.notificationStatus {
|
||||
case .checking:
|
||||
"Checking iOS notification permission."
|
||||
case .allowed:
|
||||
"OpenClaw can show approval prompts and event alerts when the app is not active."
|
||||
case .notAllowed:
|
||||
"Notifications have been denied. Enable them in iOS Settings."
|
||||
case .notSet:
|
||||
"Enable notifications to receive approval prompts and event alerts outside the app."
|
||||
case .unknown:
|
||||
"OpenClaw cannot determine the current notification permission state."
|
||||
}
|
||||
}
|
||||
|
||||
var notificationRelayDetail: String {
|
||||
if PushBuildConfig.current.usesOpenClawHostedRelay {
|
||||
let host = PushBuildConfig.current.relayBaseURL.flatMap {
|
||||
URLComponents(url: $0, resolvingAgainstBaseURL: false)?.host
|
||||
} ?? "ios-push-relay.openclaw.ai"
|
||||
return """
|
||||
This build uses OpenClaw's hosted push relay at \(host) for notification \
|
||||
delivery data.
|
||||
"""
|
||||
}
|
||||
return "This build is not configured to use OpenClaw's hosted push relay."
|
||||
}
|
||||
|
||||
var notificationRelayDisclosureMessage: String {
|
||||
"Enabling this sends delivery data through OpenClaw's hosted push relay."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user