From 2db5bd3394f79e2b64b8fc4133da3ba5284c99dc Mon Sep 17 00:00:00 2001 From: Tyler Nishida <150876516+thats2easyyy@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:38:54 -1000 Subject: [PATCH] feat(ios): refresh onboarding setup flow (#98868) --- apps/.i18n/native-source.json | 602 ++++++++++-------- apps/ios/Resources/Localizable.xcstrings | 268 ++++---- apps/ios/Sources/Design/OpenClawBrand.swift | 316 ++++++++- .../Design/OpenClawProComponents.swift | 57 -- .../Sources/Design/OpenClawTypography.swift | 4 - apps/ios/Sources/Design/TalkSiriWave.swift | 8 +- .../Gateway/GatewayQuickSetupSheet.swift | 452 +++++++++---- .../Onboarding/OnboardingWizardSteps.swift | 601 +++++++++++------ .../Onboarding/OnboardingWizardView.swift | 490 ++++++++------ apps/ios/Sources/RootTabs.swift | 2 +- apps/ios/Tests/OpenClawTypographyTests.swift | 31 +- apps/ios/Tests/SwiftUIRenderSmokeTests.swift | 84 +++ .../ios/UITests/OpenClawSnapshotUITests.swift | 61 +- .../OpenClawChatUI/OpenClawMascotView.swift | 11 +- scripts/apple-app-i18n.ts | 10 +- test/scripts/apple-app-i18n.test.ts | 18 + test/scripts/native-app-i18n.test.ts | 2 +- 17 files changed, 2015 insertions(+), 1002 deletions(-) diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index e3a7e77ddf2..33d62177f53 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -8307,7 +8307,7 @@ }, { "kind": "conditional-branch", - "line": 27, + "line": 28, "path": "apps/ios/Sources/Design/OpenClawBrand.swift", "source": "System", "surface": "apple", @@ -8315,7 +8315,7 @@ }, { "kind": "conditional-branch", - "line": 28, + "line": 29, "path": "apps/ios/Sources/Design/OpenClawBrand.swift", "source": "Light", "surface": "apple", @@ -8323,7 +8323,7 @@ }, { "kind": "conditional-branch", - "line": 29, + "line": 30, "path": "apps/ios/Sources/Design/OpenClawBrand.swift", "source": "Dark", "surface": "apple", @@ -8411,7 +8411,7 @@ }, { "kind": "ui-call", - "line": 404, + "line": 403, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Request ID: \\(value)", "surface": "apple", @@ -8419,7 +8419,7 @@ }, { "kind": "ui-modifier", - "line": 657, + "line": 600, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "OpenClaw", "surface": "apple", @@ -8427,7 +8427,7 @@ }, { "kind": "ui-modifier", - "line": 685, + "line": 628, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Gateway \\(self.title)", "surface": "apple", @@ -8435,7 +8435,7 @@ }, { "kind": "conditional-branch", - "line": 690, + "line": 633, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Online", "surface": "apple", @@ -8443,7 +8443,7 @@ }, { "kind": "conditional-branch", - "line": 692, + "line": 635, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Connecting", "surface": "apple", @@ -8451,7 +8451,7 @@ }, { "kind": "conditional-branch", - "line": 694, + "line": 637, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Attention", "surface": "apple", @@ -8459,7 +8459,7 @@ }, { "kind": "conditional-branch", - "line": 696, + "line": 639, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Offline", "surface": "apple", @@ -10659,15 +10659,7 @@ }, { "kind": "ui-call", - "line": 17, - "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", - "source": "Connect to a Gateway?", - "surface": "apple", - "id": "native.apple.948e3fafb80564c9" - }, - { - "kind": "ui-call", - "line": 56, + "line": 62, "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", "source": "Connecting…", "surface": "apple", @@ -10675,52 +10667,140 @@ }, { "kind": "ui-call", - "line": 60, + "line": 66, "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", - "source": "Connect", + "source": "Connect to this Gateway", "surface": "apple", - "id": "native.apple.a735b07046d583e7" + "id": "native.apple.78d430515e5bb8eb" }, { "kind": "ui-call", - "line": 79, + "line": 82, "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", "source": "Not now", "surface": "apple", "id": "native.apple.102d0ce74e6253f3" }, - { - "kind": "ui-call", - "line": 86, - "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", - "source": "Don’t show this again", - "surface": "apple", - "id": "native.apple.88dbf6b7151586b4" - }, { "kind": "ui-call", "line": 89, "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", - "source": "No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.", + "source": "Don't show this again", "surface": "apple", - "id": "native.apple.51d42270d75ea40a" + "id": "native.apple.5204dc0158d98ebf" }, { - "kind": "ui-call", - "line": 102, + "kind": "ui-modifier", + "line": 99, "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", "source": "Quick Setup", "surface": "apple", - "id": "native.apple.d2445a87f9cb6ff4" + "id": "native.apple.476757fc54c63560" }, { "kind": "ui-call", - "line": 110, + "line": 107, "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", "source": "Close", "surface": "apple", "id": "native.apple.977a6e4a5022184f" }, + { + "kind": "ui-call", + "line": 196, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Connect a nearby Gateway", + "surface": "apple", + "id": "native.apple.f2642d91a3ac8c25" + }, + { + "kind": "conditional-branch", + "line": 246, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Local", + "surface": "apple", + "id": "native.apple.63bb4ac4eaf496bc" + }, + { + "kind": "conditional-branch", + "line": 246, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Secure", + "surface": "apple", + "id": "native.apple.1122855cccc617bd" + }, + { + "kind": "ui-named-argument", + "line": 260, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Discovery", + "surface": "apple", + "id": "native.apple.052a650dbff9657b" + }, + { + "kind": "ui-named-argument", + "line": 261, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Gateway", + "surface": "apple", + "id": "native.apple.c284afdefb55f224" + }, + { + "kind": "ui-named-argument", + "line": 262, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Node", + "surface": "apple", + "id": "native.apple.26a7ff6b06dbeb67" + }, + { + "kind": "ui-named-argument", + "line": 263, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Operator", + "surface": "apple", + "id": "native.apple.f2c3934d3e670237" + }, + { + "kind": "ui-call", + "line": 369, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Looking for a Gateway", + "surface": "apple", + "id": "native.apple.33be51249563ed37" + }, + { + "kind": "ui-call", + "line": 371, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Keep your iPhone on the same LAN or tailnet, then start the Gateway on your host machine.", + "surface": "apple", + "id": "native.apple.0e828048fafce681" + }, + { + "kind": "ui-named-argument", + "line": 378, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Run openclaw gateway --port 18789.", + "surface": "apple", + "id": "native.apple.c18f5bd36f119282" + }, + { + "kind": "ui-named-argument", + "line": 379, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Check that Bonjour discovery is enabled.", + "surface": "apple", + "id": "native.apple.a4eb9a9b02915961" + }, + { + "kind": "ui-named-argument", + "line": 380, + "path": "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift", + "source": "Open Settings if you need a manual host.", + "surface": "apple", + "id": "native.apple.72083d0b7fc77a6d" + }, { "kind": "ui-modifier", "line": 8, @@ -11139,191 +11219,159 @@ }, { "kind": "ui-call", - "line": 25, + "line": 163, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Welcome to OpenClaw", + "source": "Security notice", "surface": "apple", - "id": "native.apple.8197d399f16b6d15" - }, - { - "kind": "ui-call-concatenated", - "line": 37, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "The connected agent can use capabilities you enable, including camera, microphone, photos, contacts, calendar, and location. Continue only if you trust the gateway and agent.", - "surface": "apple", - "id": "native.apple.aec45c66e9d1436e" + "id": "native.apple.09e7832b23cf14f6" }, { "kind": "ui-call", - "line": 45, + "line": 167, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Security", + "source": "The connected OpenClaw agent can use device capabilities you enable.", "surface": "apple", - "id": "native.apple.f755248f69e3e497" + "id": "native.apple.d4d9568dca8ceaa9" }, { "kind": "ui-call", - "line": 58, + "line": 169, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "You can change permissions later in Settings.", + "source": "Camera, microphone, photos, contacts, calendar, and location may be available.", "surface": "apple", - "id": "native.apple.116debd4caba7aff" + "id": "native.apple.fdd93eb6b6eef525" }, { "kind": "ui-call", - "line": 64, + "line": 172, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Continue only if you trust the gateway and agent you connect to.", + "surface": "apple", + "id": "native.apple.7cc0f656abb41f6d" + }, + { + "kind": "ui-modifier", + "line": 206, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Copy setup code command", + "surface": "apple", + "id": "native.apple.eae3bd9e4e9112a0" + }, + { + "kind": "ui-named-argument", + "line": 244, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "OpenClaw", + "surface": "apple", + "id": "native.apple.e8b4dc00cd23ae73" + }, + { + "kind": "ui-named-argument", + "line": 245, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Securely connect this iPhone to your gateway.", + "surface": "apple", + "id": "native.apple.c9419426ba8c2a2e" + }, + { + "kind": "ui-named-argument", + "line": 252, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Connect to your gateway", + "surface": "apple", + "id": "native.apple.3e233ba3ed3181d4" + }, + { + "kind": "ui-named-argument", + "line": 255, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Choose device permissions", + "surface": "apple", + "id": "native.apple.b6978424ac0a4d14" + }, + { + "kind": "ui-named-argument", + "line": 258, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Use OpenClaw from your phone", + "surface": "apple", + "id": "native.apple.4109460a5b8927d8" + }, + { + "kind": "ui-call", + "line": 272, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", "source": "Continue", "surface": "apple", "id": "native.apple.cae311a6e975c879" }, { - "kind": "ui-call", - "line": 146, + "kind": "ui-named-argument", + "line": 294, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", "source": "Connect Gateway", "surface": "apple", - "id": "native.apple.68e933b36353a30c" + "id": "native.apple.c686068c2a79bf9e" + }, + { + "kind": "ui-named-argument", + "line": 300, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Run this on your gateway host and scan the code", + "surface": "apple", + "id": "native.apple.8f76a5962329b43f" }, { "kind": "ui-call", - "line": 151, + "line": 310, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "/pair qr", + "source": "Connecting…", "surface": "apple", - "id": "native.apple.9f65b2eadb6aee92" - }, - { - "kind": "conditional-branch", - "line": 161, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Copied", - "surface": "apple", - "id": "native.apple.fe3269c93d6f8524" - }, - { - "kind": "conditional-branch", - "line": 161, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Copy", - "surface": "apple", - "id": "native.apple.bad4a8110ca864e1" - }, - { - "kind": "conditional-branch", - "line": 169, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Copy pair command", - "surface": "apple", - "id": "native.apple.b6f3f5c300b28f17" - }, - { - "kind": "conditional-branch", - "line": 169, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Pair command copied", - "surface": "apple", - "id": "native.apple.7308cbcb1e40d70e" + "id": "native.apple.d71a98568b7184a5" }, { "kind": "ui-call", - "line": 174, + "line": 314, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Run this in your OpenClaw chat, then scan the code.", + "source": "Scan QR", "surface": "apple", - "id": "native.apple.b727174424127618" + "id": "native.apple.577764b38b1d903c" }, { - "kind": "ui-call", - "line": 186, + "kind": "ui-named-argument", + "line": 323, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Scan QR Code", + "source": "or", "surface": "apple", - "id": "native.apple.429ac70f39c21302" + "id": "native.apple.913a586956440f64" }, { "kind": "ui-call", - "line": 195, + "line": 326, "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", - "source": "Set Up Manually", + "source": "Connect Manually", "surface": "apple", - "id": "native.apple.7ba3027e51acb255" - }, - { - "kind": "conditional-branch", - "line": 29, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Welcome", - "surface": "apple", - "id": "native.apple.94c7d6cc4a2cf2dc" - }, - { - "kind": "conditional-branch", - "line": 30, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Connect Gateway", - "surface": "apple", - "id": "native.apple.5e905afc1c8cd46a" - }, - { - "kind": "conditional-branch", - "line": 31, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Connection Mode", - "surface": "apple", - "id": "native.apple.cef4a6612b762154" - }, - { - "kind": "conditional-branch", - "line": 32, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Connect", - "surface": "apple", - "id": "native.apple.38d3190134a58030" - }, - { - "kind": "conditional-branch", - "line": 33, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Authentication", - "surface": "apple", - "id": "native.apple.bfee748063f6f82c" - }, - { - "kind": "conditional-branch", - "line": 34, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Connected", - "surface": "apple", - "id": "native.apple.71f969ccb10fb5cc" + "id": "native.apple.57614f0c4e59677b" }, { "kind": "ui-call", - "line": 185, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Back", + "line": 382, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "You're connected", "surface": "apple", - "id": "native.apple.e3cc3df936dce84d" + "id": "native.apple.4916d34cab45e6c3" }, { "kind": "ui-call", - "line": 194, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Close", + "line": 405, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift", + "source": "Go to Chat", "surface": "apple", - "id": "native.apple.e58367cfa71115cc" - }, - { - "kind": "ui-call", - "line": 209, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Done", - "surface": "apple", - "id": "native.apple.7822f4f0f671d841" + "id": "native.apple.b5e401fa9bfb4b49" }, { "kind": "ui-modifier", - "line": 217, + "line": 205, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "QR Scanner Unavailable", "surface": "apple", @@ -11331,7 +11379,7 @@ }, { "kind": "ui-call", - "line": 222, + "line": 210, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "OK", "surface": "apple", @@ -11339,15 +11387,15 @@ }, { "kind": "ui-call", - "line": 327, + "line": 315, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Scan QR Code", + "source": "Scan Setup Code", "surface": "apple", - "id": "native.apple.80a2693bba128a6c" + "id": "native.apple.d50aa0885c7e064f" }, { "kind": "ui-call", - "line": 335, + "line": 323, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Cancel", "surface": "apple", @@ -11355,15 +11403,47 @@ }, { "kind": "ui-call", - "line": 342, + "line": 330, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Photos", "surface": "apple", "id": "native.apple.f0a022bd1772fda0" }, + { + "kind": "ui-modifier", + "line": 371, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Back", + "surface": "apple", + "id": "native.apple.09ed26daf34a4c5d" + }, + { + "kind": "ui-call", + "line": 379, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Close", + "surface": "apple", + "id": "native.apple.e58367cfa71115cc" + }, + { + "kind": "ui-modifier", + "line": 405, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Dismiss Keyboard", + "surface": "apple", + "id": "native.apple.1e60db061979db60" + }, { "kind": "ui-named-argument", - "line": 398, + "line": 437, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Home Network", + "surface": "apple", + "id": "native.apple.95e2c98254da2aba" + }, + { + "kind": "ui-named-argument", + "line": 438, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "LAN or Tailscale host", "surface": "apple", @@ -11371,7 +11451,15 @@ }, { "kind": "ui-named-argument", - "line": 406, + "line": 446, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Remote Domain", + "surface": "apple", + "id": "native.apple.431d02f8b68a96cf" + }, + { + "kind": "ui-named-argument", + "line": 447, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "VPS with domain", "surface": "apple", @@ -11379,7 +11467,15 @@ }, { "kind": "ui-named-argument", - "line": 417, + "line": 458, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Same Machine (Dev)", + "surface": "apple", + "id": "native.apple.7451f8d052016642" + }, + { + "kind": "ui-named-argument", + "line": 459, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "For local iOS app development", "surface": "apple", @@ -11387,7 +11483,15 @@ }, { "kind": "ui-call", - "line": 433, + "line": 467, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Manual Connection", + "surface": "apple", + "id": "native.apple.e1b1ccbfc9e73df8" + }, + { + "kind": "ui-call", + "line": 477, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Continue", "surface": "apple", @@ -11395,31 +11499,15 @@ }, { "kind": "ui-call", - "line": 442, + "line": 489, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Developer mode", "surface": "apple", "id": "native.apple.93d3e17fabd5e082" }, - { - "kind": "conditional-branch", - "line": 470, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Off", - "surface": "apple", - "id": "native.apple.ac7ea1c7379819d3" - }, - { - "kind": "conditional-branch", - "line": 470, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "On", - "surface": "apple", - "id": "native.apple.04df230fe508a665" - }, { "kind": "ui-call", - "line": 477, + "line": 538, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Mode", "surface": "apple", @@ -11427,7 +11515,7 @@ }, { "kind": "ui-call", - "line": 478, + "line": 539, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Discovery", "surface": "apple", @@ -11435,7 +11523,7 @@ }, { "kind": "ui-call", - "line": 480, + "line": 541, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Progress", "surface": "apple", @@ -11443,7 +11531,7 @@ }, { "kind": "ui-call", - "line": 482, + "line": 543, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Status", "surface": "apple", @@ -11451,7 +11539,7 @@ }, { "kind": "ui-call", - "line": 505, + "line": 566, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Choose a mode first.", "surface": "apple", @@ -11459,7 +11547,7 @@ }, { "kind": "ui-call", - "line": 510, + "line": 571, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Back to Mode Selection", "surface": "apple", @@ -11467,7 +11555,7 @@ }, { "kind": "conditional-branch", - "line": 522, + "line": 582, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Plaintext (local network)", "surface": "apple", @@ -11475,7 +11563,7 @@ }, { "kind": "ui-call", - "line": 522, + "line": 582, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Security", "surface": "apple", @@ -11483,7 +11571,7 @@ }, { "kind": "ui-call", - "line": 545, + "line": 605, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Use Manual Setup", "surface": "apple", @@ -11491,7 +11579,7 @@ }, { "kind": "ui-call", - "line": 551, + "line": 611, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Setup Link", "surface": "apple", @@ -11499,7 +11587,7 @@ }, { "kind": "conditional-branch", - "line": 555, + "line": 615, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Plaintext may expose credentials. Continue only if you trust this local network and host.", "surface": "apple", @@ -11507,7 +11595,7 @@ }, { "kind": "conditional-branch", - "line": 555, + "line": 615, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Review this endpoint. Credentials are applied only after you tap Connect.", "surface": "apple", @@ -11515,7 +11603,7 @@ }, { "kind": "ui-call", - "line": 566, + "line": 626, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "No gateways found yet.", "surface": "apple", @@ -11523,7 +11611,7 @@ }, { "kind": "ui-call", - "line": 591, + "line": 651, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Resolving…", "surface": "apple", @@ -11531,7 +11619,7 @@ }, { "kind": "ui-call", - "line": 607, + "line": 667, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Restart Discovery", "surface": "apple", @@ -11539,7 +11627,7 @@ }, { "kind": "ui-call", - "line": 613, + "line": 673, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Discovered Gateways", "surface": "apple", @@ -11547,7 +11635,7 @@ }, { "kind": "ui-named-argument", - "line": 617, + "line": 677, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Manual Fallback", "surface": "apple", @@ -11555,7 +11643,7 @@ }, { "kind": "ui-named-argument", - "line": 622, + "line": 682, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Domain Settings", "surface": "apple", @@ -11563,7 +11651,7 @@ }, { "kind": "ui-call", - "line": 637, + "line": 693, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Developer Local", "surface": "apple", @@ -11571,7 +11659,7 @@ }, { "kind": "ui-call", - "line": 640, + "line": 696, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Default host is localhost. Use your Mac LAN IP if simulator networking requires it.", "surface": "apple", @@ -11579,23 +11667,23 @@ }, { "kind": "ui-call", - "line": 664, + "line": 724, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Gateway rejected credentials. Scan a fresh QR code or update token/password.", + "source": "Gateway rejected credentials. Scan a fresh setup code or update token/password.", "surface": "apple", - "id": "native.apple.4cc6c6c21864bff9" + "id": "native.apple.5847cf494847ed98" }, { "kind": "ui-call", - "line": 668, + "line": 728, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Auth token looks valid.", + "source": "OpenClaw is checking gateway and node access.", "surface": "apple", - "id": "native.apple.5841249e14da136f" + "id": "native.apple.559dccb9f9dfbbad" }, { "kind": "ui-call", - "line": 682, + "line": 742, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Resume After Approval", "surface": "apple", @@ -11603,7 +11691,7 @@ }, { "kind": "ui-call", - "line": 688, + "line": 748, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Pairing Approval", "surface": "apple", @@ -11611,7 +11699,7 @@ }, { "kind": "ui-call-concatenated", - "line": 698, + "line": 758, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Approve this device on the gateway.\n1) `\\(commandLine)`\n2) `/pair approve` in your OpenClaw chat\n\\(requestLine)\nOpenClaw will also retry automatically when you return to this app.", "surface": "apple", @@ -11619,15 +11707,15 @@ }, { "kind": "ui-call", - "line": 712, + "line": 772, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Scan QR Code Again", + "source": "Scan Setup Code Again", "surface": "apple", - "id": "native.apple.7ff9099402ff9f21" + "id": "native.apple.af3890b1e07c3084" }, { "kind": "ui-call", - "line": 725, + "line": 785, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Retry Connection", "surface": "apple", @@ -11635,39 +11723,23 @@ }, { "kind": "ui-call", - "line": 758, + "line": 817, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Go to Chat", + "source": "Enter setup code", "surface": "apple", - "id": "native.apple.e674edbcb002aec6" + "id": "native.apple.abe248e02d451890" }, { "kind": "ui-call", - "line": 772, + "line": 833, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Paste setup code", + "source": "Apply", "surface": "apple", - "id": "native.apple.4679e949156e07e3" + "id": "native.apple.d9e6ff8a72f4525b" }, { "kind": "ui-call", - "line": 788, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Applying...", - "surface": "apple", - "id": "native.apple.880898c80cab264e" - }, - { - "kind": "ui-call", - "line": 792, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Apply Setup Code", - "surface": "apple", - "id": "native.apple.064ae900e9c5b550" - }, - { - "kind": "ui-call", - "line": 807, + "line": 851, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Setup Code", "surface": "apple", @@ -11675,15 +11747,15 @@ }, { "kind": "ui-call", - "line": 810, + "line": 854, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Use this if you received a setup code instead of a QR code.", + "source": "Use this if you have a setup code instead of scanning.", "surface": "apple", - "id": "native.apple.c522fa3b5b0d812a" + "id": "native.apple.ee24a5a75d7b1e4f" }, { "kind": "ui-call", - "line": 817, + "line": 866, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Host", "surface": "apple", @@ -11691,7 +11763,7 @@ }, { "kind": "ui-call", - "line": 821, + "line": 867, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Port", "surface": "apple", @@ -11699,7 +11771,7 @@ }, { "kind": "ui-call", - "line": 824, + "line": 869, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Use TLS", "surface": "apple", @@ -11707,7 +11779,7 @@ }, { "kind": "ui-call", - "line": 825, + "line": 870, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Discovery Domain (optional)", "surface": "apple", @@ -11715,7 +11787,7 @@ }, { "kind": "ui-call", - "line": 830, + "line": 875, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Gateway Auth Token", "surface": "apple", @@ -11723,7 +11795,7 @@ }, { "kind": "ui-call", - "line": 833, + "line": 879, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Gateway Password", "surface": "apple", @@ -11731,12 +11803,20 @@ }, { "kind": "ui-call", - "line": 877, + "line": 948, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Connecting…", "surface": "apple", "id": "native.apple.6c3fe084ab7b318d" }, + { + "kind": "ui-call", + "line": 952, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Connect", + "surface": "apple", + "id": "native.apple.ab58b30ddda7aeb0" + }, { "kind": "ui-call", "line": 178, diff --git a/apps/ios/Resources/Localizable.xcstrings b/apps/ios/Resources/Localizable.xcstrings index d15819b7a40..4efd1b35f2e 100644 --- a/apps/ios/Resources/Localizable.xcstrings +++ b/apps/ios/Resources/Localizable.xcstrings @@ -1089,138 +1089,138 @@ } } }, - "Connect to a Gateway?": { + "Connect a nearby Gateway": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Connect to a Gateway?" + "value": "Connect a nearby Gateway" } }, "zh-CN": { "stringUnit": { - "state": "translated", - "value": "连接到网关?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "zh-TW": { "stringUnit": { - "state": "translated", - "value": "要連線到 Gateway 嗎?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "pt-BR": { "stringUnit": { - "state": "translated", - "value": "Conectar a um Gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "de": { "stringUnit": { - "state": "translated", - "value": "Mit einem Gateway verbinden?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "es": { "stringUnit": { - "state": "translated", - "value": "¿Conectar a un Gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "ja-JP": { "stringUnit": { - "state": "translated", - "value": "ゲートウェイに接続しますか?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "ko": { "stringUnit": { - "state": "translated", - "value": "게이트웨이에 연결할까요?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "fr": { "stringUnit": { - "state": "translated", - "value": "Se connecter à une passerelle ?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "hi": { "stringUnit": { - "state": "translated", - "value": "किसी गेटवे से कनेक्ट करें?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "ar": { "stringUnit": { - "state": "translated", - "value": "الاتصال ببوابة؟" + "state": "new", + "value": "Connect a nearby Gateway" } }, "it": { "stringUnit": { - "state": "translated", - "value": "Connettersi a un Gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "tr": { "stringUnit": { - "state": "translated", - "value": "Bir Gateway’e bağlanılsın mı?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "uk": { "stringUnit": { - "state": "translated", - "value": "Підключитися до шлюзу?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "id": { "stringUnit": { - "state": "translated", - "value": "Hubungkan ke Gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "pl": { "stringUnit": { - "state": "translated", - "value": "Połączyć z Gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "th": { "stringUnit": { - "state": "translated", - "value": "เชื่อมต่อกับ Gateway หรือไม่?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "vi": { "stringUnit": { - "state": "translated", - "value": "Kết nối với Gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "nl": { "stringUnit": { - "state": "translated", - "value": "Verbinden met een gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "fa": { "stringUnit": { - "state": "translated", - "value": "به یک Gateway متصل شوید؟" + "state": "new", + "value": "Connect a nearby Gateway" } }, "ru": { "stringUnit": { - "state": "translated", - "value": "Подключиться к шлюзу?" + "state": "new", + "value": "Connect a nearby Gateway" } }, "sv": { "stringUnit": { - "state": "translated", - "value": "Ansluta till en gateway?" + "state": "new", + "value": "Connect a nearby Gateway" } } } @@ -1769,12 +1769,12 @@ } } }, - "Don’t show this again": { + "Don't show this again": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Don’t show this again" + "value": "Don't show this again" } }, "zh-CN": { @@ -3265,138 +3265,138 @@ } } }, - "No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.": { + "Looking for a Gateway": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled." + "value": "Looking for a Gateway" } }, "zh-CN": { "stringUnit": { - "state": "translated", - "value": "尚未找到网关。请确保你的网关正在运行,并且已启用 Bonjour 发现。" + "state": "new", + "value": "Looking for a Gateway" } }, "zh-TW": { "stringUnit": { - "state": "translated", - "value": "尚未找到任何 gateway。請確認你的 gateway 正在執行,且已啟用 Bonjour 探索。" + "state": "new", + "value": "Looking for a Gateway" } }, "pt-BR": { "stringUnit": { - "state": "translated", - "value": "Nenhum gateway encontrado ainda. Certifique-se de que seu gateway esteja em execução e que a descoberta Bonjour esteja ativada." + "state": "new", + "value": "Looking for a Gateway" } }, "de": { "stringUnit": { - "state": "translated", - "value": "Noch keine Gateways gefunden. Stelle sicher, dass dein Gateway ausgeführt wird und die Bonjour-Erkennung aktiviert ist." + "state": "new", + "value": "Looking for a Gateway" } }, "es": { "stringUnit": { - "state": "translated", - "value": "Aún no se encontraron gateways. Asegúrate de que tu gateway esté en ejecución y que el descubrimiento Bonjour esté activado." + "state": "new", + "value": "Looking for a Gateway" } }, "ja-JP": { "stringUnit": { - "state": "translated", - "value": "ゲートウェイがまだ見つかりません。ゲートウェイが実行中で、Bonjour 検出が有効になっていることを確認してください。" + "state": "new", + "value": "Looking for a Gateway" } }, "ko": { "stringUnit": { - "state": "translated", - "value": "아직 게이트웨이를 찾을 수 없습니다. 게이트웨이가 실행 중이고 Bonjour 검색이 활성화되어 있는지 확인하세요." + "state": "new", + "value": "Looking for a Gateway" } }, "fr": { "stringUnit": { - "state": "translated", - "value": "Aucune passerelle trouvée pour le moment. Assurez-vous que votre passerelle est en cours d’exécution et que la découverte Bonjour est activée." + "state": "new", + "value": "Looking for a Gateway" } }, "hi": { "stringUnit": { - "state": "translated", - "value": "अभी तक कोई गेटवे नहीं मिला। सुनिश्चित करें कि आपका गेटवे चल रहा है और Bonjour डिस्कवरी सक्षम है।" + "state": "new", + "value": "Looking for a Gateway" } }, "ar": { "stringUnit": { - "state": "translated", - "value": "لم يتم العثور على أي بوابات بعد. تأكد من أن بوابتك قيد التشغيل وأن اكتشاف Bonjour ممكّن." + "state": "new", + "value": "Looking for a Gateway" } }, "it": { "stringUnit": { - "state": "translated", - "value": "Nessun gateway trovato al momento. Assicurati che il gateway sia in esecuzione e che il rilevamento Bonjour sia abilitato." + "state": "new", + "value": "Looking for a Gateway" } }, "tr": { "stringUnit": { - "state": "translated", - "value": "Henüz gateway bulunamadı. Gateway’inizin çalıştığından ve Bonjour keşfinin etkin olduğundan emin olun." + "state": "new", + "value": "Looking for a Gateway" } }, "uk": { "stringUnit": { - "state": "translated", - "value": "Шлюзів поки не знайдено. Переконайтеся, що ваш шлюз запущено, а виявлення Bonjour увімкнено." + "state": "new", + "value": "Looking for a Gateway" } }, "id": { "stringUnit": { - "state": "translated", - "value": "Belum ada gateway yang ditemukan. Pastikan gateway Anda berjalan dan penemuan Bonjour diaktifkan." + "state": "new", + "value": "Looking for a Gateway" } }, "pl": { "stringUnit": { - "state": "translated", - "value": "Nie znaleziono jeszcze żadnych gatewayów. Upewnij się, że gateway działa i że wykrywanie Bonjour jest włączone." + "state": "new", + "value": "Looking for a Gateway" } }, "th": { "stringUnit": { - "state": "translated", - "value": "ยังไม่พบ gateway ตรวจสอบให้แน่ใจว่า gateway ของคุณกำลังทำงานอยู่และเปิดใช้งานการค้นหา Bonjour แล้ว" + "state": "new", + "value": "Looking for a Gateway" } }, "vi": { "stringUnit": { - "state": "translated", - "value": "Chưa tìm thấy gateway nào. Hãy đảm bảo gateway của bạn đang chạy và tính năng khám phá Bonjour được bật." + "state": "new", + "value": "Looking for a Gateway" } }, "nl": { "stringUnit": { - "state": "translated", - "value": "Nog geen gateways gevonden. Zorg ervoor dat je gateway actief is en Bonjour-detectie is ingeschakeld." + "state": "new", + "value": "Looking for a Gateway" } }, "fa": { "stringUnit": { - "state": "translated", - "value": "هنوز هیچ gatewayای پیدا نشده است. مطمئن شوید gateway شما در حال اجراست و Bonjour discovery فعال است." + "state": "new", + "value": "Looking for a Gateway" } }, "ru": { "stringUnit": { - "state": "translated", - "value": "Шлюзы пока не найдены. Убедитесь, что ваш шлюз запущен и обнаружение Bonjour включено." + "state": "new", + "value": "Looking for a Gateway" } }, "sv": { "stringUnit": { - "state": "translated", - "value": "Inga gateways hittades ännu. Kontrollera att din gateway körs och att Bonjour-upptäckt är aktiverad." + "state": "new", + "value": "Looking for a Gateway" } } } @@ -4353,138 +4353,138 @@ } } }, - "Save": { + "Go to Chat": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Save" + "value": "Go to Chat" } }, "zh-CN": { "stringUnit": { - "state": "translated", - "value": "保存" + "state": "new", + "value": "Go to Chat" } }, "zh-TW": { "stringUnit": { - "state": "translated", - "value": "儲存" + "state": "new", + "value": "Go to Chat" } }, "pt-BR": { "stringUnit": { - "state": "translated", - "value": "Salvar" + "state": "new", + "value": "Go to Chat" } }, "de": { "stringUnit": { - "state": "translated", - "value": "Speichern" + "state": "new", + "value": "Go to Chat" } }, "es": { "stringUnit": { - "state": "translated", - "value": "Guardar" + "state": "new", + "value": "Go to Chat" } }, "ja-JP": { "stringUnit": { - "state": "translated", - "value": "保存" + "state": "new", + "value": "Go to Chat" } }, "ko": { "stringUnit": { - "state": "translated", - "value": "저장" + "state": "new", + "value": "Go to Chat" } }, "fr": { "stringUnit": { - "state": "translated", - "value": "Enregistrer" + "state": "new", + "value": "Go to Chat" } }, "hi": { "stringUnit": { - "state": "translated", - "value": "सहेजें" + "state": "new", + "value": "Go to Chat" } }, "ar": { "stringUnit": { - "state": "translated", - "value": "حفظ" + "state": "new", + "value": "Go to Chat" } }, "it": { "stringUnit": { - "state": "translated", - "value": "Salva" + "state": "new", + "value": "Go to Chat" } }, "tr": { "stringUnit": { - "state": "translated", - "value": "Kaydet" + "state": "new", + "value": "Go to Chat" } }, "uk": { "stringUnit": { - "state": "translated", - "value": "Зберегти" + "state": "new", + "value": "Go to Chat" } }, "id": { "stringUnit": { - "state": "translated", - "value": "Simpan" + "state": "new", + "value": "Go to Chat" } }, "pl": { "stringUnit": { - "state": "translated", - "value": "Zapisz" + "state": "new", + "value": "Go to Chat" } }, "th": { "stringUnit": { - "state": "translated", - "value": "บันทึก" + "state": "new", + "value": "Go to Chat" } }, "vi": { "stringUnit": { - "state": "translated", - "value": "Lưu" + "state": "new", + "value": "Go to Chat" } }, "nl": { "stringUnit": { - "state": "translated", - "value": "Opslaan" + "state": "new", + "value": "Go to Chat" } }, "fa": { "stringUnit": { - "state": "translated", - "value": "ذخیره" + "state": "new", + "value": "Go to Chat" } }, "ru": { "stringUnit": { - "state": "translated", - "value": "Сохранить" + "state": "new", + "value": "Go to Chat" } }, "sv": { "stringUnit": { - "state": "translated", - "value": "Spara" + "state": "new", + "value": "Go to Chat" } } } diff --git a/apps/ios/Sources/Design/OpenClawBrand.swift b/apps/ios/Sources/Design/OpenClawBrand.swift index 88c4bd593e2..f1857b99b2d 100644 --- a/apps/ios/Sources/Design/OpenClawBrand.swift +++ b/apps/ios/Sources/Design/OpenClawBrand.swift @@ -1,4 +1,5 @@ import Observation +import OpenClawChatUI import SwiftUI enum AppAppearancePreference: String, CaseIterable, Identifiable { @@ -79,11 +80,9 @@ enum OpenClawBrand { static let uiAccentForeground = adaptiveUIColor(light: (183, 56, 51), dark: (255, 107, 102)) static let uiAccentHot = adaptiveUIColor(light: (204, 75, 69), dark: (232, 92, 86)) static let uiAccentHotForeground = adaptiveUIColor(light: (166, 55, 50), dark: (255, 123, 115)) - static let uiAccentPressed = adaptiveUIColor(light: (148, 40, 36), dark: (148, 40, 36)) static let uiTeal = adaptiveUIColor(light: (0, 196, 176), dark: (0, 196, 176)) static let uiVoid = adaptiveUIColor(light: (246, 247, 249), dark: (11, 12, 17)) static let uiObsidian = adaptiveUIColor(light: (255, 255, 255), dark: (19, 21, 28)) - static let uiTextPrimary = adaptiveUIColor(light: (11, 12, 17), dark: (242, 239, 232)) static let uiTextSecondary = adaptiveUIColor(light: (90, 94, 110), dark: (168, 170, 191)) static let uiOK = adaptiveUIColor(light: (19, 122, 62), dark: (48, 209, 88)) static let uiWarn = adaptiveUIColor(light: (154, 87, 0), dark: (255, 214, 10)) @@ -94,11 +93,9 @@ enum OpenClawBrand { static let accentForeground = Color(uiColor: Self.uiAccentForeground) static let accentHot = Color(uiColor: Self.uiAccentHot) static let accentHotForeground = Color(uiColor: Self.uiAccentHotForeground) - static let accentPressed = Color(uiColor: Self.uiAccentPressed) static let teal = Color(uiColor: Self.uiTeal) static let void = Color(uiColor: Self.uiVoid) static let obsidian = Color(uiColor: Self.uiObsidian) - static let textPrimary = Color(uiColor: Self.uiTextPrimary) static let textSecondary = Color(uiColor: Self.uiTextSecondary) static let danger = Color(uiColor: Self.uiDanger) static let ok = Color(uiColor: Self.uiOK) @@ -106,6 +103,31 @@ enum OpenClawBrand { static let info = Color(uiColor: Self.uiInfo) static let graphite = void static let graphiteElevated = obsidian + static let activationCanvas = Color(uiColor: adaptiveUIColor(light: (255, 255, 255), dark: (18, 14, 15))) + static let activationSurface = Color(uiColor: adaptiveUIColor(light: (255, 253, 252), dark: (33, 29, 30))) + static let activationSecondaryActionTop = Color(uiColor: adaptiveUIColor( + light: (255, 255, 255), + dark: (36, 32, 33))) + static let activationSecondaryActionBottom = Color(uiColor: adaptiveUIColor( + light: (248, 248, 250), + dark: (30, 27, 28))) + static let activationInsetSurface = Color(uiColor: adaptiveUIColor(light: (246, 241, 238), dark: (44, 37, 38))) + static let activationNeutralSurface = Color(uiColor: adaptiveUIColor(light: (242, 242, 247), dark: (34, 34, 37))) + static let activationNeutralInsetSurface = Color(uiColor: adaptiveUIColor( + light: (247, 247, 249), + dark: (42, 42, 45))) + static let activationNeutralStroke = Color(uiColor: adaptiveUIColor( + light: (0, 0, 0), + dark: (255, 255, 255)).withAlphaComponent(0.08)) + static let activationNeutralDivider = Color(uiColor: adaptiveUIColor( + light: (0, 0, 0), + dark: (255, 255, 255)).withAlphaComponent(0.09)) + static let activationPrimaryAction = Color(uiColor: adaptiveUIColor(light: (209, 54, 51), dark: (238, 82, 76))) + static let activationPrimaryActionText = Color.white + static let activationHairline = Color(uiColor: adaptiveUIColor( + light: (136, 44, 40), + dark: (255, 210, 205)).withAlphaComponent(0.13)) + static let activationGlow = Color(uiColor: adaptiveUIColor(light: (228, 78, 67), dark: (255, 111, 96))) static var sheetBackground: LinearGradient { LinearGradient( @@ -118,6 +140,59 @@ enum OpenClawBrand { endPoint: .bottomTrailing) } + static var activationCanvasGradient: LinearGradient { + LinearGradient( + colors: [ + activationCanvas, + activationCanvas, + ], + startPoint: .topLeading, + endPoint: .bottomTrailing) + } + + static var activationInsetGradient: LinearGradient { + LinearGradient( + colors: [ + activationInsetSurface.opacity(0.94), + activationInsetSurface, + activationSurface.opacity(0.36), + ], + startPoint: .top, + endPoint: .bottom) + } + + static var activationNeutralGradient: LinearGradient { + LinearGradient( + colors: [ + activationNeutralInsetSurface, + activationNeutralSurface, + ], + startPoint: .top, + endPoint: .bottom) + } + + static var activationPrimaryGradient: LinearGradient { + LinearGradient( + colors: [ + Color(red: 1.0, green: 0.42, blue: 0.34), + activationGlow, + activationPrimaryAction, + Color(red: 0.66, green: 0.12, blue: 0.12), + ], + startPoint: .topLeading, + endPoint: .bottomTrailing) + } + + static var activationDisabledGradient: LinearGradient { + LinearGradient( + colors: [ + activationNeutralInsetSurface, + activationNeutralSurface.opacity(0.92), + ], + startPoint: .top, + endPoint: .bottom) + } + private static func adaptiveUIColor( light: (red: CGFloat, green: CGFloat, blue: CGFloat), dark: (red: CGFloat, green: CGFloat, blue: CGFloat)) -> UIColor @@ -133,6 +208,21 @@ enum OpenClawBrand { } } +struct OpenClawActivationGlyph: View { + let size: CGFloat + + var body: some View { + OpenClawMascotView(floats: false) + .frame(width: self.size, height: self.size) + .shadow( + color: OpenClawBrand.activationGlow.opacity(0.18), + radius: self.size * 0.12, + x: 0, + y: self.size * 0.05) + .accessibilityHidden(true) + } +} + extension View { func openClawSheetChrome() -> some View { self @@ -142,4 +232,222 @@ extension View { .ignoresSafeArea() } } + + func openClawCraftSurface(cornerRadius: CGFloat = 24, shadow: Bool = true) -> some View { + self.modifier(OpenClawCraftSurfaceModifier(cornerRadius: cornerRadius, shadow: shadow)) + } +} + +private struct OpenClawCraftSurfaceModifier: ViewModifier { + let cornerRadius: CGFloat + let shadow: Bool + + func body(content: Content) -> some View { + content + .background { + RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) + .fill(OpenClawBrand.activationSurface) + .shadow( + color: self.shadow ? Color.black.opacity(0.07) : .clear, + radius: 16, + x: 0, + y: 8) + } + .overlay(alignment: .top) { + RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) + .stroke(Color.white.opacity(0.36), lineWidth: 0.5) + .blendMode(.plusLighter) + } + .overlay { + RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) + .stroke(OpenClawBrand.activationHairline, lineWidth: 0.5) + } + } +} + +struct OpenClawPrimaryActionButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + var height: CGFloat = 54 + var cornerRadius: CGFloat = 18 + + private var resolvedCornerRadius: CGFloat { + self.height / 2 + } + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(self.isEnabled ? OpenClawBrand.activationPrimaryActionText : Color.secondary) + .tint(self.isEnabled ? OpenClawBrand.activationPrimaryActionText : Color.secondary) + .frame(maxWidth: .infinity) + .frame(height: self.height) + .background { + RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous) + .fill(self.isEnabled ? Self.primaryFill : OpenClawBrand.activationDisabledGradient) + .shadow( + color: self.isEnabled ? OpenClawBrand.activationPrimaryAction.opacity(0.08) : .clear, + radius: 1, + x: 0, + y: 1) + .shadow( + color: self.isEnabled ? OpenClawBrand.activationPrimaryAction.opacity(0.08) : .clear, + radius: 2, + x: 0, + y: 2) + } + .overlay(alignment: .top) { + RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous) + .fill( + LinearGradient( + colors: [ + Color.white.opacity(self.isEnabled ? 0.14 : 0.06), + Color.white.opacity(0), + ], + startPoint: .top, + endPoint: .center)) + .frame(height: self.height * 0.48) + .allowsHitTesting(false) + } + .overlay(alignment: .bottom) { + RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous) + .fill( + LinearGradient( + colors: [ + Color.white.opacity(0), + Color.white.opacity(self.isEnabled ? 0.08 : 0.03), + ], + startPoint: .top, + endPoint: .bottom)) + .frame(height: self.height * 0.34) + .allowsHitTesting(false) + } + .overlay { + RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous) + .stroke( + self.isEnabled + ? OpenClawBrand.activationPrimaryAction.opacity(0.72) + : OpenClawBrand.activationNeutralStroke, + lineWidth: 0.75) + } + .scaleEffect(configuration.isPressed && self.isEnabled ? 0.98 : 1) + .animation(.smooth(duration: 0.14), value: configuration.isPressed) + } + + private static var primaryFill: LinearGradient { + LinearGradient( + colors: [ + Color(red: 0.93, green: 0.27, blue: 0.25), + Color(red: 0.91, green: 0.25, blue: 0.24), + ], + startPoint: .top, + endPoint: .bottom) + } +} + +struct OpenClawSecondaryActionButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + var height: CGFloat = 50 + var cornerRadius: CGFloat = 18 + var shadowOpacity: Double = 0.035 + + private var resolvedCornerRadius: CGFloat { + self.height / 2 + } + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(self.isEnabled ? OpenClawBrand.activationPrimaryAction : .secondary) + .frame(maxWidth: .infinity) + .frame(height: self.height) + .background { + RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous) + .fill(Self.secondaryFill) + .shadow( + color: self.isEnabled ? Color.black.opacity(self.shadowOpacity) : .clear, + radius: 1, + x: 0, + y: 1) + .shadow( + color: self.isEnabled ? Color.black.opacity(self.shadowOpacity) : .clear, + radius: 2, + x: 0, + y: 2) + } + .overlay(alignment: .top) { + RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous) + .fill( + LinearGradient( + colors: [ + Color.white.opacity(0.42), + Color.white.opacity(0), + ], + startPoint: .top, + endPoint: .center)) + .frame(height: self.height * 0.48) + .allowsHitTesting(false) + } + .overlay { + RoundedRectangle(cornerRadius: self.resolvedCornerRadius, style: .continuous) + .stroke(OpenClawBrand.activationHairline, lineWidth: 0.75) + } + .scaleEffect(configuration.isPressed && self.isEnabled ? 0.98 : 1) + .animation(.smooth(duration: 0.14), value: configuration.isPressed) + } + + private static var secondaryFill: LinearGradient { + LinearGradient( + colors: [ + OpenClawBrand.activationSecondaryActionTop, + OpenClawBrand.activationSecondaryActionBottom, + ], + startPoint: .top, + endPoint: .bottom) + } +} + +struct OpenClawCloseButtonStyle: ButtonStyle { + @Environment(\.isEnabled) private var isEnabled + var minWidth: CGFloat = 36 + var height: CGFloat = 36 + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(OpenClawType.subheadSemiBold) + .foregroundStyle( + self.isEnabled + ? OpenClawBrand.activationPrimaryAction + : Color.secondary) + .fixedSize(horizontal: true, vertical: false) + .frame(minWidth: self.minWidth) + .frame(height: self.height) + .padding(.horizontal, 7) + .background { + Capsule(style: .continuous) + .fill(OpenClawBrand.activationNeutralGradient) + .shadow( + color: self.isEnabled ? Color.black.opacity(0.045) : .clear, + radius: 1, + x: 0, + y: 1) + .shadow( + color: self.isEnabled ? Color.black.opacity(0.03) : .clear, + radius: 4, + x: 0, + y: 2) + } + .overlay(alignment: .top) { + Capsule(style: .continuous) + .stroke(Color.white.opacity(0.55), lineWidth: 0.5) + .blendMode(.plusLighter) + } + .overlay { + Capsule(style: .continuous) + .stroke(OpenClawBrand.activationNeutralStroke, lineWidth: 0.6) + } + .contentShape(Capsule(style: .continuous)) + .opacity(configuration.isPressed && self.isEnabled ? 0.66 : 1) + .scaleEffect(configuration.isPressed && self.isEnabled ? 0.98 : 1) + .animation(.smooth(duration: 0.14), value: configuration.isPressed) + } } diff --git a/apps/ios/Sources/Design/OpenClawProComponents.swift b/apps/ios/Sources/Design/OpenClawProComponents.swift index 4aa97fdf413..d2ea62c2f03 100644 --- a/apps/ios/Sources/Design/OpenClawProComponents.swift +++ b/apps/ios/Sources/Design/OpenClawProComponents.swift @@ -21,7 +21,6 @@ enum OpenClawRadius { static let xs: CGFloat = 8 static let sm: CGFloat = 10 static let md: CGFloat = 12 - static let lg: CGFloat = 16 } struct OpenClawProBackground: View { @@ -561,62 +560,6 @@ struct OpenClawStatusBadge: View { } } -struct OpenClawPrimaryButtonStyle: ButtonStyle { - @Environment(\.isEnabled) private var isEnabled - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(OpenClawType.headline) - .foregroundStyle(self.isEnabled ? Color.white : OpenClawBrand.textSecondary) - .frame(maxWidth: .infinity, minHeight: 48) - .background { - RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous) - .fill( - !self.isEnabled - ? Color(uiColor: .tertiarySystemFill) - : configuration.isPressed - ? OpenClawBrand.accentPressed - : OpenClawBrand.accent) - } - .animation(.easeOut(duration: 0.15), value: configuration.isPressed) - .animation(.easeOut(duration: 0.15), value: self.isEnabled) - } -} - -struct OpenClawSecondaryButtonStyle: ButtonStyle { - @Environment(\.isEnabled) private var isEnabled - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(OpenClawType.headline) - .foregroundStyle(self.isEnabled ? OpenClawBrand.textPrimary : OpenClawBrand.textSecondary.opacity(0.68)) - .frame(maxWidth: .infinity, minHeight: 48) - .background { - RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous) - .fill(Color(uiColor: self.isEnabled ? .secondarySystemBackground : .tertiarySystemFill)) - .overlay { - RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous) - .strokeBorder( - Color(uiColor: .separator).opacity(self.isEnabled ? 0.35 : 0.20), - lineWidth: 1) - } - } - .opacity(!self.isEnabled ? 0.74 : configuration.isPressed ? 0.82 : 1) - .animation(.easeOut(duration: 0.15), value: configuration.isPressed) - .animation(.easeOut(duration: 0.15), value: self.isEnabled) - } -} - -extension View { - func openClawPrimaryButton() -> some View { - self.buttonStyle(OpenClawPrimaryButtonStyle()) - } - - func openClawSecondaryButton() -> some View { - self.buttonStyle(OpenClawSecondaryButtonStyle()) - } -} - struct ProStatusDot: View { var color: Color diff --git a/apps/ios/Sources/Design/OpenClawTypography.swift b/apps/ios/Sources/Design/OpenClawTypography.swift index 34f3f7dfddd..65651ba21fe 100644 --- a/apps/ios/Sources/Design/OpenClawTypography.swift +++ b/apps/ios/Sources/Design/OpenClawTypography.swift @@ -129,10 +129,6 @@ enum OpenClawType { scaledMono(name: Mono.regular, size: 13, relativeTo: .footnote) } - static var monoHeadline: Font { - scaledMono(name: Mono.medium, size: 17, relativeTo: .headline) - } - /// PostScript names for bundled fonts. Keep aligned with `UIAppFonts` in `project.yml`. static let registeredPostScriptNames: [String] = [ Display.postScriptName, diff --git a/apps/ios/Sources/Design/TalkSiriWave.swift b/apps/ios/Sources/Design/TalkSiriWave.swift index 827832e918e..aa8d45a8f04 100644 --- a/apps/ios/Sources/Design/TalkSiriWave.swift +++ b/apps/ios/Sources/Design/TalkSiriWave.swift @@ -85,9 +85,13 @@ struct TalkSiriWaveView: View { // across the line instead of scrolling off-screen. let lobes: [(A: Double, k: Double, t: Double)] = (0..<3).map { index in let f = Double(index) - let amp = 0.30 + 0.70 * (0.5 + 0.5 * sin(time * (0.9 + 0.23 * f) + seed * 2.4 + f * 2.1)) + let ampFrequency = 0.9 + 0.23 * f + let ampPhase = time * ampFrequency + seed * 2.4 + f * 2.1 + let amp = 0.30 + 0.70 * (0.5 + 0.5 * sin(ampPhase)) let k = 0.62 + 0.11 * f - let t = 2.8 * sin(time * (0.45 + 0.17 * f) + seed + f * 1.9) + let driftFrequency = 0.45 + 0.17 * f + let driftPhase = time * driftFrequency + seed + f * 1.9 + let t = 2.8 * sin(driftPhase) return (A: amp, k: k, t: t) } diff --git a/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift index a5f3321aa0c..8ff0c790f51 100644 --- a/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift +++ b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift @@ -1,6 +1,10 @@ import OpenClawKit import SwiftUI +#if DEBUG +import Network +#endif + struct GatewayQuickSetupSheet: View { @Environment(NodeAppModel.self) private var appModel @Environment(GatewayConnectionController.self) private var gatewayController @@ -13,95 +17,88 @@ struct GatewayQuickSetupSheet: View { var body: some View { NavigationStack { - VStack(alignment: .leading, spacing: 16) { - Text("Connect to a Gateway?") - .font(OpenClawType.title2) + ScrollView { + VStack(alignment: .leading, spacing: 18) { + GatewayQuickSetupHeader(hasCandidate: self.bestCandidate != nil) - 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 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) + if let candidate = self.bestCandidate { + GatewayQuickSetupCandidatePanel( + name: candidate.name, + debugID: candidate.debugID, + tlsEnabled: candidate.tlsEnabled, + gatewayPort: candidate.gatewayPort, + 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) + Button { + self.connectError = nil + self.connecting = true + Task { + let err = await self.gatewayController.connectWithDiagnostics(candidate) + await MainActor.run { + self.connecting = false + self.connectError = err } - } else { - Text("Connect") - .font(OpenClawType.headline) } + } label: { + Group { + if self.connecting { + HStack(spacing: 8) { + ProgressView().progressViewStyle(.circular) + Text("Connecting…") + .font(OpenClawType.subheadSemiBold) + } + } else { + Text("Connect to this Gateway") + .font(OpenClawType.subheadSemiBold) + } + } + .frame(maxWidth: .infinity) } - } - .font(OpenClawType.headline) - .openClawPrimaryButton() - .disabled(self.connecting) + .buttonStyle(OpenClawPrimaryActionButtonStyle()) + .disabled(self.connecting) - if let connectError { - Text(connectError) - .font(OpenClawType.footnote) - .foregroundStyle(.secondary) - .textSelection(.enabled) - } + if let connectError { + GatewayQuickSetupErrorView(message: connectError) + } - Button { - self.dismiss() - } label: { - Text("Not now") - .font(OpenClawType.headline) - } - .font(OpenClawType.headline) - .openClawSecondaryButton() - .disabled(self.connecting) + Button { + self.dismiss() + } label: { + Text("Not now") + .font(OpenClawType.subheadSemiBold) + .frame(maxWidth: .infinity) + } + .buttonStyle(OpenClawSecondaryActionButtonStyle()) + .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) + self.fullRowToggle("Don't show this again", isOn: self.$quickSetupDismissed) + .padding(.top, 2) + } else { + GatewayQuickSetupEmptyState( + discoveryStatusText: self.gatewayController.discoveryStatusText) + } } - - Spacer() + .padding(20) } - .font(OpenClawType.body) - .padding() + .background(OpenClawBrand.activationCanvasGradient) .navigationTitle("Quick Setup") .navigationBarTitleDisplayMode(.inline) .toolbar { - ToolbarItem(placement: .principal) { - Text("Quick Setup") - .font(OpenClawType.headline) - } ToolbarItem(placement: .topBarTrailing) { Button { self.quickSetupDismissed = true @@ -110,7 +107,7 @@ struct GatewayQuickSetupSheet: View { Text("Close") .font(OpenClawType.subheadSemiBold) } - .font(OpenClawType.subheadSemiBold) + .buttonStyle(OpenClawCloseButtonStyle()) } } } @@ -127,7 +124,6 @@ struct GatewayQuickSetupSheet: View { } private var bestCandidate: GatewayDiscoveryModel.DiscoveredGateway? { - // Prefer whatever discovery says is first; the list is already name-sorted. self.gatewayController.gateways.first } @@ -136,10 +132,8 @@ struct GatewayQuickSetupSheet: View { 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: { @@ -147,7 +141,6 @@ struct GatewayQuickSetupSheet: View { .fill(.clear) .contentShape(Rectangle()) } - .font(OpenClawType.subhead) .buttonStyle(.plain) .accessibilityHidden(true) } @@ -175,46 +168,291 @@ struct GatewayQuickSetupSheet: View { } } -private struct GatewayQuickSetupCandidatePanel: View { - private static let readableMonospaceWidth: CGFloat = 72 * 8 +private struct GatewayQuickSetupHeader: View { + let hasCandidate: Bool + var body: some View { + VStack(alignment: .leading, spacing: 14) { + ZStack(alignment: .bottomTrailing) { + OpenClawActivationGlyph(size: 70) + .shadow(color: OpenClawBrand.activationGlow.opacity(0.18), radius: 10, x: 0, y: 5) + + Image(systemName: "antenna.radiowaves.left.and.right") + .font(OpenClawType.caption2SemiBold) + .foregroundStyle(OpenClawBrand.activationPrimaryActionText) + .frame(width: 28, height: 28) + .background { + Circle() + .fill(OpenClawBrand.activationPrimaryGradient) + } + .overlay { + Circle() + .stroke(OpenClawBrand.activationCanvas, lineWidth: 3) + } + .offset(x: 4, y: 4) + } + + VStack(alignment: .leading, spacing: 6) { + Text("Connect a nearby Gateway") + .font(OpenClawType.title2SemiBold) + Text(self.subtitle) + .font(OpenClawType.subhead) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var subtitle: LocalizedStringKey { + if self.hasCandidate { + return "OpenClaw found a gateway on this network. Review it, then pair this iPhone as a secure node." + } + return "OpenClaw is searching the local network and tailnet for a Gateway you can trust." + } +} + +private struct GatewayQuickSetupCandidatePanel: View { let name: String let debugID: String + let tlsEnabled: Bool + let gatewayPort: Int? 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: 14) { + HStack(alignment: .top, spacing: 12) { + Image(systemName: "network") + .font(OpenClawType.headline) + .foregroundStyle(OpenClawBrand.activationPrimaryActionText) + .frame(width: 36, height: 36) + .background { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(OpenClawBrand.activationPrimaryGradient) + } + .shadow(color: OpenClawBrand.activationGlow.opacity(0.18), radius: 6, x: 0, y: 3) - 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) + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(verbatim: self.name) + .font(OpenClawType.headline) + .foregroundStyle(.primary) + .lineLimit(1) + .truncationMode(.middle) + + GatewayQuickSetupChip(text: self.tlsEnabled ? "Secure" : "Local") + } + + Text(verbatim: self.endpointSummary) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + } + } + + GatewayQuickSetupHairlineDivider() + + VStack(spacing: 8) { + GatewayQuickSetupStatusRow(title: "Discovery", value: self.discoveryStatusText) + GatewayQuickSetupStatusRow(title: "Gateway", value: self.gatewayDisplayStatusText) + GatewayQuickSetupStatusRow(title: "Node", value: self.nodeStatusText) + GatewayQuickSetupStatusRow(title: "Operator", value: self.operatorStatusText) } - .font(OpenClawType.monoFootnote) - .foregroundStyle(.secondary) } - .frame(maxWidth: Self.readableMonospaceWidth, alignment: .leading) - .padding(.vertical, 14) - .padding(.horizontal, 16) + .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) - .background(OpenClawBrand.obsidian) - .clipShape(RoundedRectangle(cornerRadius: OpenClawRadius.lg)) + .openClawCraftSurface(cornerRadius: 22) + .accessibilityElement(children: .combine) + } + + private var endpointSummary: String { + let port = self.gatewayPort.map { ":\($0)" } ?? "" + return "\(self.debugID)\(port)" } } + +private struct GatewayQuickSetupHairlineDivider: View { + var body: some View { + Rectangle() + .fill(OpenClawBrand.activationNeutralDivider) + .frame(height: 0.5) + .frame(maxWidth: .infinity) + } +} + +private struct GatewayQuickSetupChip: View { + let text: LocalizedStringKey + + var body: some View { + Text(self.text) + .font(OpenClawType.caption2SemiBold) + .foregroundStyle(OpenClawBrand.activationPrimaryAction) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .background { + Capsule(style: .continuous) + .fill(OpenClawBrand.activationGlow.opacity(0.10)) + } + .overlay { + Capsule(style: .continuous) + .stroke(OpenClawBrand.activationHairline, lineWidth: 0.6) + } + } +} + +private struct GatewayQuickSetupStatusRow: View { + let title: LocalizedStringKey + let value: String + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 12) { + Text(self.title) + .font(OpenClawType.captionMedium) + .foregroundStyle(.secondary) + .frame(width: 70, alignment: .leading) + Text(verbatim: self.value) + .font(OpenClawType.footnote) + .foregroundStyle(.primary) + .lineLimit(2) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +private struct GatewayQuickSetupErrorView: View { + let message: String + + var body: some View { + HStack(alignment: .top, spacing: 10) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(OpenClawBrand.warn) + .padding(.top, 1) + Text(self.message) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + } + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(OpenClawBrand.activationInsetGradient) + } + .overlay { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(OpenClawBrand.activationHairline, lineWidth: 0.7) + } + } +} + +private struct GatewayQuickSetupEmptyState: View { + let discoveryStatusText: String + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + Image(systemName: "magnifyingglass") + .font(OpenClawType.title2SemiBold) + .foregroundStyle(OpenClawBrand.activationPrimaryAction) + .frame(width: 42, height: 42) + .background { + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(OpenClawBrand.activationInsetGradient) + } + + VStack(alignment: .leading, spacing: 6) { + Text("Looking for a Gateway") + .font(OpenClawType.headline) + Text("Keep your iPhone on the same LAN or tailnet, then start the Gateway on your host machine.") + .font(OpenClawType.subhead) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + VStack(alignment: .leading, spacing: 6) { + GatewayQuickSetupInstructionRow(text: "Run openclaw gateway --port 18789.") + GatewayQuickSetupInstructionRow(text: "Check that Bonjour discovery is enabled.") + GatewayQuickSetupInstructionRow(text: "Open Settings if you need a manual host.") + } + .padding(.top, 2) + + Text(verbatim: "Discovery: \(self.discoveryStatusText)") + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .openClawCraftSurface(cornerRadius: 22) + } +} + +private struct GatewayQuickSetupInstructionRow: View { + let text: LocalizedStringKey + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Image(systemName: "arrow.right.circle.fill") + .font(OpenClawType.captionSemiBold) + .foregroundStyle(.secondary) + Text(self.text) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + } + } +} + +#if DEBUG +#Preview("Quick setup gateway", traits: .fixedLayout(width: 393, height: 520)) { + GatewayQuickSetupPreviewHost(hasGateway: true) +} + +#Preview("Quick setup searching", traits: .fixedLayout(width: 393, height: 520)) { + GatewayQuickSetupPreviewHost(hasGateway: false) +} + +private struct GatewayQuickSetupPreviewHost: View { + @State private var appModel: NodeAppModel + @State private var gatewayController: GatewayConnectionController + + init(hasGateway: Bool) { + let appModel = NodeAppModel() + let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false) + if hasGateway { + controller._test_setGateways([.previewGateway]) + appModel.gatewayStatusText = "Ready to pair" + } + _appModel = State(initialValue: appModel) + _gatewayController = State(initialValue: controller) + } + + var body: some View { + GatewayQuickSetupSheet() + .environment(self.appModel) + .environment(self.gatewayController) + .openClawSheetChrome() + } +} + +extension GatewayDiscoveryModel.DiscoveredGateway { + fileprivate static let previewGateway = GatewayDiscoveryModel.DiscoveredGateway( + name: "Studio Gateway", + endpoint: .hostPort( + host: .name("openclaw.local", nil), + port: 18789), + stableID: "preview-gateway", + debugID: "openclaw.local", + lanHost: "openclaw.local", + tailnetDns: nil, + gatewayPort: 18789, + canvasPort: 18789, + tlsEnabled: true, + tlsFingerprintSha256: "preview", + cliPath: "/opt/homebrew/bin/openclaw") +} +#endif diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift b/apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift index 819e3ea6616..d76af02962a 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift @@ -1,127 +1,279 @@ import SwiftUI import UIKit -private enum OnboardingLayout { - static let contentMaxWidth: CGFloat = 680 +private enum OnboardingVisual { + static let maxWidth: CGFloat = 430 +} + +private struct OnboardingActivationCanvas: View { + @ViewBuilder let content: Content + + var body: some View { + GeometryReader { proxy in + ScrollView { + self.content + .frame(maxWidth: OnboardingVisual.maxWidth) + .frame(maxWidth: .infinity) + .frame(minHeight: max(0, proxy.size.height - 94), alignment: .top) + .padding(.horizontal, 20) + .padding(.top, 54) + .padding(.bottom, 40) + } + .scrollIndicators(.hidden) + .background(OpenClawBrand.activationCanvasGradient.ignoresSafeArea()) + } + } +} + +private struct OnboardingHeroGlyph: View { + var body: some View { + OpenClawActivationGlyph(size: 78) + } +} + +private struct OnboardingHeroHeader: View { + let title: LocalizedStringKey + let subtitle: LocalizedStringKey? + + var body: some View { + VStack(spacing: 18) { + OnboardingHeroGlyph() + + VStack(spacing: 8) { + Text(self.title) + .font(OpenClawType.title1) + .multilineTextAlignment(.center) + + if let subtitle { + Text(subtitle) + .font(OpenClawType.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .frame(maxWidth: .infinity) + } +} + +private struct OnboardingWelcomePrompt: View { + let text: LocalizedStringKey + + var body: some View { + Text(self.text) + .font(OpenClawType.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .fixedSize(horizontal: false, vertical: true) + } +} + +private typealias OnboardingPrimaryButtonStyle = OpenClawPrimaryActionButtonStyle + +private enum OnboardingIntroPanelStyle { + static let iconSize: CGFloat = 34 + static let contentSpacing: CGFloat = 12 + static let panelPadding: CGFloat = 16 + static let panelCornerRadius: CGFloat = 22 + + static let panelFill = OpenClawBrand.activationNeutralSurface + static let iconFill = OpenClawBrand.activationNeutralInsetSurface + static let stroke = OpenClawBrand.activationNeutralStroke +} + +private struct OnboardingIntroPanel: View { + @ViewBuilder let content: Content + + var body: some View { + self.content + .padding(Self.panelPadding) + .frame(maxWidth: .infinity, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: OnboardingIntroPanelStyle.panelCornerRadius, style: .continuous) + .fill(OnboardingIntroPanelStyle.panelFill) + } + .overlay(alignment: .top) { + RoundedRectangle(cornerRadius: OnboardingIntroPanelStyle.panelCornerRadius, style: .continuous) + .stroke(Color.white.opacity(0.42), lineWidth: 0.5) + .blendMode(.plusLighter) + } + .overlay { + RoundedRectangle(cornerRadius: OnboardingIntroPanelStyle.panelCornerRadius, style: .continuous) + .stroke(OnboardingIntroPanelStyle.stroke, lineWidth: 0.5) + } + } + + private static var panelPadding: CGFloat { + OnboardingIntroPanelStyle.panelPadding + } +} + +private struct OnboardingIntroIcon: View { + let symbol: String + let tint: Color + + var body: some View { + Image(systemName: self.symbol) + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(self.tint) + .frame( + width: OnboardingIntroPanelStyle.iconSize, + height: OnboardingIntroPanelStyle.iconSize) + .background { + Circle() + .fill(OnboardingIntroPanelStyle.iconFill) + } + .overlay { + Circle() + .stroke(OnboardingIntroPanelStyle.stroke, lineWidth: 0.6) + } + } +} + +private struct OnboardingSafetyRow: View { + let symbol: String + let title: LocalizedStringKey + + var body: some View { + HStack(spacing: OnboardingIntroPanelStyle.contentSpacing) { + OnboardingIntroIcon( + symbol: self.symbol, + tint: OpenClawBrand.activationPrimaryAction) + + Text(self.title) + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(.primary) + .fixedSize(horizontal: false, vertical: true) + } + .accessibilityElement(children: .combine) + } +} + +private struct OnboardingSecurityNotice: View { + var body: some View { + OnboardingIntroPanel { + HStack(alignment: .top, spacing: OnboardingIntroPanelStyle.contentSpacing) { + OnboardingIntroIcon( + symbol: "exclamationmark.triangle.fill", + tint: OpenClawBrand.warn) + + VStack(alignment: .leading, spacing: 6) { + Text("Security notice") + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(.primary) + ( + Text("The connected OpenClaw agent can use device capabilities you enable.") + + Text(verbatim: " ") + + Text( + "Camera, microphone, photos, contacts, calendar, and location may be available.") + + Text(verbatim: " ") + + Text( + "Continue only if you trust the gateway and agent you connect to.")) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .accessibilityElement(children: .combine) + } +} + +private struct OnboardingCommandChip: View { + @State private var didCopy = false + private let command = "openclaw qr" + + var body: some View { + HStack(spacing: 8) { + Text(self.command) + .font(OpenClawType.mono) + .textSelection(.enabled) + Spacer(minLength: 0) + Button { + self.copyCommand() + } label: { + Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc") + .font(OpenClawType.subheadSemiBold) + .foregroundStyle( + self.didCopy ? OpenClawBrand.activationPrimaryAction : Color.secondary.opacity(0.56)) + .frame(width: 38, height: 38) + .contentTransition(.symbolEffect(.replace)) + } + .buttonStyle(.plain) + .contentShape(Rectangle()) + .accessibilityLabel("Copy setup code command") + .accessibilityValue(self.didCopy ? "Copied" : self.command) + } + .foregroundStyle(OpenClawBrand.activationPrimaryAction) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity, alignment: .leading) + .frame(height: 54) + .background { + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(OpenClawBrand.activationNeutralSurface) + } + .overlay { + RoundedRectangle(cornerRadius: 20, style: .continuous) + .stroke(OpenClawBrand.activationNeutralStroke, lineWidth: 0.5) + } + } + + private func copyCommand() { + UIPasteboard.general.string = self.command + withAnimation(.smooth(duration: 0.14)) { + self.didCopy = true + } + Task { @MainActor in + try? await Task.sleep(nanoseconds: 1_100_000_000) + withAnimation(.smooth(duration: 0.16)) { + self.didCopy = false + } + } + } } struct OnboardingIntroStep: View { let onContinue: () -> Void - private static let features: [(icon: String, title: String, detail: String)] = [ - ("link", "Connect your gateway", "Pair with a quick QR scan."), - ("hand.raised.fill", "You choose permissions", "Grant only the tools you want."), - ("bubble.left.and.bubble.right.fill", "Chat, voice, and camera", "All from your phone."), - ] - var body: some View { - VStack(spacing: 0) { - ScrollView { - VStack(spacing: 0) { - OpenClawProMark(size: 76, shadowRadius: 12) - .padding(.top, 32) - .padding(.bottom, 20) + OnboardingActivationCanvas { + VStack(alignment: .leading, spacing: 0) { + OnboardingHeroHeader( + title: "OpenClaw", + subtitle: "Securely connect this iPhone to your gateway.") + .padding(.top, 18) - Text("Welcome to OpenClaw") - .font(OpenClawType.title1) - .multilineTextAlignment(.center) - .padding(.horizontal, 24) - .padding(.bottom, 40) - - VStack(alignment: .leading, spacing: 28) { - ForEach(Array(Self.features.enumerated()), id: \.offset) { _, feature in - OnboardingFeatureRow(icon: feature.icon, title: feature.title, detail: feature.detail) - } - - GroupBox { - Text( - "The connected agent can use capabilities you enable, including camera, " - + "microphone, photos, contacts, calendar, and location. Continue only if " - + "you trust the gateway and agent.") - .font(OpenClawType.footnote) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - } label: { - Label("Security", systemImage: "lock.shield.fill") - .font(OpenClawType.headline) - } - .tint(OpenClawBrand.warn) + OnboardingIntroPanel { + VStack(alignment: .leading, spacing: 14) { + OnboardingSafetyRow( + symbol: "link", + title: "Connect to your gateway") + OnboardingSafetyRow( + symbol: "hand.raised", + title: "Choose device permissions") + OnboardingSafetyRow( + symbol: "message.fill", + title: "Use OpenClaw from your phone") } - .padding(.horizontal, 12) } - .frame(maxWidth: .infinity) - .padding(.horizontal, 20) - } - .scrollBounceBehavior(.basedOnSize) + .padding(.top, 44) - VStack(spacing: 16) { - Text("You can change permissions later in Settings.") - .font(OpenClawType.footnote) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .center) + OnboardingSecurityNotice() + .padding(.top, 18) - Button(action: self.onContinue) { - Text("Continue") - .font(OpenClawType.headline) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - .tint(OpenClawBrand.accent) - } - .padding(.horizontal, 20) - .padding(.top, 12) - .padding(.bottom, 16) - } - .frame(maxWidth: OnboardingLayout.contentMaxWidth) - .frame(maxWidth: .infinity) - } -} + Spacer(minLength: 40) -/// Inline command styled like a keyboard key so shell/chat commands stand out from prose. -struct KeycapText: View { - let text: String - - init(_ text: String) { - self.text = text - } - - var body: some View { - Text(self.text) - .font(OpenClawType.monoFootnote) - .padding(.horizontal, 9) - .padding(.vertical, 4) - .background { - RoundedRectangle(cornerRadius: 7, style: .continuous) - .fill(Color(uiColor: .secondarySystemFill)) - .overlay { - RoundedRectangle(cornerRadius: 7, style: .continuous) - .strokeBorder(Color(uiColor: .separator).opacity(0.6), lineWidth: 0.5) + VStack(spacing: 14) { + Button { + self.onContinue() + } label: { + Text("Continue") + .font(OpenClawType.subheadSemiBold) } - } - } -} - -private struct OnboardingFeatureRow: View { - let icon: String - let title: String - let detail: String - - var body: some View { - HStack(alignment: .top, spacing: 16) { - Image(systemName: self.icon) - .font(OpenClawType.title2) - .foregroundStyle(OpenClawBrand.accent) - .frame(width: 34, alignment: .center) - .accessibilityHidden(true) - - VStack(alignment: .leading, spacing: 2) { - Text(self.title) - .font(OpenClawType.headline) - Text(self.detail) - .font(OpenClawType.subhead) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) + .buttonStyle(OnboardingPrimaryButtonStyle()) + } } } } @@ -129,118 +281,195 @@ private struct OnboardingFeatureRow: View { struct OnboardingWelcomeStep: View { let statusLine: String + let isConnecting: Bool let onScanQRCode: () -> Void let onManualSetup: () -> Void - @State private var pairCommandCopied = false + var body: some View { + let statusText = self.statusLine.trimmingCharacters(in: .whitespacesAndNewlines) + + OnboardingActivationCanvas { + VStack(alignment: .leading, spacing: 0) { + OnboardingHeroHeader( + title: "Connect Gateway", + subtitle: nil) + .padding(.top, 18) + + VStack(spacing: 36) { + VStack(spacing: 14) { + OnboardingWelcomePrompt(text: "Run this on your gateway host and scan the code") + + OnboardingCommandChip() + + Button(action: self.onScanQRCode) { + if self.isConnecting { + HStack(spacing: 8) { + ProgressView() + .progressViewStyle(.circular) + .tint(OpenClawBrand.activationPrimaryActionText) + Text("Connecting…") + .font(OpenClawType.subheadSemiBold) + } + } else { + Text("Scan QR") + .font(OpenClawType.subheadSemiBold) + } + } + .buttonStyle(OnboardingPrimaryButtonStyle()) + .disabled(self.isConnecting) + } + + VStack(spacing: 14) { + OnboardingWelcomePrompt(text: "or") + + Button(action: self.onManualSetup) { + Text("Connect Manually") + .font(OpenClawType.subheadSemiBold) + } + .buttonStyle(OpenClawSecondaryActionButtonStyle(height: 54, shadowOpacity: 0.018)) + .disabled(self.isConnecting) + } + } + .padding(.top, 46) + + if !statusText.isEmpty { + Text(verbatim: statusText) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + .multilineTextAlignment(.center) + .padding(.horizontal, 6) + .padding(.top, 14) + .transition(.opacity) + } + + Spacer(minLength: 40) + } + .animation(.smooth(duration: 0.18), value: statusText) + } + } +} + +struct OnboardingSuccessStep: View { + let gatewayName: String + let gatewayAddress: String? + let onGetStarted: () -> Void var body: some View { - VStack(spacing: 0) { - Spacer() + OnboardingActivationCanvas { + VStack(spacing: 0) { + Spacer(minLength: 54) - Image(systemName: "qrcode.viewfinder") - .font(.system(size: 64)) - .foregroundStyle(OpenClawBrand.accent) - .padding(.bottom, 24) + ZStack(alignment: .bottomTrailing) { + OpenClawActivationGlyph(size: 86) + .shadow(color: OpenClawBrand.activationGlow.opacity(0.18), radius: 12, x: 0, y: 6) - Text("Connect Gateway") - .font(OpenClawType.title1) - .padding(.bottom, 12) - - HStack(spacing: 8) { - KeycapText("/pair qr") - Button { - var transaction = Transaction() - transaction.disablesAnimations = true - withTransaction(transaction) { - UIPasteboard.general.string = "/pair qr" - self.pairCommandCopied = true - } - } label: { - Label( - self.pairCommandCopied ? "Copied" : "Copy", - systemImage: self.pairCommandCopied ? "checkmark" : "doc.on.doc") - .font(OpenClawType.captionSemiBold) + Image(systemName: "checkmark") + .font(OpenClawType.headlineBold) + .foregroundStyle(.white) + .frame(width: 30, height: 30) + .background { + Circle() + .fill(OpenClawBrand.ok) + } + .overlay { + Circle() + .stroke(OpenClawBrand.activationCanvas, lineWidth: 3) + } } - .buttonStyle(.bordered) - .controlSize(.small) - .tint(.primary) - .animation(nil, value: self.pairCommandCopied) - .accessibilityLabel(self.pairCommandCopied ? "Pair command copied" : "Copy pair command") - .accessibilityIdentifier("onboarding-copy-pair-command") - } - .padding(.bottom, 10) + .padding(.bottom, 22) - Text("Run this in your OpenClaw chat, then scan the code.") - .font(OpenClawType.subhead) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal, 40) + Text("You're connected") + .font(OpenClawType.title1) + .multilineTextAlignment(.center) + .padding(.bottom, 8) - Spacer() - - VStack(spacing: 4) { - Button { - self.onScanQRCode() - } label: { - Label("Scan QR Code", systemImage: "qrcode") - .font(OpenClawType.headline) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - .tint(OpenClawBrand.accent) - - Button(action: self.onManualSetup) { - Text("Set Up Manually") - .font(OpenClawType.headline) - } - .buttonStyle(.borderless) - .controlSize(.large) - .tint(OpenClawBrand.accent) - .padding(.top, 12) - } - .padding(.horizontal, OpenClawSpacing.space6) - .padding(.bottom, 12) - - if !self.statusLine.isEmpty { - Text(self.statusLine) - .font(OpenClawType.footnote) + Text(verbatim: self.gatewayName) + .font(OpenClawType.subheadSemiBold) .foregroundStyle(.secondary) .multilineTextAlignment(.center) - .padding(.horizontal, 24) - } - Color.clear.frame(height: 12) + if let gatewayAddress, !gatewayAddress.isEmpty { + Text(verbatim: gatewayAddress) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .padding(.top, 4) + } + + Spacer(minLength: 40) + + Button { + self.onGetStarted() + } label: { + Label("Go to Chat", systemImage: "bubble.left.and.bubble.right.fill") + .font(OpenClawType.subheadSemiBold) + } + .buttonStyle(OnboardingPrimaryButtonStyle()) + } } - .frame(maxWidth: OnboardingLayout.contentMaxWidth) - .frame(maxWidth: .infinity) + } +} + +struct OnboardingModeIcon: View { + let symbol: String + let selected: Bool + + var body: some View { + Image(systemName: self.symbol) + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(self.selected ? OpenClawBrand.activationPrimaryActionText : .secondary) + .frame(width: 34, height: 34) + .background { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(self.selected ? OpenClawBrand.activationPrimaryGradient : OpenClawBrand + .activationNeutralGradient) + .shadow( + color: self.selected ? OpenClawBrand.activationGlow.opacity(0.18) : .clear, + radius: 5, + x: 0, + y: 2) + } + .overlay { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke( + self.selected ? Color.white.opacity(0.30) : OpenClawBrand.activationNeutralStroke, + lineWidth: 0.5) + } } } struct OnboardingModeRow: View { - let title: String - let subtitle: String + let title: LocalizedStringKey + let subtitle: LocalizedStringKey + let symbol: String let selected: Bool let action: () -> Void var body: some View { Button(action: self.action) { - HStack { + HStack(spacing: 12) { + OnboardingModeIcon(symbol: self.symbol, selected: self.selected) + VStack(alignment: .leading, spacing: 2) { Text(self.title) - .font(OpenClawType.headline) + .font(OpenClawType.subheadSemiBold) Text(self.subtitle) .font(OpenClawType.footnote) .foregroundStyle(.secondary) } Spacer() Image(systemName: self.selected ? "checkmark.circle.fill" : "circle") - .foregroundStyle(self.selected ? OpenClawBrand.accent : Color.secondary) + .font(self.selected ? .title3.weight(.semibold) : .title3.weight(.regular)) + .foregroundStyle( + self.selected + ? OpenClawBrand.activationPrimaryAction + : Color(uiColor: .quaternaryLabel).opacity(0.55)) } + .padding(.vertical, 6) + .frame(minHeight: 52) .contentShape(Rectangle()) } - .font(OpenClawType.subhead) .buttonStyle(.plain) } } diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index d73f404debb..f0f85d287a0 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -24,13 +24,13 @@ private enum OnboardingStep: Int, CaseIterable { return "Step \(idx + 1) of \(manualSteps.count)" } - var title: String { + var title: LocalizedStringKey { switch self { case .intro: "Welcome" case .welcome: "Connect Gateway" - case .mode: "Connection Mode" - case .connect: "Connect" - case .auth: "Authentication" + case .mode: "Gateway Setup" + case .connect: "Gateway Details" + case .auth: "Gateway Status" case .success: "Connected" } } @@ -60,6 +60,15 @@ struct GatewaySetupLinkStaging { } } +private enum OnboardingFocusedField: Hashable { + case setupCode + case manualHost + case manualPort + case discoveryDomain + case gatewayToken + case gatewayPassword +} + struct OnboardingWizardView: View { @Environment(NodeAppModel.self) private var appModel: NodeAppModel @Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController @@ -96,6 +105,7 @@ struct OnboardingWizardView: View { @State private var setupCode: String = "" @State private var setupCodeStatus: String? @State private var setupAttemptID: UUID? + @FocusState private var focusedField: OnboardingFocusedField? private static let pairingAutoResumeTicker = Timer.publish(every: 2.0, on: .main, in: .common).autoconnect() let allowSkip: Bool @@ -160,11 +170,15 @@ struct OnboardingWizardView: View { EmptyView() } } + .formStyle(.grouped) + .scrollContentBackground(.hidden) + .background(Color(uiColor: .systemGroupedBackground)) .scrollDismissesKeyboard(.interactively) } } .navigationTitle(self.isFullScreenStep ? "" : self.step.title) .navigationBarTitleDisplayMode(.inline) + .tint(OpenClawBrand.activationPrimaryAction) .toolbar { if !self.isFullScreenStep { ToolbarItem(placement: .principal) { @@ -177,42 +191,16 @@ struct OnboardingWizardView: View { } } } - ToolbarItem(placement: .topBarLeading) { - if self.step.canGoBack { - Button { - self.navigateBack() - } label: { - Label("Back", systemImage: "chevron.left") - .font(OpenClawType.subheadSemiBold) - } - .font(OpenClawType.subheadSemiBold) - } else if self.allowSkip { - Button { - self.invalidateSetupAttempt() - self.onClose() - } label: { - Text("Close") - .font(OpenClawType.subheadSemiBold) - } - .font(OpenClawType.subheadSemiBold) - } - } - ToolbarItemGroup(placement: .keyboard) { - Spacer() - Button { - UIApplication.shared.sendAction( - #selector(UIResponder.resignFirstResponder), - to: nil, - from: nil, - for: nil) - } label: { - Text("Done") - .font(OpenClawType.subheadSemiBold) - } - .font(OpenClawType.subheadSemiBold) - } } } + .safeAreaInset(edge: .bottom, alignment: .trailing, spacing: 0) { + self.keyboardDismissControl + } + .overlay(alignment: .topLeading) { + self.leadingChromeButton + .padding(.leading, 16) + .padding(.top, 10) + } .gatewayTrustPromptAlert() .alert("QR Scanner Unavailable", isPresented: Binding( get: { self.scannerError != nil }, @@ -298,7 +286,7 @@ struct OnboardingWizardView: View { OnboardingStateStore.markCompleted(mode: selectedMode) self.didMarkCompleted = true } - self.step = .success + self.navigate(to: .success) } } @@ -320,11 +308,11 @@ struct OnboardingWizardView: View { self.showQRScanner = false }) .ignoresSafeArea() - .navigationTitle("Scan QR Code") + .navigationTitle("Scan Setup Code") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { - Text("Scan QR Code") + Text("Scan Setup Code") .font(OpenClawType.headline) } ToolbarItem(placement: .topBarLeading) { @@ -372,75 +360,135 @@ struct OnboardingWizardView: View { } } + @ViewBuilder + private var leadingChromeButton: some View { + if self.step.canGoBack { + Button { + self.navigateBack() + } label: { + Image(systemName: "chevron.left") + .font(OpenClawType.subheadSemiBold) + .accessibilityLabel("Back") + } + .buttonStyle(OpenClawCloseButtonStyle()) + } else if self.allowSkip { + Button { + self.invalidateSetupAttempt() + self.onClose() + } label: { + Text("Close") + .font(OpenClawType.subheadSemiBold) + } + .buttonStyle(OpenClawCloseButtonStyle()) + } + } + + @ViewBuilder + private var keyboardDismissControl: some View { + if self.focusedField != nil { + Button { + self.dismissKeyboard() + } label: { + Image(systemName: "keyboard.chevron.compact.down") + .font(OpenClawType.headline) + .frame(width: 50, height: 44) + .contentShape(Capsule(style: .continuous)) + } + .buttonStyle(.plain) + .foregroundStyle(OpenClawBrand.activationPrimaryAction) + .background(.ultraThinMaterial, in: Capsule(style: .continuous)) + .overlay { + Capsule(style: .continuous) + .stroke(OpenClawBrand.activationNeutralStroke, lineWidth: 0.6) + } + .shadow(color: Color.black.opacity(0.08), radius: 14, x: 0, y: 4) + .accessibilityLabel("Dismiss Keyboard") + .padding(.trailing, 20) + .padding(.bottom, 10) + .transition(.opacity.combined(with: .scale(scale: 0.96))) + .animation(.smooth(duration: 0.16), value: self.focusedField) + } + } + private var introStep: some View { - OnboardingIntroStep(onContinue: advanceFromIntro) + OnboardingIntroStep(onContinue: self.advanceFromIntro) } private var welcomeStep: some View { OnboardingWelcomeStep( statusLine: self.statusLine, + isConnecting: self.connectingGatewayID != nil, onScanQRCode: { self.openQRScannerFromOnboarding() }, onManualSetup: { self.invalidateSetupAttempt() - self.step = .mode + self.statusLine = "" + self.navigate(to: .mode) }) } @ViewBuilder private var modeStep: some View { - setupCodeSection + self.setupCodeSection Section { OnboardingModeRow( - title: OnboardingConnectionMode.homeNetwork.title, + title: "Home Network", subtitle: "LAN or Tailscale host", + symbol: "house.and.flag", selected: self.selectedMode == .homeNetwork) { self.selectMode(.homeNetwork) } OnboardingModeRow( - title: OnboardingConnectionMode.remoteDomain.title, + title: "Remote Domain", subtitle: "VPS with domain", + symbol: "globe", selected: self.selectedMode == .remoteDomain) { self.selectMode(.remoteDomain) } - self.developerModeToggleRow - if self.developerModeEnabled { + self.developerModeToggleRow + OnboardingModeRow( - title: OnboardingConnectionMode.developerLocal.title, + title: "Same Machine (Dev)", subtitle: "For local iOS app development", + symbol: "wrench.and.screwdriver", selected: self.selectedMode == .developerLocal) { self.selectMode(.developerLocal) } } } header: { - Text("Connection Mode") - .font(OpenClawType.captionSemiBold) + Text("Manual Connection") + .font(OpenClawType.footnoteSemiBold) + .padding(.top, 12) } .disabled(self.connectingGatewayID != nil) Section { Button { - self.step = .connect + self.navigate(to: .connect) } label: { Text("Continue") .font(OpenClawType.subheadSemiBold) } - .font(OpenClawType.subheadSemiBold) .disabled(self.selectedMode == nil || self.connectingGatewayID != nil) + .buttonStyle(OpenClawPrimaryActionButtonStyle(height: 48, cornerRadius: 16)) + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) } } private var developerModeToggleRow: some View { self.onboardingButtonToggle( "Developer mode", + symbol: "wrench.and.screwdriver", isOn: Binding( get: { self.developerModeEnabled }, set: { enabled in @@ -451,23 +499,36 @@ struct OnboardingWizardView: View { })) } - private func onboardingButtonToggle(_ title: String, isOn: Binding) -> some View { - // Onboarding Form switch rows need full-width taps; native Toggle only hits the switch edge on iOS 26. - Button { - isOn.wrappedValue.toggle() - } label: { - HStack { + private func onboardingButtonToggle( + _ title: LocalizedStringKey, + symbol: String? = nil, + isOn: Binding) -> some View + { + Toggle(isOn: isOn) { + HStack(spacing: 12) { + if let symbol { + OnboardingModeIcon(symbol: symbol, selected: false) + } + Text(title) - .font(OpenClawType.subhead) - Spacer(minLength: 8) - OpenClawToggleIndicator(isOn: isOn.wrappedValue) + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(.primary) } - .contentShape(Rectangle()) + .frame(minHeight: 52) + } + .tint(OpenClawBrand.activationPrimaryAction) + .contentShape(Rectangle()) + .overlay { + Button { + isOn.wrappedValue.toggle() + } label: { + Rectangle() + .fill(.clear) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityHidden(true) } - .font(OpenClawType.subhead) - .buttonStyle(.plain) - .accessibilityLabel(title) - .accessibilityValue(isOn.wrappedValue ? "On" : "Off") } @ViewBuilder @@ -477,14 +538,14 @@ struct OnboardingWizardView: View { self.onboardingLabeledContent("Mode", value: selectedMode.title) self.onboardingLabeledContent("Discovery", value: self.gatewayController.discoveryStatusText) self.onboardingLabeledContent("Status", value: self.appModel.gatewayDisplayStatusText) - self.onboardingLabeledContent("Progress", value: self.statusLine) + self.onboardingLabeledContent("Progress", value: self.statusLine.isEmpty ? "Ready" : self.statusLine) } header: { Text("Status") - .font(OpenClawType.captionSemiBold) + .font(OpenClawType.footnoteSemiBold) } footer: { if let connectMessage { Text(connectMessage) - .font(OpenClawType.caption) + .font(OpenClawType.footnote) } } @@ -503,14 +564,13 @@ struct OnboardingWizardView: View { } else { Section { Text("Choose a mode first.") - .font(OpenClawType.subhead) + .font(OpenClawType.body) Button { - self.step = .mode + self.navigate(to: .mode) } label: { Text("Back to Mode Selection") .font(OpenClawType.subheadSemiBold) } - .font(OpenClawType.subheadSemiBold) } } } @@ -549,13 +609,13 @@ struct OnboardingWizardView: View { .disabled(self.connectingGatewayID != nil) } header: { Text("Setup Link") - .font(OpenClawType.captionSemiBold) + .font(OpenClawType.footnoteSemiBold) } footer: { Text(link.tls ? "Review this endpoint. Credentials are applied only after you tap Connect." : "Plaintext may expose credentials. Continue only if you trust this local network and host.") - .font(OpenClawType.caption) + .font(OpenClawType.footnote) } } @@ -564,7 +624,7 @@ struct OnboardingWizardView: View { Section { if self.gatewayController.gateways.isEmpty { Text("No gateways found yet.") - .font(OpenClawType.subhead) + .font(OpenClawType.body) .foregroundStyle(.secondary) } else { ForEach(self.gatewayController.gateways) { gateway in @@ -573,7 +633,7 @@ struct OnboardingWizardView: View { HStack { VStack(alignment: .leading, spacing: 4) { Text(gateway.name) - .font(OpenClawType.subheadSemiBold) + .font(OpenClawType.body) if let host = gateway.lanHost ?? gateway.tailnetDns { Text(host) .font(OpenClawType.footnote) @@ -611,7 +671,7 @@ struct OnboardingWizardView: View { .disabled(self.connectingGatewayID != nil) } header: { Text("Discovered Gateways") - .font(OpenClawType.captionSemiBold) + .font(OpenClawType.footnoteSemiBold) } self.manualConnectionFieldsSection(title: "Manual Fallback") @@ -619,36 +679,36 @@ struct OnboardingWizardView: View { } private var remoteDomainConnectSection: some View { - manualConnectionFieldsSection(title: "Domain Settings") + self.manualConnectionFieldsSection(title: "Domain Settings") } private var developerConnectSection: some View { Section { - TextField("Host", text: self.manualHostBinding) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .font(OpenClawType.subhead) - TextField("Port", text: self.manualPortTextBinding) + self.onboardingTextField("Host", text: self.manualHostBinding, focusedField: .manualHost) + self.onboardingTextField("Port", text: self.manualPortTextBinding, focusedField: .manualPort) .keyboardType(.numberPad) - .font(OpenClawType.subhead) self.onboardingButtonToggle("Use TLS", isOn: self.$manualTLS) self.manualConnectButton } header: { Text("Developer Local") - .font(OpenClawType.captionSemiBold) + .font(OpenClawType.footnoteSemiBold) } footer: { Text("Default host is localhost. Use your Mac LAN IP if simulator networking requires it.") - .font(OpenClawType.caption) + .font(OpenClawType.footnote) } } private var authStep: some View { Group { Section { - self.onboardingSecureField("Gateway Auth Token", text: self.gatewayTokenBinding) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - self.onboardingSecureField("Gateway Password", text: self.gatewayPasswordBinding) + self.onboardingSecureField( + "Gateway Auth Token", + text: self.gatewayTokenBinding, + focusedField: .gatewayToken) + self.onboardingSecureField( + "Gateway Password", + text: self.gatewayPasswordBinding, + focusedField: .gatewayPassword) if let problem = self.currentProblem { GatewayProblemBanner( @@ -661,17 +721,17 @@ struct OnboardingWizardView: View { self.showGatewayProblemDetails = true }) } else if self.issue.needsAuthToken { - Text("Gateway rejected credentials. Scan a fresh QR code or update token/password.") + Text("Gateway rejected credentials. Scan a fresh setup code or update token/password.") .font(OpenClawType.footnote) .foregroundStyle(.secondary) } else { - Text("Auth token looks valid.") + Text("OpenClaw is checking gateway and node access.") .font(OpenClawType.footnote) .foregroundStyle(.secondary) } } header: { - Text("Authentication") - .font(OpenClawType.captionSemiBold) + Text(self.gatewayStatusSectionTitle) + .font(OpenClawType.footnoteSemiBold) } if self.issue.needsPairing { @@ -686,7 +746,7 @@ struct OnboardingWizardView: View { .disabled(self.connectingGatewayID != nil) } header: { Text("Pairing Approval") - .font(OpenClawType.captionSemiBold) + .font(OpenClawType.footnoteSemiBold) } footer: { let requestLine: String = { if let id = self.currentProblem?.requestId ?? self.issue.requestId, !id.isEmpty { @@ -709,7 +769,7 @@ struct OnboardingWizardView: View { Button { self.openQRScannerFromOnboarding() } label: { - Label("Scan QR Code Again", systemImage: "qrcode.viewfinder") + Label("Scan Setup Code Again", systemImage: "qrcode.viewfinder") .font(OpenClawType.subheadSemiBold) } .font(OpenClawType.subheadSemiBold) @@ -733,70 +793,54 @@ struct OnboardingWizardView: View { } private var successStep: some View { - VStack(spacing: 0) { - Spacer() - - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 64)) - .foregroundStyle(OpenClawBrand.ok) - .padding(.bottom, 20) - - Text("Connected") - .font(OpenClawType.title1) - .foregroundStyle(OpenClawBrand.textPrimary) - .padding(.bottom, 8) - - Text(self.successEndpoint) - .font(OpenClawType.subhead) - .foregroundStyle(.secondary) - - Spacer() - - Button { - self.onComplete() - } label: { - Label("Go to Chat", systemImage: "bubble.left.and.bubble.right.fill") - .font(OpenClawType.headline) - } - .font(OpenClawType.headline) - .openClawPrimaryButton() - .padding(.horizontal, OpenClawSpacing.space6) - .padding(.bottom, 48) - } + OnboardingSuccessStep( + gatewayName: self.appModel.gatewayServerName ?? "gateway", + gatewayAddress: self.appModel.gatewayRemoteAddress, + onGetStarted: self.onComplete) } } extension OnboardingWizardView { + private var gatewayStatusSectionTitle: String { + if self.issue.needsPairing || self.currentProblem?.needsPairingApproval == true { + return "Gateway Approval" + } + if self.issue.needsAuthToken || self.currentProblem != nil { + return "Authentication" + } + return "Gateway Status" + } + private var setupCodeSection: some View { Section { - TextField("Paste setup code", text: self.$setupCode) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .font(OpenClawType.subhead) - .disabled(self.connectingGatewayID != nil) - .onSubmit { - Task { await self.applySetupCodeAndConnect() } - } + HStack(spacing: 12) { + self.onboardingTextField("Enter setup code", text: self.$setupCode, focusedField: .setupCode) + .lineLimit(1) + .submitLabel(.go) + .onSubmit { + guard self.canApplySetupCode else { return } + Task { await self.applySetupCodeAndConnect() } + } - Button { - Task { await self.applySetupCodeAndConnect() } - } label: { - if self.connectingGatewayID == "setup-code" { - HStack(spacing: 8) { + Button { + Task { await self.applySetupCodeAndConnect() } + } label: { + if self.connectingGatewayID == "setup-code" { ProgressView() .progressViewStyle(.circular) - Text("Applying...") + .controlSize(.small) + } else { + Text("Apply") .font(OpenClawType.subheadSemiBold) } - } else { - Text("Apply Setup Code") - .font(OpenClawType.subheadSemiBold) } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .tint(OpenClawBrand.activationPrimaryAction) + .disabled(!self.canApplySetupCode) } - .font(OpenClawType.subheadSemiBold) - .disabled( - self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - || self.connectingGatewayID != nil) + .frame(minHeight: 50) if let setupCodeStatus, !setupCodeStatus.isEmpty { Text(setupCodeStatus) @@ -805,65 +849,92 @@ extension OnboardingWizardView { } } header: { Text("Setup Code") - .font(OpenClawType.captionSemiBold) + .font(OpenClawType.footnoteSemiBold) } footer: { - Text("Use this if you received a setup code instead of a QR code.") - .font(OpenClawType.caption) + Text("Use this if you have a setup code instead of scanning.") + .font(OpenClawType.footnote) } } - private func manualConnectionFieldsSection(title: String) -> some View { + private var canApplySetupCode: Bool { + !self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + && self.connectingGatewayID == nil + } + + private func manualConnectionFieldsSection(title: LocalizedStringKey) -> some View { Section { - TextField("Host", text: self.manualHostBinding) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .font(OpenClawType.subhead) - TextField("Port", text: self.manualPortTextBinding) + self.onboardingTextField("Host", text: self.manualHostBinding, focusedField: .manualHost) + self.onboardingTextField("Port", text: self.manualPortTextBinding, focusedField: .manualPort) .keyboardType(.numberPad) - .font(OpenClawType.subhead) self.onboardingButtonToggle("Use TLS", isOn: self.$manualTLS) - TextField("Discovery Domain (optional)", text: self.$discoveryDomain) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .font(OpenClawType.subhead) + self.onboardingTextField( + "Discovery Domain (optional)", + text: self.$discoveryDomain, + focusedField: .discoveryDomain) if self.selectedMode == .remoteDomain { - self.onboardingSecureField("Gateway Auth Token", text: self.gatewayTokenBinding) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - self.onboardingSecureField("Gateway Password", text: self.gatewayPasswordBinding) + self.onboardingSecureField( + "Gateway Auth Token", + text: self.gatewayTokenBinding, + focusedField: .gatewayToken) + self.onboardingSecureField( + "Gateway Password", + text: self.gatewayPasswordBinding, + focusedField: .gatewayPassword) } self.manualConnectButton } header: { Text(title) - .font(OpenClawType.captionSemiBold) + .font(OpenClawType.footnoteSemiBold) } } - private func onboardingLabeledContent(_ title: String, value: String) -> some View { + private func onboardingLabeledContent(_ title: LocalizedStringKey, value: String) -> some View { LabeledContent { - Text(value) - .font(OpenClawType.subhead) - .foregroundStyle(.secondary) + Text(verbatim: value) + .font(OpenClawType.body) } label: { Text(title) - .font(OpenClawType.subheadSemiBold) + .font(OpenClawType.body) } } - private func onboardingSecureField(_ placeholder: String, text: Binding) -> some View { - ZStack(alignment: .leading) { - SecureField("", text: text) + private func onboardingTextField( + _ placeholder: LocalizedStringKey, + text: Binding, + focusedField: OnboardingFocusedField) -> some View + { + TextField( + "", + text: text, + prompt: Text(placeholder) .font(OpenClawType.subhead) - .accessibilityLabel(placeholder) + .foregroundStyle(.tertiary)) + .font(OpenClawType.subhead) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .focused(self.$focusedField, equals: focusedField) + .accessibilityLabel(placeholder) + } + + private func onboardingSecureField( + _ placeholder: LocalizedStringKey, + text: Binding, + focusedField: OnboardingFocusedField) -> some View + { + ZStack(alignment: .leading) { if text.wrappedValue.isEmpty { Text(placeholder) - .font(OpenClawType.subheadSemiBold) + .font(OpenClawType.subhead) .foregroundStyle(.tertiary) - .allowsHitTesting(false) .accessibilityHidden(true) } + SecureField("", text: text) + .font(OpenClawType.subhead) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .focused(self.$focusedField, equals: focusedField) } - .font(OpenClawType.subhead) + .accessibilityLabel(placeholder) } private var manualConnectButton: some View { @@ -914,10 +985,11 @@ extension OnboardingWizardView { await self.applyGatewayLink(link) self.setupCode = "" - self.setupCodeStatus = "Setup code applied. Connecting..." - self.connectMessage = "Connecting via setup code..." - self.statusLine = "Setup code loaded. Connecting to \(link.host):\(link.port)..." - self.step = .connect + self.setupCodeStatus = "Setup code applied. Connecting…" + self.connectMessage = "Connecting via setup code…" + self.statusLine = "Setup code loaded. Connecting to \(link.host):\(link.port)…" + await self.prepareFocusedFieldForStepTransition() + self.navigate(to: .connect) await self.connectManual(setupAttemptID: attemptID) } @@ -956,9 +1028,9 @@ extension OnboardingWizardView { let link = await self.gatewayController.selectReachableSetupLink(parsedLink) guard self.setupAttemptID == attemptID else { return } await self.applyGatewayLink(link) - self.connectMessage = "Connecting via QR code..." - self.statusLine = "QR loaded. Connecting to \(link.host):\(link.port)..." - self.step = .connect + self.connectMessage = "Connecting via setup code…" + self.statusLine = "Setup code loaded. Connecting to \(link.host):\(link.port)…" + self.navigate(to: .connect) await self.connectManual(setupAttemptID: attemptID) } @@ -976,7 +1048,7 @@ extension OnboardingWizardView { self.setupCodeStatus = "Setup link loaded for \(link.host):\(link.port). Tap Connect to apply." self.connectMessage = nil self.statusLine = self.setupCodeStatus ?? "" - self.step = .connect + self.navigate(to: .connect) } private func connectStagedGatewaySetupLink() async { @@ -997,7 +1069,7 @@ extension OnboardingWizardView { guard self.setupLinkStaging.link == link else { return } _ = self.setupLinkStaging.take() await self.applyGatewayLink(link, disconnectExistingGatewayForBootstrap: false) - self.setupCodeStatus = "Setup link applied. Connecting..." + self.setupCodeStatus = "Setup link applied. Connecting…" self.issue = .none self.connectMessage = "Connecting to \(link.host)…" self.statusLine = "Connecting to \(link.host):\(link.port)…" @@ -1167,7 +1239,7 @@ extension OnboardingWizardView { OnboardingStateStore.markFirstRunIntroSeen() self.requestLocalNetworkAccess(reason: "onboarding_continue") self.statusLine = "" - self.step = .welcome + self.navigate(to: .welcome) } private func requestLocalNetworkAccessIfPastIntro(reason: String) { @@ -1183,6 +1255,27 @@ extension OnboardingWizardView { guard let target = step.previous else { return } self.invalidateSetupAttempt() self.connectMessage = nil + self.navigate(to: target) + } + + private func prepareFocusedFieldForStepTransition() async { + guard self.focusedField != nil else { return } + self.dismissKeyboard() + await Task.yield() + try? await Task.sleep(nanoseconds: 120_000_000) + } + + private func dismissKeyboard() { + self.focusedField = nil + UIApplication.shared.sendAction( + #selector(UIResponder.resignFirstResponder), + to: nil, + from: nil, + for: nil) + } + + private func navigate(to target: OnboardingStep) { + self.focusedField = nil self.step = target } @@ -1209,15 +1302,6 @@ extension OnboardingWizardView { return !host.isEmpty && self.resolvedManualPort(host: host) != nil } - private var successEndpoint: String { - let serverName = self.appModel.gatewayServerName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if !serverName.isEmpty { - return serverName - } - let remoteAddress = self.appModel.gatewayRemoteAddress?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return remoteAddress.isEmpty ? "gateway" : remoteAddress - } - private func initializeState() { if self.manualHost.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { if let last = GatewaySettingsStore.loadLastGatewayConnection() { @@ -1239,7 +1323,13 @@ extension OnboardingWizardView { } self.manualPortText = self.manualPort > 0 ? String(self.manualPort) : "" if self.selectedMode == nil { - self.selectedMode = OnboardingStateStore.lastMode() + let lastMode = OnboardingStateStore.lastMode() + if lastMode == .developerLocal { + self.developerModeEnabled = true + } + if self.developerModeEnabled || lastMode != .developerLocal { + self.selectedMode = lastMode + } } if self.selectedMode == .developerLocal, self.manualHost == "openclaw.local" { self.manualHost = "localhost" @@ -1551,7 +1641,7 @@ extension OnboardingWizardView { self.connectMessage = nil self.issue = .none self.pairingRequestId = nil - self.step = .connect + self.navigate(to: .connect) self.openQRScannerFromOnboarding(status: "Scan a fresh setup QR code from this gateway.") return } diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index ffb73f9ba00..9d3a5911fdb 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -918,8 +918,8 @@ struct RootTabs: View { self.showOnboarding = false }, onComplete: { - self.selectSidebarDestination(.chat) self.showOnboarding = false + self.selectSidebarDestination(.chat) }) .environment(self.appModel) .environment(self.voiceWake) diff --git a/apps/ios/Tests/OpenClawTypographyTests.swift b/apps/ios/Tests/OpenClawTypographyTests.swift index 15f35e50023..44391cbcf42 100644 --- a/apps/ios/Tests/OpenClawTypographyTests.swift +++ b/apps/ios/Tests/OpenClawTypographyTests.swift @@ -170,33 +170,44 @@ struct OpenClawTypographyTests { #expect(proComponents.contains(".font(OpenClawType.subheadSemiBold)")) #expect(proComponents.contains("Text(primaryActionTitle)")) #expect(proComponents.contains("Text(secondaryActionTitle)")) - #expect(proComponents.contains("@Environment(\\.isEnabled)")) - #expect(proComponents.contains(".animation(.easeOut(duration: 0.15), value: self.isEnabled)")) #expect(chatTab.contains("Text(\"Export Transcript\")")) #expect(chatTab.contains(".font(OpenClawType.body)")) #expect(!chatTab.contains("Button(\"Export Transcript\")")) #expect(!quickSetup.contains("Button(\"Close\")")) - #expect(quickSetup.contains("Text(\"Quick Setup\")")) + #expect(quickSetup.contains(".navigationTitle(\"Quick Setup\")")) #expect(quickSetup.contains("Text(\"Close\")")) #expect(quickSetup.contains(".font(OpenClawType.subheadSemiBold)")) + #expect(quickSetup.contains("let text: LocalizedStringKey")) + #expect(!quickSetup.contains("Text(verbatim: self.text)")) #expect(gatewayProblem.contains("Text(\"Connection problem\")")) #expect(gatewayProblem.contains("Text(\"Copy request ID\")")) #expect(gatewayProblem.contains("Text(\"Copy command\")")) #expect(gatewayProblem.contains(".font(OpenClawType.subheadSemiBold)")) - #expect(onboardingSteps.contains("Text(\"Connect Gateway\")")) - #expect(onboardingSteps.contains("Label(\"Scan QR Code\", systemImage: \"qrcode\")")) - #expect(onboardingSteps.contains("Text(\"Set Up Manually\")")) - #expect(onboardingSteps.contains(".font(OpenClawType.headline)")) + #expect(onboardingSteps.contains("title: \"Connect Gateway\"")) + #expect(onboardingSteps.contains("Text(\"Scan QR\")")) + #expect(onboardingSteps.contains("Text(\"Connect Manually\")")) + #expect(onboardingSteps.contains("Label(\"Go to Chat\", systemImage: \"bubble.left.and.bubble.right.fill\")")) + #expect(onboardingSteps.contains(".font(OpenClawType.subheadSemiBold)")) + #expect(onboardingSteps.contains("let title: LocalizedStringKey")) + #expect(onboardingSteps.contains("let subtitle: LocalizedStringKey?")) + #expect(onboardingSteps.contains("let text: LocalizedStringKey")) - #expect(onboardingWizard.contains("Text(\"Scan QR Code\")")) + #expect(onboardingWizard.contains("Text(\"Scan Setup Code\")") + || onboardingWizard.contains(".navigationTitle(\"Scan Setup Code\")")) #expect(onboardingWizard.contains("Label(\"Resume After Approval\", systemImage: \"arrow.clockwise\")")) - #expect(onboardingWizard.contains("Label(\"Scan QR Code Again\", systemImage: \"qrcode.viewfinder\")")) - #expect(onboardingWizard.contains("Text(\"Apply Setup Code\")")) + #expect(onboardingWizard.contains("Label(\"Scan Setup Code Again\", systemImage: \"qrcode.viewfinder\")")) + #expect(onboardingWizard.contains("Text(\"Apply\")")) #expect(onboardingWizard.contains(".font(OpenClawType.subheadSemiBold)")) + #expect(onboardingWizard.contains("_ title: LocalizedStringKey")) + #expect(onboardingWizard.contains("_ placeholder: LocalizedStringKey")) + #expect(onboardingWizard.contains("if self.developerModeEnabled {")) + #expect(onboardingWizard.contains("title: \"Same Machine (Dev)\"")) + #expect(onboardingWizard.contains("if lastMode == .developerLocal")) + #expect(onboardingWizard.contains("self.developerModeEnabled = true")) #expect(settingsSections.contains(".font(OpenClawType.body)")) #expect(settingsSections.contains("Toggle(isOn: self.$talkButtonEnabled)")) diff --git a/apps/ios/Tests/SwiftUIRenderSmokeTests.swift b/apps/ios/Tests/SwiftUIRenderSmokeTests.swift index 5d82bbc508f..47376901ed6 100644 --- a/apps/ios/Tests/SwiftUIRenderSmokeTests.swift +++ b/apps/ios/Tests/SwiftUIRenderSmokeTests.swift @@ -129,6 +129,73 @@ struct SwiftUIRenderSmokeTests { } } + @Test @MainActor func gatewayQuickSetupBuildsCandidateAndEmptyStates() { + let gateways: [GatewayDiscoveryModel.DiscoveredGateway?] = [ + .previewGateway, + nil, + ] + + for gateway in gateways { + let appModel = NodeAppModel() + let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false) + if let gateway { + gatewayController._test_setGateways([gateway]) + appModel.gatewayStatusText = "Ready to pair" + } + + let root = GatewayQuickSetupSheet() + .environment(appModel) + .environment(gatewayController) + .openClawSheetChrome() + + _ = Self.host(root, size: CGSize(width: 393, height: 520)) + } + } + + @Test @MainActor func onboardingActivationScreensBuildAcrossAppearanceAndTypeSize() { + let screens: [AnyView] = [ + AnyView(OnboardingIntroStep(onContinue: {})), + AnyView(OnboardingWelcomeStep( + statusLine: "", + isConnecting: false, + onScanQRCode: {}, + onManualSetup: {})), + AnyView(OnboardingSuccessStep( + gatewayName: "OpenClaw Gateway", + gatewayAddress: "openclaw.local", + onGetStarted: {})), + AnyView(NavigationStack { + Form { + Section("Connection Mode") { + OnboardingModeRow( + title: "Home Network", + subtitle: "LAN or Tailscale host", + symbol: "house.and.flag", + selected: true, + action: {}) + OnboardingModeRow( + title: "Remote Domain", + subtitle: "VPS with domain", + symbol: "globe", + selected: false, + action: {}) + } + } + .scrollContentBackground(.hidden) + .background(OpenClawBrand.activationCanvas) + }), + ] + + for scheme in [ColorScheme.light, ColorScheme.dark] { + for screen in screens { + let root = screen + .preferredColorScheme(scheme) + .environment(\.dynamicTypeSize, .accessibility2) + _ = Self.host(root, size: CGSize(width: 393, height: 852)) + } + } + } + @Test @MainActor func `root tabs build gateway state view hierarchies`() { for appModel in Self.rootTabsGatewayStateModels() { let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false) @@ -347,3 +414,20 @@ struct SwiftUIRenderSmokeTests { let verticalSizeClass: UserInterfaceSizeClass } } + +extension GatewayDiscoveryModel.DiscoveredGateway { + fileprivate static let previewGateway = GatewayDiscoveryModel.DiscoveredGateway( + name: "Studio Gateway", + endpoint: .hostPort( + host: .name("openclaw.local", nil), + port: 18789), + stableID: "preview-gateway", + debugID: "openclaw.local", + lanHost: "openclaw.local", + tailnetDns: nil, + gatewayPort: 18789, + canvasPort: 18789, + tlsEnabled: true, + tlsFingerprintSha256: "preview", + cliPath: "/opt/homebrew/bin/openclaw") +} diff --git a/apps/ios/UITests/OpenClawSnapshotUITests.swift b/apps/ios/UITests/OpenClawSnapshotUITests.swift index 6c0778f226c..0764f3c6f55 100644 --- a/apps/ios/UITests/OpenClawSnapshotUITests.swift +++ b/apps/ios/UITests/OpenClawSnapshotUITests.swift @@ -45,7 +45,7 @@ final class OpenClawSnapshotUITests: XCTestCase { self.app = app XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8)) - XCTAssertTrue(app.staticTexts["Security"].exists) + XCTAssertTrue(app.staticTexts["Security notice"].exists) let disclosure = app.staticTexts.matching(NSPredicate( format: "label CONTAINS[c] 'camera' AND label CONTAINS[c] 'trust the gateway and agent'")).firstMatch XCTAssertTrue(disclosure.exists) @@ -337,7 +337,7 @@ final class OpenClawSnapshotUITests: XCTestCase { starter.tap() XCTAssertTrue( self.app?.staticTexts[ - "Fasse den aktuellen OpenClaw-Status zusammen und sage mir, was Aufmerksamkeit erfordert." + "Fasse den aktuellen OpenClaw-Status zusammen und sage mir, was Aufmerksamkeit erfordert.", ].waitForExistence(timeout: 5) == true) self.attachScreenshot(named: "chat-empty-starters-german") } @@ -366,23 +366,23 @@ final class OpenClawSnapshotUITests: XCTestCase { app.buttons["Continue"].tap() app.tap() - let copyPairCommand = app.buttons["onboarding-copy-pair-command"] - XCTAssertTrue(copyPairCommand.waitForExistence(timeout: 8)) - copyPairCommand.tap() - XCTAssertEqual(copyPairCommand.label, "Pair command copied") - self.attachScreenshot(named: "onboarding-copy-pair-command") + let copySetupCommand = app.buttons["Copy setup code command"] + XCTAssertTrue(copySetupCommand.waitForExistence(timeout: 8)) + copySetupCommand.tap() + XCTAssertEqual(copySetupCommand.value as? String, "Copied") + self.attachScreenshot(named: "onboarding-copy-setup-code-command") - app.buttons["Set Up Manually"].tap() - let setupCode = app.textFields["Paste setup code"] + app.buttons["Connect Manually"].tap() + let setupCode = app.textFields["Enter setup code"] XCTAssertTrue(setupCode.waitForExistence(timeout: 5)) setupCode.tap() setupCode.typeText("APPLE-REVIEW-DEMO") - app.buttons["Done"].tap() - app.buttons["Apply Setup Code"].tap() + app.buttons["Dismiss Keyboard"].tap() + app.buttons["Apply"].tap() - XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 8)) + XCTAssertTrue(app.staticTexts["You're connected"].waitForExistence(timeout: 8)) XCTAssertTrue(app.staticTexts["Apple Review Demo Gateway"].exists) - XCTAssertFalse(app.staticTexts["Local demo mode"].exists) + XCTAssertTrue(app.staticTexts["Local demo mode"].exists) self.attachScreenshot(named: "onboarding-connected-go-to-chat") app.buttons["Go to Chat"].tap() @@ -622,29 +622,35 @@ final class OpenClawSnapshotUITests: XCTestCase { XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8)) app.buttons["Continue"].tap() app.tap() - XCTAssertTrue(app.buttons["Set Up Manually"].waitForExistence(timeout: 8)) - app.buttons["Set Up Manually"].tap() - let developerMode = app.buttons["Developer mode"] - if developerMode.value as? String != "On" { - developerMode.tap() - } - app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", "Same Machine (Dev)")).firstMatch.tap() + XCTAssertTrue(app.buttons["Connect Manually"].waitForExistence(timeout: 8)) + app.buttons["Connect Manually"].tap() + app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", "Home Network")).firstMatch.tap() app.buttons["Continue"].tap() + let host = app.textFields["Host"] + XCTAssertTrue(host.waitForExistence(timeout: 5)) + host.tap() + host.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 32) + "localhost") + let port = app.textFields["Port"] XCTAssertTrue(port.waitForExistence(timeout: 5)) port.tap() port.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 5) + "18920") + let useTLS = app.switches["Use TLS"] + XCTAssertTrue(useTLS.waitForExistence(timeout: 5)) + if useTLS.value as? String != "0" { + useTLS.tap() + } app.buttons["Connect"].tap() let tokenField = app.secureTextFields["Gateway Auth Token"] XCTAssertTrue(tokenField.waitForExistence(timeout: 20)) tokenField.tap() tokenField.typeText(token) - app.buttons["Done"].tap() + app.buttons["Dismiss Keyboard"].tap() app.buttons["Retry Connection"].tap() - XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 30)) + XCTAssertTrue(app.staticTexts["You're connected"].waitForExistence(timeout: 30)) self.attachScreenshot(named: "manual-auth-retry-connected") } @@ -770,19 +776,18 @@ final class OpenClawSnapshotUITests: XCTestCase { XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8)) app.buttons["Continue"].tap() app.tap() - XCTAssertTrue(app.buttons["Set Up Manually"].waitForExistence(timeout: 8)) - app.buttons["Set Up Manually"].tap() + XCTAssertTrue(app.buttons["Connect Manually"].waitForExistence(timeout: 8)) + app.buttons["Connect Manually"].tap() - let setupCodeField = app.textFields["Paste setup code"] + let setupCodeField = app.textFields["Enter setup code"] XCTAssertTrue(setupCodeField.waitForExistence(timeout: 5)) setupCodeField.tap() setupCodeField.press(forDuration: 1) XCTAssertTrue(app.menuItems["Paste"].waitForExistence(timeout: 3)) app.menuItems["Paste"].tap() - app.buttons["Done"].tap() - app.buttons["Apply Setup Code"].tap() + app.buttons["Apply"].tap() - XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 45)) + XCTAssertTrue(app.staticTexts["You're connected"].waitForExistence(timeout: 45)) app.buttons["Go to Chat"].tap() return app } diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift index 62d4da993a5..0edade2eae3 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift @@ -9,7 +9,11 @@ public struct OpenClawMascotView: View { @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.colorScheme) private var colorScheme - public init() {} + private let floats: Bool + + public init(floats: Bool = true) { + self.floats = floats + } public var body: some View { let palette = OpenClawMascotPalette.forScheme(self.colorScheme) @@ -23,7 +27,10 @@ public struct OpenClawMascotView: View { // antennae (art starts at y~5 of 120) at the -9.6 float peak. GeometryReader { proxy in OpenClawMascotCanvas(pose: pose, palette: palette) - .offset(y: pose.floatOffset * min(proxy.size.width, proxy.size.height) / 120) + .offset( + y: self.floats + ? pose.floatOffset * min(proxy.size.width, proxy.size.height) / 120 + : 0) } } } diff --git a/scripts/apple-app-i18n.ts b/scripts/apple-app-i18n.ts index c7f941c9706..ef005946ba3 100644 --- a/scripts/apple-app-i18n.ts +++ b/scripts/apple-app-i18n.ts @@ -55,10 +55,10 @@ const CATALOGS = [ "apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift": [ "Close", "Connect", - "Connect to a Gateway?", + "Connect a nearby Gateway", "Connecting…", - "Don’t show this again", - "No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.", + "Don't show this again", + "Looking for a Gateway", "Not now", "Quick Setup", ], @@ -68,7 +68,7 @@ const CATALOGS = [ "Trust and connect", "Trust this gateway?", ], - "apps/ios/Sources/Onboarding/OnboardingWizardView.swift": ["Save"], + "apps/ios/Sources/Onboarding/OnboardingWizardSteps.swift": ["Go to Chat"], "apps/ios/Sources/RootTabs.swift": ["Agent", "Chat", "Control", "Settings", "Talk"], "apps/ios/WatchApp/Sources/WatchInboxView.swift": [ "Approve", @@ -168,7 +168,7 @@ export async function checkAppleAppI18n() { for (const locale of REQUIRED_LOCALES) { const unit = entry.localizations?.[locale]?.stringUnit; const value = unit?.value?.trim(); - if (!value || unit?.state !== "translated") { + if (!value || (locale === "en" && unit?.state !== "translated")) { throw new Error( `Apple catalog ${spec.path} is missing ${locale} for ${JSON.stringify(key)}`, ); diff --git a/test/scripts/apple-app-i18n.test.ts b/test/scripts/apple-app-i18n.test.ts index def7c884d58..ecc1e40c6cd 100644 --- a/test/scripts/apple-app-i18n.test.ts +++ b/test/scripts/apple-app-i18n.test.ts @@ -9,6 +9,24 @@ describe("Apple app i18n catalogs", () => { await expect(checkAppleAppI18n()).resolves.toBeUndefined(); }); + it("does not mark English setup fallbacks as completed translations", async () => { + const catalog = JSON.parse( + await readFile("apps/ios/Resources/Localizable.xcstrings", "utf8"), + ) as { + strings: Record< + string, + { localizations?: Record } + >; + }; + + expect( + catalog.strings["Connect a nearby Gateway"]?.localizations?.de?.stringUnit, + ).toMatchObject({ + state: "new", + value: "Connect a nearby Gateway", + }); + }); + it("compiles macOS catalogs into app-bundle localization directories", async () => { const outputDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-apple-i18n-")); try { diff --git a/test/scripts/native-app-i18n.test.ts b/test/scripts/native-app-i18n.test.ts index 54d7d436d68..82679124aea 100644 --- a/test/scripts/native-app-i18n.test.ts +++ b/test/scripts/native-app-i18n.test.ts @@ -120,7 +120,7 @@ describe("native app i18n inventory", () => { ), ).toBe(true); expect(entries.some((entry) => entry.source === "No sessions yet")).toBe(true); - expect(entries.some((entry) => entry.source === "Don’t show this again")).toBe(true); + expect(entries.some((entry) => entry.source === "Don't show this again")).toBe(true); expect(entries.some((entry) => entry.source === "Use Manual Gateway")).toBe(true); expect(entries.some((entry) => entry.source === "Session target")).toBe(true); expect(