From 6438c89f052cdc8a32ac5c7679c87973dcafa55f Mon Sep 17 00:00:00 2001 From: Josh Avant <830519+joshavant@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:43:11 -0500 Subject: [PATCH] fix(mobile): clarify gateway connection security setup (#101325) * fix(mobile): clarify gateway connection setup * chore(i18n): sync native mobile strings * fix(mobile): align manual gateway previews * chore(i18n): refresh native source inventory * chore(android): remove unused connect tab strings * test(i18n): drop removed connect tab sentinel * fix(ios): brand connection security picker labels * fix(mobile): hide gateway endpoint previews * chore(i18n): sync native source inventory * fix(mobile): remove redundant TLS helper copy --- apps/.i18n/native-source.json | 550 ++++++++++-------- .../main/java/ai/openclaw/app/NodeRuntime.kt | 2 +- .../openclaw/app/ui/GatewayConfigResolver.kt | 54 ++ .../java/ai/openclaw/app/ui/OnboardingFlow.kt | 74 ++- .../ai/openclaw/app/ui/SettingsScreens.kt | 30 +- .../openclaw/app/ui/design/ClawComponents.kt | 11 +- .../openclaw/app/GatewayBootstrapAuthTest.kt | 2 +- .../app/ui/GatewayConfigResolverTest.kt | 39 ++ .../app/ui/OnboardingFlowLogicTest.kt | 17 + .../Design/SettingsProTabSections.swift | 31 +- .../Gateway/GatewayConnectionController.swift | 33 +- .../Gateway/GatewayProblemPrimaryAction.swift | 6 +- .../Gateway/GatewayQuickSetupSheet.swift | 2 +- .../Onboarding/OnboardingWizardView.swift | 41 +- apps/ios/Sources/RootTabs.swift | 2 +- .../GatewayConnectionSecurityTests.swift | 34 +- .../GatewayProblemPrimaryActionTests.swift | 17 + apps/ios/Tests/OpenClawTypographyTests.swift | 28 + apps/ios/Tests/RootTabsSourceGuardTests.swift | 2 +- .../ios/UITests/OpenClawSnapshotUITests.swift | 8 +- .../GatewayConnectionProblem.swift | 8 +- .../OpenClawKitTests/GatewayErrorsTests.swift | 2 + 22 files changed, 703 insertions(+), 290 deletions(-) diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index a50477f46e2..d34ee357025 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -205,9 +205,9 @@ "kind": "conditional-branch", "line": 2714, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", - "source": "Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected.", + "source": "Failed: no secure gateway endpoint was detected. Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address with Unencrypted selected.", "surface": "android", - "id": "native.android.81009c40eed2216d" + "id": "native.android.4b07456682e725d9" }, { "kind": "conditional-branch", @@ -1147,7 +1147,7 @@ }, { "kind": "conditional-branch", - "line": 299, + "line": 306, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "Setup code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix", "surface": "android", @@ -1155,7 +1155,7 @@ }, { "kind": "conditional-branch", - "line": 301, + "line": 308, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "QR code points to an insecure remote gateway. $remoteGatewaySecurityRule $remoteGatewaySecurityFix", "surface": "android", @@ -1163,7 +1163,7 @@ }, { "kind": "conditional-branch", - "line": 303, + "line": 310, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "$remoteGatewaySecurityRule $remoteGatewaySecurityFix", "surface": "android", @@ -1171,7 +1171,7 @@ }, { "kind": "conditional-branch", - "line": 308, + "line": 315, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "Setup code uses an IPv6 zone ID. Use an unscoped IPv6 address or a LAN hostname.", "surface": "android", @@ -1179,7 +1179,7 @@ }, { "kind": "conditional-branch", - "line": 310, + "line": 317, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "QR code uses an IPv6 zone ID. Use an unscoped IPv6 address or a LAN hostname.", "surface": "android", @@ -1187,7 +1187,7 @@ }, { "kind": "conditional-branch", - "line": 312, + "line": 319, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "IPv6 zone IDs are not supported. Use an unscoped IPv6 address or a LAN hostname.", "surface": "android", @@ -1195,7 +1195,7 @@ }, { "kind": "conditional-branch", - "line": 317, + "line": 324, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "Setup code has invalid gateway URL.", "surface": "android", @@ -1203,7 +1203,7 @@ }, { "kind": "conditional-branch", - "line": 318, + "line": 325, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "QR code did not contain a valid setup code.", "surface": "android", @@ -1211,7 +1211,7 @@ }, { "kind": "conditional-branch", - "line": 319, + "line": 326, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt", "source": "Enter a valid manual endpoint to connect.", "surface": "android", @@ -1667,7 +1667,7 @@ }, { "kind": "ui-call", - "line": 693, + "line": 698, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Trust", "surface": "android", @@ -1675,7 +1675,7 @@ }, { "kind": "ui-call", - "line": 698, + "line": 703, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Cancel", "surface": "android", @@ -1683,7 +1683,7 @@ }, { "kind": "ui-named-argument", - "line": 889, + "line": 894, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Welcome to OpenClaw", "surface": "android", @@ -1691,7 +1691,7 @@ }, { "kind": "ui-named-argument", - "line": 890, + "line": 895, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Turn this device into a secure OpenClaw node for chat, voice, camera, and device tools.", "surface": "android", @@ -1699,7 +1699,7 @@ }, { "kind": "ui-named-argument", - "line": 915, + "line": 920, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "OpenClaw logo", "surface": "android", @@ -1707,7 +1707,7 @@ }, { "kind": "ui-named-argument", - "line": 971, + "line": 976, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Connect to your Gateway", "surface": "android", @@ -1715,7 +1715,7 @@ }, { "kind": "ui-named-argument", - "line": 972, + "line": 977, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Choose device permissions", "surface": "android", @@ -1723,7 +1723,7 @@ }, { "kind": "ui-named-argument", - "line": 973, + "line": 978, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Use OpenClaw from your phone", "surface": "android", @@ -1731,7 +1731,7 @@ }, { "kind": "ui-named-argument", - "line": 995, + "line": 1000, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Security notice", "surface": "android", @@ -1739,7 +1739,7 @@ }, { "kind": "ui-named-argument", - "line": 997, + "line": 1002, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "The connected OpenClaw agent can use device capabilities you enable. Continue only if you trust the Gateway and agent you connect to.", "surface": "android", @@ -1747,7 +1747,7 @@ }, { "kind": "ui-named-argument", - "line": 1038, + "line": 1043, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Connect Gateway", "surface": "android", @@ -1755,7 +1755,7 @@ }, { "kind": "ui-named-argument", - "line": 1039, + "line": 1044, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Scan a QR code or use the setup code from your OpenClaw Gateway.", "surface": "android", @@ -1763,7 +1763,7 @@ }, { "kind": "ui-toast", - "line": 1048, + "line": 1053, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Could not open setup guide.", "surface": "android", @@ -1771,7 +1771,7 @@ }, { "kind": "ui-named-argument", - "line": 1056, + "line": 1061, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Scan QR or setup code", "surface": "android", @@ -1779,7 +1779,7 @@ }, { "kind": "ui-named-argument", - "line": 1062, + "line": 1067, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Set up manually", "surface": "android", @@ -1787,7 +1787,7 @@ }, { "kind": "ui-named-argument", - "line": 1084, + "line": 1089, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Before you start", "surface": "android", @@ -1795,7 +1795,7 @@ }, { "kind": "ui-named-argument", - "line": 1090, + "line": 1095, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Access to the Gateway device", "surface": "android", @@ -1803,7 +1803,7 @@ }, { "kind": "ui-named-argument", - "line": 1091, + "line": 1096, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Have a terminal open on the device running OpenClaw.", "surface": "android", @@ -1811,7 +1811,7 @@ }, { "kind": "ui-named-argument", - "line": 1094, + "line": 1099, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Phone can reach the Gateway", "surface": "android", @@ -1819,7 +1819,7 @@ }, { "kind": "ui-named-argument", - "line": 1095, + "line": 1100, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Use the same network, or a secure remote Gateway URL.", "surface": "android", @@ -1827,7 +1827,7 @@ }, { "kind": "ui-named-argument", - "line": 1101, + "line": 1106, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Android setup guide", "surface": "android", @@ -1835,7 +1835,7 @@ }, { "kind": "ui-named-argument", - "line": 1144, + "line": 1149, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Setup Gateway", "surface": "android", @@ -1843,7 +1843,7 @@ }, { "kind": "ui-named-argument", - "line": 1150, + "line": 1155, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Start your Gateway.", "surface": "android", @@ -1851,7 +1851,7 @@ }, { "kind": "ui-named-argument", - "line": 1151, + "line": 1156, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "openclaw gateway", "surface": "android", @@ -1859,7 +1859,7 @@ }, { "kind": "ui-named-argument", - "line": 1156, + "line": 1161, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Generate a QR code.", "surface": "android", @@ -1867,7 +1867,7 @@ }, { "kind": "ui-named-argument", - "line": 1157, + "line": 1162, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "openclaw qr", "surface": "android", @@ -1875,7 +1875,7 @@ }, { "kind": "ui-named-argument", - "line": 1177, + "line": 1182, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Choose from gallery", "surface": "android", @@ -1883,7 +1883,7 @@ }, { "kind": "ui-named-argument", - "line": 1231, + "line": 1236, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "QR code not accepted", "surface": "android", @@ -1891,7 +1891,7 @@ }, { "kind": "ui-named-argument", - "line": 1246, + "line": 1251, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Choose another image", "surface": "android", @@ -1899,7 +1899,7 @@ }, { "kind": "ui-named-argument", - "line": 1299, + "line": 1304, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Scan QR code", "surface": "android", @@ -1907,7 +1907,7 @@ }, { "kind": "ui-named-argument", - "line": 1301, + "line": 1306, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Open the camera and frame the code from openclaw qr.", "surface": "android", @@ -1915,7 +1915,7 @@ }, { "kind": "ui-named-argument", - "line": 1335, + "line": 1340, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Align the QR code inside the square.", "surface": "android", @@ -1923,7 +1923,7 @@ }, { "kind": "ui-named-argument", - "line": 1353, + "line": 1358, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Camera access is needed to scan the setup QR.", "surface": "android", @@ -1931,7 +1931,7 @@ }, { "kind": "ui-named-argument", - "line": 1358, + "line": 1363, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Allow camera", "surface": "android", @@ -1939,7 +1939,7 @@ }, { "kind": "ui-named-argument", - "line": 1379, + "line": 1384, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Close scanner", "surface": "android", @@ -1947,7 +1947,7 @@ }, { "kind": "ui-named-argument", - "line": 1541, + "line": 1546, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Enter setup code", "surface": "android", @@ -1955,7 +1955,7 @@ }, { "kind": "ui-named-argument", - "line": 1542, + "line": 1547, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Setup code", "surface": "android", @@ -1963,7 +1963,7 @@ }, { "kind": "ui-named-argument", - "line": 1546, + "line": 1551, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Paste setup code", "surface": "android", @@ -1971,7 +1971,7 @@ }, { "kind": "ui-named-argument", - "line": 1550, + "line": 1555, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Setup code was not accepted", "surface": "android", @@ -1979,7 +1979,7 @@ }, { "kind": "ui-named-argument", - "line": 1554, + "line": 1559, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Use setup code", "surface": "android", @@ -1987,7 +1987,7 @@ }, { "kind": "ui-named-argument", - "line": 1585, + "line": 1597, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Manual setup", "surface": "android", @@ -1995,7 +1995,7 @@ }, { "kind": "ui-named-argument", - "line": 1588, + "line": 1600, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway URL", "surface": "android", @@ -2003,7 +2003,7 @@ }, { "kind": "ui-named-argument", - "line": 1590, + "line": 1602, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Host", "surface": "android", @@ -2011,7 +2011,7 @@ }, { "kind": "ui-named-argument", - "line": 1591, + "line": 1603, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Port", "surface": "android", @@ -2019,7 +2019,7 @@ }, { "kind": "ui-named-argument", - "line": 1594, + "line": 1606, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Use the Gateway computer's LAN address or secure remote hostname.", "surface": "android", @@ -2027,7 +2027,7 @@ }, { "kind": "ui-named-argument", - "line": 1601, + "line": 1613, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Token", "surface": "android", @@ -2035,7 +2035,7 @@ }, { "kind": "ui-named-argument", - "line": 1602, + "line": 1614, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Paste token", "surface": "android", @@ -2043,7 +2043,7 @@ }, { "kind": "ui-named-argument", - "line": 1604, + "line": 1616, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Paste a shared Gateway token or operator-issued token.", "surface": "android", @@ -2051,7 +2051,7 @@ }, { "kind": "ui-named-argument", - "line": 1611, + "line": 1623, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Password", "surface": "android", @@ -2059,7 +2059,7 @@ }, { "kind": "ui-named-argument", - "line": 1612, + "line": 1624, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Password optional", "surface": "android", @@ -2067,39 +2067,31 @@ }, { "kind": "ui-named-argument", - "line": 1616, + "line": 1628, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", - "source": "Connection type", + "source": "Connection security", "surface": "android", - "id": "native.android.1259d0d0d1671ed7" + "id": "native.android.0b90d2ae01fe2adc" }, { "kind": "ui-named-argument", - "line": 1618, + "line": 1631, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", - "source": "Local network", + "source": "Unencrypted", "surface": "android", - "id": "native.android.278d9abb3abc12e6" + "id": "native.android.475b12d58426a78b" }, { "kind": "ui-named-argument", - "line": 1619, + "line": 1637, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", - "source": "Secure remote", + "source": "Secure (TLS)", "surface": "android", - "id": "native.android.4aa2b8be11a971eb" + "id": "native.android.60133981279752d2" }, { "kind": "ui-named-argument", - "line": 1622, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", - "source": "Local works for LAN or emulator hosts. Secure remote is for wss:// or Tailscale Serve/Funnel.", - "surface": "android", - "id": "native.android.8c31e2762b496d61" - }, - { - "kind": "ui-named-argument", - "line": 1630, + "line": 1653, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Could not test connection", "surface": "android", @@ -2107,7 +2099,7 @@ }, { "kind": "ui-named-argument", - "line": 1635, + "line": 1658, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Test connection", "surface": "android", @@ -2115,7 +2107,7 @@ }, { "kind": "ui-call", - "line": 1813, + "line": 1847, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "View details", "surface": "android", @@ -2123,7 +2115,7 @@ }, { "kind": "ui-call", - "line": 1846, + "line": 1880, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Connection details", "surface": "android", @@ -2131,7 +2123,7 @@ }, { "kind": "ui-call", - "line": 1858, + "line": 1892, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Copy", "surface": "android", @@ -2139,7 +2131,7 @@ }, { "kind": "ui-call", - "line": 1863, + "line": 1897, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Close", "surface": "android", @@ -2147,7 +2139,7 @@ }, { "kind": "ui-toast", - "line": 1875, + "line": 1909, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Details copied", "surface": "android", @@ -2155,7 +2147,7 @@ }, { "kind": "ui-named-argument", - "line": 1960, + "line": 1994, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Approve node access", "surface": "android", @@ -2163,7 +2155,7 @@ }, { "kind": "ui-named-argument", - "line": 1967, + "line": 2001, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway pairing is complete. Approve this phone as a node so OpenClaw can use the device capabilities you enable.", "surface": "android", @@ -2171,7 +2163,7 @@ }, { "kind": "ui-named-argument", - "line": 1975, + "line": 2009, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "On the Gateway computer, run:", "surface": "android", @@ -2179,7 +2171,7 @@ }, { "kind": "ui-named-argument", - "line": 1984, + "line": 2018, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Use the requestId from the pending command in the approve command.", "surface": "android", @@ -2187,7 +2179,7 @@ }, { "kind": "ui-named-argument", - "line": 1995, + "line": 2029, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "I have approved", "surface": "android", @@ -2195,7 +2187,7 @@ }, { "kind": "ui-named-argument", - "line": 1996, + "line": 2030, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Checking approval…", "surface": "android", @@ -2203,7 +2195,7 @@ }, { "kind": "ui-named-argument", - "line": 2008, + "line": 2042, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Still waiting for approval", "surface": "android", @@ -2211,7 +2203,7 @@ }, { "kind": "ui-named-argument", - "line": 2011, + "line": 2045, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Run the approve command on the Gateway computer, then check again.", "surface": "android", @@ -2219,7 +2211,7 @@ }, { "kind": "ui-named-argument", - "line": 2018, + "line": 2052, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "OK", "surface": "android", @@ -2227,7 +2219,7 @@ }, { "kind": "ui-named-argument", - "line": 2168, + "line": 2202, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Copy approval command", "surface": "android", @@ -2235,7 +2227,7 @@ }, { "kind": "ui-named-argument", - "line": 2194, + "line": 2228, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Only enable access you are comfortable letting OpenClaw use while this phone is connected. You can change these later in Android Settings.", "surface": "android", @@ -2243,7 +2235,7 @@ }, { "kind": "ui-named-argument", - "line": 2206, + "line": 2240, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Continue", "surface": "android", @@ -2251,7 +2243,7 @@ }, { "kind": "ui-named-argument", - "line": 2233, + "line": 2267, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Back", "surface": "android", @@ -2259,7 +2251,7 @@ }, { "kind": "ui-named-argument", - "line": 2280, + "line": 2316, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Permissions", "surface": "android", @@ -2267,7 +2259,7 @@ }, { "kind": "conditional-branch", - "line": 2605, + "line": 2641, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "The code may have expired or been generated for another Gateway.", "surface": "android", @@ -2275,7 +2267,7 @@ }, { "kind": "conditional-branch", - "line": 2609, + "line": 2645, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway password is required. Enter it again or edit this connection.", "surface": "android", @@ -2283,7 +2275,7 @@ }, { "kind": "conditional-branch", - "line": 2610, + "line": 2646, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway password is invalid. Re-enter it or reset this gateway connection.", "surface": "android", @@ -2291,7 +2283,7 @@ }, { "kind": "conditional-branch", - "line": 2611, + "line": 2647, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway token is required. Enter it again or edit this connection.", "surface": "android", @@ -2299,7 +2291,7 @@ }, { "kind": "conditional-branch", - "line": 2613, + "line": 2649, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway requires this device identity. Re-authenticate or reset this gateway connection.", "surface": "android", @@ -2307,7 +2299,7 @@ }, { "kind": "conditional-branch", - "line": 2617, + "line": 2653, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Saved authentication is invalid. Re-authenticate or reset this gateway connection.", "surface": "android", @@ -2315,7 +2307,7 @@ }, { "kind": "conditional-branch", - "line": 2618, + "line": 2654, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway authentication is not configured. Edit this connection and try again.", "surface": "android", @@ -2323,7 +2315,7 @@ }, { "kind": "conditional-branch", - "line": 2619, + "line": 2655, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Gateway authentication needs review. Check gateway settings, then retry.", "surface": "android", @@ -2331,7 +2323,7 @@ }, { "kind": "conditional-branch", - "line": 2636, + "line": 2672, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "$summary $it", "surface": "android", @@ -2339,15 +2331,23 @@ }, { "kind": "ui-toast", - "line": 2758, + "line": 2803, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Approval command copied", "surface": "android", "id": "native.android.a7933196a18ec924" }, + { + "kind": "ui-toast", + "line": 2812, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Command copied", + "surface": "android", + "id": "native.android.32690c5e41e8a4cb" + }, { "kind": "conditional-branch", - "line": 2820, + "line": 2874, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Granted", "surface": "android", @@ -2355,7 +2355,7 @@ }, { "kind": "conditional-branch", - "line": 2820, + "line": 2874, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", "source": "Not granted", "surface": "android", @@ -3443,7 +3443,7 @@ }, { "kind": "ui-call", - "line": 1168, + "line": 1175, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Replace gateway setup?", "surface": "android", @@ -3451,7 +3451,7 @@ }, { "kind": "ui-call", - "line": 1183, + "line": 1190, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Replace setup", "surface": "android", @@ -3459,7 +3459,7 @@ }, { "kind": "ui-call", - "line": 1199, + "line": 1206, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Forget gateway?", "surface": "android", @@ -3467,7 +3467,7 @@ }, { "kind": "ui-call", - "line": 1212, + "line": 1219, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Cancel", "surface": "android", @@ -3475,7 +3475,7 @@ }, { "kind": "ui-named-argument", - "line": 1218, + "line": 1225, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Connection between this phone and OpenClaw.", "surface": "android", @@ -3483,7 +3483,7 @@ }, { "kind": "conditional-branch", - "line": 1222, + "line": 1229, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Connected", "surface": "android", @@ -3491,7 +3491,7 @@ }, { "kind": "conditional-branch", - "line": 1222, + "line": 1229, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Offline", "surface": "android", @@ -3499,7 +3499,7 @@ }, { "kind": "conditional-branch", - "line": 1223, + "line": 1230, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Not paired", "surface": "android", @@ -3507,7 +3507,7 @@ }, { "kind": "conditional-branch", - "line": 1223, + "line": 1230, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Online", "surface": "android", @@ -3515,7 +3515,7 @@ }, { "kind": "ui-named-argument", - "line": 1233, + "line": 1240, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Reconnect", "surface": "android", @@ -3523,7 +3523,7 @@ }, { "kind": "ui-named-argument", - "line": 1234, + "line": 1241, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Disconnect", "surface": "android", @@ -3531,7 +3531,7 @@ }, { "kind": "ui-named-argument", - "line": 1238, + "line": 1245, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Gateways", "surface": "android", @@ -3539,7 +3539,7 @@ }, { "kind": "ui-named-argument", - "line": 1240, + "line": 1247, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "No paired gateways.", "surface": "android", @@ -3547,7 +3547,7 @@ }, { "kind": "ui-call", - "line": 1260, + "line": 1267, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Forget", "surface": "android", @@ -3555,7 +3555,7 @@ }, { "kind": "ui-named-argument", - "line": 1282, + "line": 1289, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Gateway setup", "surface": "android", @@ -3563,7 +3563,7 @@ }, { "kind": "ui-named-argument", - "line": 1284, + "line": 1291, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Scan or paste a setup code to add another gateway.", "surface": "android", @@ -3571,7 +3571,7 @@ }, { "kind": "ui-named-argument", - "line": 1291, + "line": 1298, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Add gateway", "surface": "android", @@ -3579,7 +3579,7 @@ }, { "kind": "ui-named-argument", - "line": 1292, + "line": 1299, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Setup Code", "surface": "android", @@ -3587,7 +3587,7 @@ }, { "kind": "ui-named-argument", - "line": 1296, + "line": 1303, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Android can scan or paste an existing setup code, but this gateway does not expose setup-code generation to the app yet. Generate the QR/code on the gateway host with openclaw qr, then scan it here or paste the setup code below.", "surface": "android", @@ -3595,7 +3595,7 @@ }, { "kind": "ui-named-argument", - "line": 1305, + "line": 1312, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Connection Setup", "surface": "android", @@ -3603,7 +3603,7 @@ }, { "kind": "ui-named-argument", - "line": 1306, + "line": 1313, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Setup code", "surface": "android", @@ -3611,7 +3611,7 @@ }, { "kind": "ui-named-argument", - "line": 1308, + "line": 1315, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Host", "surface": "android", @@ -3619,23 +3619,39 @@ }, { "kind": "ui-named-argument", - "line": 1309, + "line": 1316, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Port", "surface": "android", "id": "native.android.066ae1c70e6db5c1" }, { - "kind": "conditional-branch", - "line": 1313, + "kind": "ui-named-argument", + "line": 1318, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", - "source": "Local", + "source": "Connection security", "surface": "android", - "id": "native.android.74e3d2481d44c4a1" + "id": "native.android.16e2f44030be076a" + }, + { + "kind": "conditional-branch", + "line": 1322, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", + "source": "Secure (TLS)", + "surface": "android", + "id": "native.android.6701e8520a27c448" + }, + { + "kind": "conditional-branch", + "line": 1322, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", + "source": "Unencrypted", + "surface": "android", + "id": "native.android.86aeb5011ef180cb" }, { "kind": "ui-named-argument", - "line": 1317, + "line": 1339, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Token", "surface": "android", @@ -3643,7 +3659,7 @@ }, { "kind": "ui-named-argument", - "line": 1318, + "line": 1340, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Bootstrap", "surface": "android", @@ -3651,7 +3667,7 @@ }, { "kind": "ui-named-argument", - "line": 1320, + "line": 1342, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Password", "surface": "android", @@ -3659,7 +3675,7 @@ }, { "kind": "ui-named-argument", - "line": 1325, + "line": 1347, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Save & Connect", "surface": "android", @@ -3667,7 +3683,7 @@ }, { "kind": "ui-named-argument", - "line": 1342, + "line": 1364, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Enter a valid setup code or gateway address.", "surface": "android", @@ -3675,7 +3691,7 @@ }, { "kind": "ui-named-argument", - "line": 1365, + "line": 1387, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "A calm, high-contrast OpenClaw interface.", "surface": "android", @@ -3683,7 +3699,7 @@ }, { "kind": "ui-named-argument", - "line": 1365, + "line": 1387, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Appearance", "surface": "android", @@ -3691,7 +3707,7 @@ }, { "kind": "ui-named-argument", - "line": 1376, + "line": 1398, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Theme", "surface": "android", @@ -3699,7 +3715,7 @@ }, { "kind": "conditional-branch", - "line": 1421, + "line": 1443, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Ready", "surface": "android", @@ -3707,7 +3723,7 @@ }, { "kind": "ui-named-argument", - "line": 1450, + "line": 1472, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "About", "surface": "android", @@ -3715,7 +3731,7 @@ }, { "kind": "ui-named-argument", - "line": 1450, + "line": 1472, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "OpenClaw for Android.", "surface": "android", @@ -3723,7 +3739,7 @@ }, { "kind": "ui-named-argument", - "line": 1463, + "line": 1485, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Gateway", "surface": "android", @@ -3731,7 +3747,7 @@ }, { "kind": "ui-named-argument", - "line": 1465, + "line": 1487, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Runtime", "surface": "android", @@ -3739,7 +3755,7 @@ }, { "kind": "ui-named-argument", - "line": 1468, + "line": 1490, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Update", "surface": "android", @@ -3747,7 +3763,7 @@ }, { "kind": "ui-named-argument", - "line": 1479, + "line": 1501, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "© 2026 OpenClaw Foundation — MIT License.", "surface": "android", @@ -3755,7 +3771,7 @@ }, { "kind": "ui-named-argument", - "line": 1496, + "line": 1518, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "OpenClaw logo", "surface": "android", @@ -3763,7 +3779,7 @@ }, { "kind": "ui-named-argument", - "line": 1498, + "line": 1520, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "OpenClaw", "surface": "android", @@ -3771,7 +3787,7 @@ }, { "kind": "ui-named-argument", - "line": 1499, + "line": 1521, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Personal AI on your devices", "surface": "android", @@ -3779,7 +3795,7 @@ }, { "kind": "ui-named-argument", - "line": 1558, + "line": 1580, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Licenses", "surface": "android", @@ -3787,7 +3803,7 @@ }, { "kind": "conditional-branch", - "line": 1559, + "line": 1581, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "OpenClaw appreciates its partners in the open-source community.", "surface": "android", @@ -3795,7 +3811,7 @@ }, { "kind": "ui-named-argument", - "line": 1568, + "line": 1590, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "No license notices are packaged in this build.", "surface": "android", @@ -3803,7 +3819,7 @@ }, { "kind": "ui-named-argument", - "line": 1594, + "line": 1616, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Open ${license.title}", "surface": "android", @@ -3811,7 +3827,7 @@ }, { "kind": "conditional-branch", - "line": 1629, + "line": 1651, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Check", "surface": "android", @@ -3819,7 +3835,7 @@ }, { "kind": "ui-named-argument", - "line": 1662, + "line": 1684, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Back", "surface": "android", @@ -3827,7 +3843,7 @@ }, { "kind": "conditional-branch", - "line": 1736, + "line": 1758, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Sending", "surface": "android", @@ -3835,7 +3851,7 @@ }, { "kind": "conditional-branch", - "line": 1745, + "line": 1767, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Allow Once", "surface": "android", @@ -3843,7 +3859,7 @@ }, { "kind": "conditional-branch", - "line": 1745, + "line": 1767, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Allowing", "surface": "android", @@ -3851,7 +3867,7 @@ }, { "kind": "conditional-branch", - "line": 1753, + "line": 1775, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Always", "surface": "android", @@ -3859,7 +3875,7 @@ }, { "kind": "conditional-branch", - "line": 1753, + "line": 1775, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Saving", "surface": "android", @@ -3867,7 +3883,7 @@ }, { "kind": "conditional-branch", - "line": 1761, + "line": 1783, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Deny", "surface": "android", @@ -3875,7 +3891,7 @@ }, { "kind": "conditional-branch", - "line": 1761, + "line": 1783, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Denying", "surface": "android", @@ -3883,7 +3899,7 @@ }, { "kind": "conditional-branch", - "line": 1786, + "line": 1808, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Review", "surface": "android", @@ -3891,7 +3907,7 @@ }, { "kind": "conditional-branch", - "line": 1814, + "line": 1836, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Issue", "surface": "android", @@ -3899,7 +3915,7 @@ }, { "kind": "conditional-branch", - "line": 1844, + "line": 1866, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Enabled", "surface": "android", @@ -3907,7 +3923,7 @@ }, { "kind": "conditional-branch", - "line": 1858, + "line": 1880, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "No", "surface": "android", @@ -3915,7 +3931,7 @@ }, { "kind": "conditional-branch", - "line": 1858, + "line": 1880, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Yes", "surface": "android", @@ -3923,7 +3939,7 @@ }, { "kind": "ui-named-argument", - "line": 1876, + "line": 1898, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Last Error", "surface": "android", @@ -3931,7 +3947,7 @@ }, { "kind": "ui-named-argument", - "line": 1879, + "line": 1901, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Delivery Error", "surface": "android", @@ -3939,7 +3955,7 @@ }, { "kind": "ui-named-argument", - "line": 1918, + "line": 1940, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Tap to copy", "surface": "android", @@ -3947,7 +3963,7 @@ }, { "kind": "ui-toast", - "line": 1933, + "line": 1955, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "$title copied", "surface": "android", @@ -3955,7 +3971,7 @@ }, { "kind": "conditional-branch", - "line": 1971, + "line": 1993, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Default assistant", "surface": "android", @@ -3963,7 +3979,7 @@ }, { "kind": "conditional-branch", - "line": 1973, + "line": 1995, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Default", "surface": "android", @@ -3971,7 +3987,7 @@ }, { "kind": "conditional-branch", - "line": 2029, + "line": 2051, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Needs attention", "surface": "android", @@ -3979,7 +3995,7 @@ }, { "kind": "conditional-branch", - "line": 2032, + "line": 2054, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Waiting ${minutes}m", "surface": "android", @@ -3987,7 +4003,7 @@ }, { "kind": "conditional-branch", - "line": 2032, + "line": 2054, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Waiting for review", "surface": "android", @@ -3995,7 +4011,7 @@ }, { "kind": "conditional-branch", - "line": 2060, + "line": 2082, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "${job.scheduleLabel} · ${formatCronWake(job.nextRunAtMs)} · ${job.promptPreview}", "surface": "android", @@ -4003,7 +4019,7 @@ }, { "kind": "conditional-branch", - "line": 2067, + "line": 2089, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "No limits reported", "surface": "android", @@ -4011,7 +4027,7 @@ }, { "kind": "conditional-branch", - "line": 2098, + "line": 2120, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Off", "surface": "android", @@ -4019,7 +4035,7 @@ }, { "kind": "conditional-branch", - "line": 2119, + "line": 2141, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "System Event Text", "surface": "android", @@ -4027,7 +4043,7 @@ }, { "kind": "conditional-branch", - "line": 2120, + "line": 2142, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Agent Prompt", "surface": "android", @@ -4035,7 +4051,7 @@ }, { "kind": "conditional-branch", - "line": 2121, + "line": 2143, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Command", "surface": "android", @@ -4043,7 +4059,7 @@ }, { "kind": "conditional-branch", - "line": 2122, + "line": 2144, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt", "source": "Payload Text", "surface": "android", @@ -6179,7 +6195,7 @@ }, { "kind": "ui-named-argument", - "line": 537, + "line": 544, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Local command center", "surface": "android", @@ -6187,7 +6203,7 @@ }, { "kind": "ui-named-argument", - "line": 538, + "line": 545, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "OC", "surface": "android", @@ -6195,7 +6211,7 @@ }, { "kind": "ui-named-argument", - "line": 540, + "line": 547, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Search", "surface": "android", @@ -6203,7 +6219,7 @@ }, { "kind": "ui-named-argument", - "line": 550, + "line": 557, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "OpenClaw", "surface": "android", @@ -6211,7 +6227,7 @@ }, { "kind": "ui-named-argument", - "line": 551, + "line": 558, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Design system prototype", "surface": "android", @@ -6219,7 +6235,7 @@ }, { "kind": "ui-named-argument", - "line": 553, + "line": 560, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Connected", "surface": "android", @@ -6227,7 +6243,7 @@ }, { "kind": "ui-named-argument", - "line": 564, + "line": 571, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Sessions", "surface": "android", @@ -6235,7 +6251,7 @@ }, { "kind": "ui-named-argument", - "line": 566, + "line": 573, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Testing testing 1 2 3", "surface": "android", @@ -6243,7 +6259,7 @@ }, { "kind": "ui-named-argument", - "line": 567, + "line": 574, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "14 messages · Android", "surface": "android", @@ -6251,7 +6267,7 @@ }, { "kind": "ui-named-argument", - "line": 571, + "line": 578, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Provider setup", "surface": "android", @@ -6259,7 +6275,7 @@ }, { "kind": "ui-named-argument", - "line": 572, + "line": 579, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "OpenClaw gateway", "surface": "android", @@ -6267,7 +6283,7 @@ }, { "kind": "ui-named-argument", - "line": 577, + "line": 584, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Ask OpenClaw anything", "surface": "android", @@ -6275,7 +6291,7 @@ }, { "kind": "ui-named-argument", - "line": 580, + "line": 587, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Start Chat", "surface": "android", @@ -6283,7 +6299,7 @@ }, { "kind": "ui-named-argument", - "line": 581, + "line": 588, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Voice", "surface": "android", @@ -6291,7 +6307,7 @@ }, { "kind": "ui-named-argument", - "line": 585, + "line": 592, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Realtime", "surface": "android", @@ -6299,7 +6315,7 @@ }, { "kind": "ui-named-argument", - "line": 586, + "line": 593, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Dictation", "surface": "android", @@ -6307,7 +6323,7 @@ }, { "kind": "ui-named-argument", - "line": 587, + "line": 594, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Screen", "surface": "android", @@ -6315,7 +6331,7 @@ }, { "kind": "ui-named-argument", - "line": 591, + "line": 598, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "Nothing needs your attention", "surface": "android", @@ -6323,7 +6339,7 @@ }, { "kind": "ui-named-argument", - "line": 592, + "line": 599, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt", "source": "OpenClaw will surface approvals, failed jobs, and channel issues here.", "surface": "android", @@ -10845,13 +10861,29 @@ "kind": "ui-call", "line": 968, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Use TLS", + "source": "Connection security", "surface": "apple", - "id": "native.apple.724f434cf4f7e84c" + "id": "native.apple.ab61dd967122023c" + }, + { + "kind": "ui-call", + "line": 969, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Unencrypted", + "surface": "apple", + "id": "native.apple.74dd3ea86bc67545" + }, + { + "kind": "ui-call", + "line": 972, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Secure (TLS)", + "surface": "apple", + "id": "native.apple.8eca91d435b2aec0" }, { "kind": "ui-named-argument", - "line": 970, + "line": 984, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Connect Manual", "surface": "apple", @@ -10859,7 +10891,7 @@ }, { "kind": "ui-call", - "line": 985, + "line": 1014, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Auto-connect on launch", "surface": "apple", @@ -10867,7 +10899,7 @@ }, { "kind": "ui-call", - "line": 986, + "line": 1015, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway Auth Token", "surface": "apple", @@ -10875,7 +10907,7 @@ }, { "kind": "ui-call", - "line": 987, + "line": 1016, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway Password", "surface": "apple", @@ -10883,7 +10915,7 @@ }, { "kind": "ui-call", - "line": 992, + "line": 1021, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Custom Headers", "surface": "apple", @@ -10891,7 +10923,7 @@ }, { "kind": "ui-call", - "line": 999, + "line": 1028, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Reset Onboarding", "surface": "apple", @@ -10899,7 +10931,7 @@ }, { "kind": "ui-call", - "line": 1026, + "line": 1055, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice Wake", "surface": "apple", @@ -10907,7 +10939,7 @@ }, { "kind": "ui-call", - "line": 1029, + "line": 1058, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Talk Mode", "surface": "apple", @@ -10915,7 +10947,7 @@ }, { "kind": "ui-call", - "line": 1037, + "line": 1066, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Speech Language", "surface": "apple", @@ -10923,7 +10955,7 @@ }, { "kind": "ui-call", - "line": 1044, + "line": 1073, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Speakerphone", "surface": "apple", @@ -10931,7 +10963,7 @@ }, { "kind": "ui-call", - "line": 1048, + "line": 1077, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Wake Words", "surface": "apple", @@ -10939,7 +10971,7 @@ }, { "kind": "ui-call", - "line": 1069, + "line": 1098, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice", "surface": "apple", @@ -10947,7 +10979,7 @@ }, { "kind": "ui-call", - "line": 1070, + "line": 1099, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Provider", "surface": "apple", @@ -10955,7 +10987,7 @@ }, { "kind": "ui-call", - "line": 1077, + "line": 1106, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Realtime Voice", "surface": "apple", @@ -10963,7 +10995,7 @@ }, { "kind": "ui-call", - "line": 1078, + "line": 1107, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway Default", "surface": "apple", @@ -10971,7 +11003,7 @@ }, { "kind": "ui-call", - "line": 1085, + "line": 1114, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice Mode", "surface": "apple", @@ -10979,7 +11011,7 @@ }, { "kind": "ui-call", - "line": 1086, + "line": 1115, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Active Voice", "surface": "apple", @@ -10987,7 +11019,7 @@ }, { "kind": "ui-call", - "line": 1088, + "line": 1117, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Last Voice Issue", "surface": "apple", @@ -10995,7 +11027,7 @@ }, { "kind": "ui-call", - "line": 1090, + "line": 1119, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Transport", "surface": "apple", @@ -11003,7 +11035,7 @@ }, { "kind": "ui-call", - "line": 1091, + "line": 1120, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "API Key", "surface": "apple", @@ -11011,7 +11043,7 @@ }, { "kind": "ui-call", - "line": 1098, + "line": 1127, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Show Talk Control", "surface": "apple", @@ -11019,7 +11051,7 @@ }, { "kind": "ui-call", - "line": 1099, + "line": 1128, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Default Share Instruction", "surface": "apple", @@ -11027,7 +11059,7 @@ }, { "kind": "ui-call", - "line": 1106, + "line": 1135, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Run Share Self-Test", "surface": "apple", @@ -11035,7 +11067,7 @@ }, { "kind": "ui-call", - "line": 1123, + "line": 1152, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Discovery Debug Logs", "surface": "apple", @@ -11043,7 +11075,7 @@ }, { "kind": "ui-call", - "line": 1126, + "line": 1155, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Debug Screen Status", "surface": "apple", @@ -11051,7 +11083,7 @@ }, { "kind": "ui-call", - "line": 1130, + "line": 1159, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Discovery Logs", "surface": "apple", @@ -11059,7 +11091,7 @@ }, { "kind": "ui-call", - "line": 1136, + "line": 1165, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Device", "surface": "apple", @@ -11067,7 +11099,7 @@ }, { "kind": "ui-call", - "line": 1137, + "line": 1166, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Device Name", "surface": "apple", @@ -11075,7 +11107,7 @@ }, { "kind": "ui-call", - "line": 1139, + "line": 1168, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Instance ID", "surface": "apple", @@ -11083,7 +11115,7 @@ }, { "kind": "conditional-branch", - "line": 1162, + "line": 1191, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "On", "surface": "apple", @@ -13017,14 +13049,6 @@ "surface": "apple", "id": "native.apple.9e9de5d188e3c693" }, - { - "kind": "ui-call", - "line": 869, - "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", - "source": "Use TLS", - "surface": "apple", - "id": "native.apple.ee93ad2b0824d2c4" - }, { "kind": "ui-call", "line": 870, @@ -13051,7 +13075,31 @@ }, { "kind": "ui-call", - "line": 948, + "line": 908, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Connection security", + "surface": "apple", + "id": "native.apple.a1eb951133402dd4" + }, + { + "kind": "ui-call", + "line": 909, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Unencrypted", + "surface": "apple", + "id": "native.apple.3e34c28edd053285" + }, + { + "kind": "ui-call", + "line": 912, + "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", + "source": "Secure (TLS)", + "surface": "apple", + "id": "native.apple.19ce2a63ffa5562a" + }, + { + "kind": "ui-call", + "line": 983, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Connecting…", "surface": "apple", @@ -13059,7 +13107,7 @@ }, { "kind": "ui-call", - "line": 952, + "line": 987, "path": "apps/ios/Sources/Onboarding/OnboardingWizardView.swift", "source": "Connect", "surface": "apple", diff --git a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt index 28ed779e40f..d2325534c31 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt @@ -2712,7 +2712,7 @@ class NodeRuntime private constructor( private fun gatewayTlsProbeFailureMessage(failure: GatewayTlsProbeFailure?): String = when (failure) { GatewayTlsProbeFailure.TLS_UNAVAILABLE -> - "Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected." + "Failed: no secure gateway endpoint was detected. Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address with Unencrypted selected." GatewayTlsProbeFailure.TLS_HANDSHAKE_TIMEOUT -> "Failed: secure endpoint reached, but TLS fingerprint verification timed out. Check Tailscale Serve or gateway TLS and retry." GatewayTlsProbeFailure.ENDPOINT_UNREACHABLE, null -> diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt index efec63bc0f9..7f55a51c8ef 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt @@ -18,6 +18,13 @@ internal data class GatewayEndpointConfig( val displayUrl: String, ) +/** Effective transport shown by manual gateway forms before they connect. */ +internal data class GatewayManualTransportPresentation( + val requiresTls: Boolean, + val effectiveTls: Boolean, + val helperText: String?, +) + /** Decoded setup-code payload; only one credential family is expected to be populated. */ internal data class GatewaySetupCode( val url: String, @@ -375,6 +382,53 @@ internal fun composeGatewayManualUrl( return "$scheme://${ai.openclaw.app.gateway.formatGatewayAuthority(bareHost, port)}" } +/** Keeps manual transport controls aligned with the runtime's remote-host TLS policy. */ +internal fun gatewayManualTransportPresentation( + hostInput: String, + requestedTls: Boolean, +): GatewayManualTransportPresentation { + val host = hostInput.trim() + if (host.isEmpty()) { + return gatewayManualTransportPresentation( + requiresTls = false, + effectiveTls = requestedTls, + ) + } + + if (host.contains("://")) { + val config = parseGatewayEndpointResult(host).config + if (config != null) { + return gatewayManualTransportPresentation( + requiresTls = !isLocalCleartextGatewayHost(config.host), + effectiveTls = config.tls, + ) + } + } + + val normalizedHost = host.trimEnd('/') + val requiresTls = !isLocalCleartextGatewayHost(normalizedHost) + val effectiveTls = requestedTls || requiresTls + return gatewayManualTransportPresentation( + requiresTls = requiresTls, + effectiveTls = effectiveTls, + ) +} + +private fun gatewayManualTransportPresentation( + requiresTls: Boolean, + effectiveTls: Boolean, +): GatewayManualTransportPresentation = + GatewayManualTransportPresentation( + requiresTls = requiresTls, + effectiveTls = effectiveTls, + helperText = + when { + requiresTls -> "Secure connection is required for this host." + effectiveTls -> null + else -> "Use only on a trusted private network." + }, + ) + private fun parseJsonObject(input: String): JsonObject? = runCatching { gatewaySetupJson.parseToJsonElement(input).jsonObject }.getOrNull() private fun resolveSetupCodeCandidate(rawInput: String): String? { diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt index cb6f35f74c6..571b77aa43c 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt @@ -588,6 +588,11 @@ fun OnboardingFlow( setupError = "That looks like a setup code. Go back and choose Setup Gateway, then Use setup code." return } + val transport = + gatewayManualTransportPresentation( + hostInput = manualHost, + requestedTls = manualTls, + ) val plan = resolveGatewayConnectPlan( useSetupCode = false, @@ -597,14 +602,14 @@ fun OnboardingFlow( savedManualTls = savedManualTls, manualHostInput = manualHost, manualPortInput = manualPort, - manualTlsInput = manualTls, + manualTlsInput = transport.effectiveTls, bootstrapTokenInput = "", tokenInput = token, passwordInput = password, ) if (plan == null) { val endpointError = - composeGatewayManualUrl(manualHost, manualPort, manualTls) + composeGatewayManualUrl(manualHost, manualPort, transport.effectiveTls) ?.let(::parseGatewayEndpointResult) ?.error ?: GatewayEndpointValidationError.INVALID_URL @@ -1574,6 +1579,13 @@ private fun ManualGatewaySetupScreen( onPair: () -> Unit, modifier: Modifier = Modifier, ) { + val transport = + remember(manualHost, manualTls) { + gatewayManualTransportPresentation( + hostInput = manualHost, + requestedTls = manualTls, + ) + } ClawScaffold(modifier = modifier, contentPadding = onboardingContentPadding()) { Column(modifier = Modifier.fillMaxSize().imePadding(), verticalArrangement = Arrangement.SpaceBetween) { LazyColumn( @@ -1613,16 +1625,27 @@ private fun ManualGatewaySetupScreen( } } item { - LabeledField(label = "Connection type") { + LabeledField(label = "Connection security") { Row(horizontalArrangement = Arrangement.spacedBy(9.dp)) { - TogglePill(text = "Local network", selected = !manualTls, onClick = { onManualTlsChange(false) }) - TogglePill(text = "Secure remote", selected = manualTls, onClick = { onManualTlsChange(true) }) + TogglePill( + text = "Unencrypted", + selected = !transport.effectiveTls, + enabled = !transport.requiresTls, + onClick = { onManualTlsChange(false) }, + ) + TogglePill( + text = "Secure (TLS)", + selected = transport.effectiveTls, + onClick = { onManualTlsChange(true) }, + ) + } + transport.helperText?.let { helperText -> + Text( + text = helperText, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, + ) } - Text( - text = "Local works for LAN or emulator hosts. Secure remote is for wss:// or Tailscale Serve/Funnel.", - style = ClawTheme.type.caption, - color = ClawTheme.colors.textMuted, - ) } } error?.let { message -> @@ -1714,6 +1737,7 @@ private fun GatewayRecoveryScreen( ) val context = LocalContext.current val approvalCommand = recoveryGatewayApprovalCommand(gatewayConnectionProblem) + val protocolUpdateCommand = recoveryGatewayProtocolMismatchCommand(gatewayConnectionProblem) val recoveryTitle = when { recoveryState == GatewayRecoveryUiState.Connected -> "Gateway paired" @@ -1803,6 +1827,16 @@ private fun GatewayRecoveryScreen( Spacer(modifier = Modifier.height(18.dp)) ApprovalCommandBlock(command = command, onCopy = { copyApprovalCommand(context, command) }) } + protocolUpdateCommand?.let { command -> + Spacer(modifier = Modifier.height(18.dp)) + Text( + text = "On the Gateway computer, run:", + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, + ) + Spacer(modifier = Modifier.height(8.dp)) + ApprovalCommandBlock(command = command, onCopy = { copyGatewayCommand(context, command) }) + } if (recoveryProgressItems.isNotEmpty()) { Spacer(modifier = Modifier.height(20.dp)) GatewayRecoveryProgress(items = recoveryProgressItems) @@ -2259,10 +2293,12 @@ private fun OnboardingHeader( private fun TogglePill( text: String, selected: Boolean, + enabled: Boolean = true, onClick: () -> Unit, ) { Surface( onClick = onClick, + enabled = enabled, modifier = Modifier.height(34.dp), shape = RoundedCornerShape(ClawTheme.radii.pill), color = if (selected) ClawTheme.colors.primary else ClawTheme.colors.surfaceRaised, @@ -2636,6 +2672,15 @@ private fun recoveryGatewayProtocolMismatchDetail(gatewayConnectionProblem: Gate return protocolMismatchVersions(clientMin, clientMax, expected)?.let { "$summary $it" } ?: summary } +internal fun recoveryGatewayProtocolMismatchCommand( + gatewayConnectionProblem: GatewayConnectionProblem?, +): String? { + if (gatewayConnectionProblem?.code != "PROTOCOL_MISMATCH") return null + val clientMin = gatewayConnectionProblem.clientMinProtocol ?: return null + val expected = gatewayConnectionProblem.expectedProtocol ?: return null + return "openclaw update".takeIf { clientMin > expected } +} + private fun protocolMismatchVersions( clientMin: Int?, clientMax: Int?, @@ -2758,6 +2803,15 @@ private fun copyApprovalCommand( Toast.makeText(context, "Approval command copied", Toast.LENGTH_SHORT).show() } +private fun copyGatewayCommand( + context: Context, + command: String, +) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("OpenClaw gateway command", command)) + Toast.makeText(context, "Command copied", Toast.LENGTH_SHORT).show() +} + /** One permission row plus launcher callback for onboarding's final setup step. */ private data class PermissionRowModel( val title: String, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt index a972f327004..31eee21d7cb 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt @@ -1156,6 +1156,13 @@ private fun GatewaySettingsScreen( var showSetupCodeHelp by remember { mutableStateOf(false) } var pendingSetupResetPlan by remember { mutableStateOf(null) } var pendingForgetStableId by remember { mutableStateOf(null) } + val transport = + remember(hostInput, tlsInput) { + gatewayManualTransportPresentation( + hostInput = hostInput, + requestedTls = tlsInput, + ) + } fun saveAndConnect(plan: GatewayConnectPlan) { validationText = null @@ -1308,11 +1315,26 @@ private fun GatewaySettingsScreen( ClawTextField(value = hostInput, onValueChange = { hostInput = it }, placeholder = "Host", modifier = Modifier.weight(1f)) ClawTextField(value = portInput, onValueChange = { portInput = it }, placeholder = "Port", modifier = Modifier.weight(0.62f)) } + Text(text = "Connection security", style = ClawTheme.type.caption, color = ClawTheme.colors.textMuted) + val securityOptions = listOf("Unencrypted", "Secure (TLS)") ClawSegmentedControl( - options = listOf("Local", "TLS"), - selected = if (tlsInput) "TLS" else "Local", - onSelect = { selected -> tlsInput = selected == "TLS" }, + options = securityOptions, + selected = if (transport.effectiveTls) "Secure (TLS)" else "Unencrypted", + onSelect = { selected -> tlsInput = selected == "Secure (TLS)" }, + enabledOptions = + if (transport.requiresTls) { + setOf("Secure (TLS)") + } else { + securityOptions.toSet() + }, ) + transport.helperText?.let { helperText -> + Text( + text = helperText, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, + ) + } Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { ClawTextField(value = tokenInput, onValueChange = { tokenInput = it }, placeholder = "Token", modifier = Modifier.weight(1f)) ClawTextField(value = bootstrapTokenInput, onValueChange = { bootstrapTokenInput = it }, placeholder = "Bootstrap", modifier = Modifier.weight(1.05f)) @@ -1333,7 +1355,7 @@ private fun GatewaySettingsScreen( savedManualTls = manualTls, manualHostInput = hostInput, manualPortInput = portInput, - manualTlsInput = tlsInput, + manualTlsInput = transport.effectiveTls, tokenInput = tokenInput, bootstrapTokenInput = bootstrapTokenInput, passwordInput = passwordInput, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt index 26420d78ceb..8a6f94c4860 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/design/ClawComponents.kt @@ -458,6 +458,7 @@ internal fun ClawSegmentedControl( selected: String, onSelect: (String) -> Unit, modifier: Modifier = Modifier, + enabledOptions: Set = options.toSet(), ) { Row( modifier = @@ -469,20 +470,26 @@ internal fun ClawSegmentedControl( ) { options.forEach { option -> val active = option == selected + val enabled = option in enabledOptions Box( modifier = Modifier .weight(1f) .clip(RoundedCornerShape(ClawTheme.radii.control)) .background(if (active) ClawTheme.colors.primary else Color.Transparent) - .clickable { onSelect(option) } + .clickable(enabled = enabled) { onSelect(option) } .padding(horizontal = 9.dp, vertical = 7.dp), contentAlignment = Alignment.Center, ) { Text( text = option, style = ClawTheme.type.caption, - color = if (active) ClawTheme.colors.primaryText else ClawTheme.colors.textMuted, + color = + when { + active -> ClawTheme.colors.primaryText + enabled -> ClawTheme.colors.textMuted + else -> ClawTheme.colors.textSubtle + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/apps/android/app/src/test/java/ai/openclaw/app/GatewayBootstrapAuthTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/GatewayBootstrapAuthTest.kt index 4e52ba4edc0..d23949d0c2d 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/GatewayBootstrapAuthTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/GatewayBootstrapAuthTest.kt @@ -630,7 +630,7 @@ class GatewayBootstrapAuthTest { ) assertEquals( - "Failed: this host requires wss:// or Tailscale Serve. No TLS endpoint detected.", + "Failed: no secure gateway endpoint was detected. Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address with Unencrypted selected.", waitForStatusText(runtime), ) assertNull(runtime.pendingGatewayTrust.value) diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/GatewayConfigResolverTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/GatewayConfigResolverTest.kt index ced306f5efe..7644323a18e 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/ui/GatewayConfigResolverTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/GatewayConfigResolverTest.kt @@ -9,6 +9,45 @@ import java.util.Base64 @RunWith(RobolectricTestRunner::class) class GatewayConfigResolverTest { + @Test + fun manualTransportForcesSecureConnectionForRemoteHosts() { + val presentation = + gatewayManualTransportPresentation( + hostInput = "gateway.example.com", + requestedTls = false, + ) + + assertEquals(true, presentation.requiresTls) + assertEquals(true, presentation.effectiveTls) + assertEquals("Secure connection is required for this host.", presentation.helperText) + } + + @Test + fun manualTransportAllowsUnencryptedPrivateLanConnections() { + val presentation = + gatewayManualTransportPresentation( + hostInput = "192.168.1.20", + requestedTls = false, + ) + + assertEquals(false, presentation.requiresTls) + assertEquals(false, presentation.effectiveTls) + assertEquals("Use only on a trusted private network.", presentation.helperText) + } + + @Test + fun manualTransportDoesNotRepeatSelectedPrivateLanTlsState() { + val presentation = + gatewayManualTransportPresentation( + hostInput = "192.168.1.20", + requestedTls = true, + ) + + assertEquals(false, presentation.requiresTls) + assertEquals(true, presentation.effectiveTls) + assertNull(presentation.helperText) + } + @Test fun parseGatewayEndpointUsesDefaultTlsPortForBareWssUrls() { val parsed = parseGatewayEndpoint("wss://gateway.example") diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt index 18b5da9966a..3a85baca39f 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/OnboardingFlowLogicTest.kt @@ -945,6 +945,23 @@ class OnboardingFlowLogicTest { ), ), ) + assertEquals( + "openclaw update", + recoveryGatewayProtocolMismatchCommand( + GatewayConnectionProblem( + code = "PROTOCOL_MISMATCH", + message = "protocol mismatch", + reason = null, + requestId = null, + recommendedNextStep = null, + pauseReconnect = true, + retryable = false, + clientMinProtocol = 6, + clientMaxProtocol = 6, + expectedProtocol = 5, + ), + ), + ) } @Test diff --git a/apps/ios/Sources/Design/SettingsProTabSections.swift b/apps/ios/Sources/Design/SettingsProTabSections.swift index 7493e757183..5206fda843d 100644 --- a/apps/ios/Sources/Design/SettingsProTabSections.swift +++ b/apps/ios/Sources/Design/SettingsProTabSections.swift @@ -965,7 +965,21 @@ extension SettingsProTab { TextField("Port", text: self.manualPortBinding) .font(OpenClawType.body) .keyboardType(.numberPad) - self.settingsToggle("Use TLS", isOn: self.$manualGatewayTLS) + Picker("Connection security", selection: self.manualGatewayTLSBinding) { + Text("Unencrypted") + .font(OpenClawType.captionSemiBold) + .tag(false) + Text("Secure (TLS)") + .font(OpenClawType.captionSemiBold) + .tag(true) + } + .pickerStyle(.segmented) + .disabled(self.manualGatewayTransport.requiresTLS) + if let helperText = self.manualGatewayTransport.helperText { + Text(helperText) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + } self.gatewayActionButton( title: "Connect Manual", icon: "network", @@ -980,6 +994,21 @@ extension SettingsProTab { .disabled(self.setupAttemptID != nil) } + private var manualGatewayTransport: GatewayManualTransportPresentation { + GatewayConnectionController.manualTransportPresentation( + host: self.manualGatewayHost, + requestedTLS: self.manualGatewayTLS) + } + + private var manualGatewayTLSBinding: Binding { + Binding( + get: { self.manualGatewayTransport.effectiveTLS }, + set: { enabled in + guard !self.manualGatewayTransport.requiresTLS else { return } + self.manualGatewayTLS = enabled + }) + } + var gatewayAdvancedCard: some View { Section { self.settingsToggle("Auto-connect on launch", isOn: self.$gatewayAutoConnect) diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index 58c360d97f7..f7bb074d6e3 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -1261,7 +1261,8 @@ final class GatewayConnectionController { "TLS fingerprint verification timed out for \(host):\(port). " + "Secure endpoint was reached, but TLS did not finish in time." case .tlsUnavailable: - "No TLS endpoint detected at \(host):\(port). Remote gateways must use HTTPS/WSS." + "No secure gateway endpoint was detected at \(host):\(port). " + + "Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address with Unencrypted selected." case .certificateUnavailable: "Could not read the TLS certificate from \(host):\(port)." } @@ -1494,6 +1495,12 @@ private struct GatewayPendingTrustConnect { let gatewayGeneration: UInt64? } +struct GatewayManualTransportPresentation: Equatable { + let requiresTLS: Bool + let effectiveTLS: Bool + let helperText: String? +} + extension GatewayConnectionController { private func buildGatewayURL(host: String, port: Int, useTLS: Bool) -> URL? { let scheme = useTLS ? "wss" : "ws" @@ -1505,11 +1512,29 @@ extension GatewayConnectionController { } private func resolveManualUseTLS(host: String, useTLS: Bool) -> Bool { - useTLS || self.shouldRequireTLS(host: host) + Self.manualTransportPresentation( + host: host, + requestedTLS: useTLS).effectiveTLS } - private func shouldRequireTLS(host: String) -> Bool { - !LoopbackHost.isLocalNetworkHost(host) + static func manualTransportPresentation( + host: String, + requestedTLS: Bool) -> GatewayManualTransportPresentation + { + let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines) + let requiresTLS = !trimmedHost.isEmpty && !LoopbackHost.isLocalNetworkHost(trimmedHost) + let effectiveTLS = requestedTLS || requiresTLS + let helperText: String? = if requiresTLS { + "Secure connection is required for this host." + } else if effectiveTLS { + nil + } else { + "Use only on a trusted private network." + } + return GatewayManualTransportPresentation( + requiresTLS: requiresTLS, + effectiveTLS: effectiveTLS, + helperText: helperText) } private func manualStableID(host: String, port: Int) -> String { diff --git a/apps/ios/Sources/Gateway/GatewayProblemPrimaryAction.swift b/apps/ios/Sources/Gateway/GatewayProblemPrimaryAction.swift index 5f5c7b40596..40e79d6726e 100644 --- a/apps/ios/Sources/Gateway/GatewayProblemPrimaryAction.swift +++ b/apps/ios/Sources/Gateway/GatewayProblemPrimaryAction.swift @@ -24,8 +24,12 @@ enum GatewayProblemPrimaryAction { } @MainActor - static func openProtocolMismatchHelpIfNeeded(_ problem: GatewayConnectionProblem) -> Bool { + static func handleProtocolMismatchIfNeeded(_ problem: GatewayConnectionProblem) -> Bool { guard problem.kind == .protocolMismatch else { return false } + if let command = problem.actionCommand { + UIPasteboard.general.string = command + return true + } if let url = problem.docsURL { UIApplication.shared.open(url) } diff --git a/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift index 8ff0c790f51..c39229eee33 100644 --- a/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift +++ b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift @@ -155,7 +155,7 @@ struct GatewayQuickSetupSheet: View { _ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem) return } - if GatewayProblemPrimaryAction.openProtocolMismatchHelpIfNeeded(problem) { + if GatewayProblemPrimaryAction.handleProtocolMismatchIfNeeded(problem) { return } guard problem.retryable else { return } diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index 87b3ff0f4da..5f7004c9ce6 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -687,7 +687,7 @@ struct OnboardingWizardView: View { self.onboardingTextField("Host", text: self.manualHostBinding, focusedField: .manualHost) self.onboardingTextField("Port", text: self.manualPortTextBinding, focusedField: .manualPort) .keyboardType(.numberPad) - self.onboardingButtonToggle("Use TLS", isOn: self.$manualTLS) + self.manualConnectionSecurityRows self.manualConnectButton } header: { Text("Developer Local") @@ -866,7 +866,7 @@ extension OnboardingWizardView { self.onboardingTextField("Host", text: self.manualHostBinding, focusedField: .manualHost) self.onboardingTextField("Port", text: self.manualPortTextBinding, focusedField: .manualPort) .keyboardType(.numberPad) - self.onboardingButtonToggle("Use TLS", isOn: self.$manualTLS) + self.manualConnectionSecurityRows self.onboardingTextField( "Discovery Domain (optional)", text: self.$discoveryDomain, @@ -888,6 +888,41 @@ extension OnboardingWizardView { } } + private var manualTransport: GatewayManualTransportPresentation { + GatewayConnectionController.manualTransportPresentation( + host: self.manualHost, + requestedTLS: self.manualTLS) + } + + private var manualTLSBinding: Binding { + Binding( + get: { self.manualTransport.effectiveTLS }, + set: { enabled in + guard !self.manualTransport.requiresTLS else { return } + self.manualTLS = enabled + }) + } + + @ViewBuilder + private var manualConnectionSecurityRows: some View { + Picker("Connection security", selection: self.manualTLSBinding) { + Text("Unencrypted") + .font(OpenClawType.captionSemiBold) + .tag(false) + Text("Secure (TLS)") + .font(OpenClawType.captionSemiBold) + .tag(true) + } + .pickerStyle(.segmented) + .disabled(self.manualTransport.requiresTLS) + + if let helperText = self.manualTransport.helperText { + Text(helperText) + .font(OpenClawType.footnote) + .foregroundStyle(.secondary) + } + } + private func onboardingLabeledContent(_ title: LocalizedStringKey, value: String) -> some View { LabeledContent { Text(verbatim: value) @@ -1650,7 +1685,7 @@ extension OnboardingWizardView { _ = await self.gatewayController.trustRotatedGatewayCertificate(from: problem) return } - if GatewayProblemPrimaryAction.openProtocolMismatchHelpIfNeeded(problem) { + if GatewayProblemPrimaryAction.handleProtocolMismatchIfNeeded(problem) { return } guard problem.retryable else { return } diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index 603d441fa84..580c097473e 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -1247,7 +1247,7 @@ extension RootTabs { } } else if problem.canTrustRotatedCertificate { Task { await self.gatewayController.trustRotatedGatewayCertificate(from: problem) } - } else if GatewayProblemPrimaryAction.openProtocolMismatchHelpIfNeeded(problem) { + } else if GatewayProblemPrimaryAction.handleProtocolMismatchIfNeeded(problem) { return } else if problem.retryable { Task { await self.gatewayController.connectActiveGateway() } diff --git a/apps/ios/Tests/GatewayConnectionSecurityTests.swift b/apps/ios/Tests/GatewayConnectionSecurityTests.swift index f9f3151d5c7..9af8361595f 100644 --- a/apps/ios/Tests/GatewayConnectionSecurityTests.swift +++ b/apps/ios/Tests/GatewayConnectionSecurityTests.swift @@ -120,6 +120,36 @@ import Testing #expect(controller._test_resolveManualUseTLS(host: "0.0.0.0", useTLS: false) == false) } + @Test @MainActor func `manual transport presentation shows effective remote security`() { + let presentation = GatewayConnectionController.manualTransportPresentation( + host: "gateway.example.com", + requestedTLS: false) + + #expect(presentation.requiresTLS) + #expect(presentation.effectiveTLS) + #expect(presentation.helperText == "Secure connection is required for this host.") + } + + @Test @MainActor func `manual transport presentation allows unencrypted private LAN`() { + let presentation = GatewayConnectionController.manualTransportPresentation( + host: "192.168.1.20", + requestedTLS: false) + + #expect(!presentation.requiresTLS) + #expect(!presentation.effectiveTLS) + #expect(presentation.helperText == "Use only on a trusted private network.") + } + + @Test @MainActor func `manual transport presentation does not repeat selected private LAN TLS state`() { + let presentation = GatewayConnectionController.manualTransportPresentation( + host: "192.168.1.20", + requestedTLS: true) + + #expect(!presentation.requiresTLS) + #expect(presentation.effectiveTLS) + #expect(presentation.helperText == nil) + } + @Test @MainActor func `manual connections allow private lan plaintext`() { let controller = self.makeController() @@ -210,7 +240,7 @@ import Testing #expect(appModel.gatewayStatusText == "Verify gateway TLS fingerprint") } - @Test @MainActor func staleTrustAcceptanceReleasesAutoConnectSuppression() async { + @Test @MainActor func `stale trust acceptance releases auto connect suppression`() async { let host = "gateway-\(UUID().uuidString).example.com" let port = 18789 let stableID = "manual|\(host.lowercased())|\(port)" @@ -346,7 +376,7 @@ import Testing #expect(appModel.gatewayStatusText == message) } - @Test @MainActor func targetSwitchCancelsSuspendedDiscoveryResolution() async { + @Test @MainActor func `target switch cancels suspended discovery resolution`() async { let defaults = UserDefaults.standard let previousInstanceID = defaults.string(forKey: "node.instanceId") defer { diff --git a/apps/ios/Tests/GatewayProblemPrimaryActionTests.swift b/apps/ios/Tests/GatewayProblemPrimaryActionTests.swift index 97b42a82641..b5a3ffe83db 100644 --- a/apps/ios/Tests/GatewayProblemPrimaryActionTests.swift +++ b/apps/ios/Tests/GatewayProblemPrimaryActionTests.swift @@ -19,6 +19,23 @@ struct GatewayProblemPrimaryActionTests { #expect(title == "Update app") } + @Test func `older gateway exposes its copyable update command`() { + let problem = GatewayConnectionProblem( + kind: .protocolMismatch, + owner: .gateway, + title: "Gateway update required", + message: "The gateway is older than this app.", + actionLabel: "Copy update command", + actionCommand: "openclaw update", + retryable: false, + pauseReconnect: true) + + let title = GatewayProblemPrimaryAction.title(for: problem, retryTitle: "Retry connection") + + #expect(title == "Copy update command") + #expect(problem.actionCommand == "openclaw update") + } + @Test func `reset-suggesting problem uses reset title when provided`() { let problem = GatewayConnectionProblem( kind: .gatewayAuthTokenMismatch, diff --git a/apps/ios/Tests/OpenClawTypographyTests.swift b/apps/ios/Tests/OpenClawTypographyTests.swift index 3d63921cb3e..9a1ccb7cc89 100644 --- a/apps/ios/Tests/OpenClawTypographyTests.swift +++ b/apps/ios/Tests/OpenClawTypographyTests.swift @@ -226,6 +226,20 @@ struct OpenClawTypographyTests { #expect(onboardingWizard.contains("title: \"Same Machine (Dev)\"")) #expect(onboardingWizard.contains("if lastMode == .developerLocal")) #expect(onboardingWizard.contains("self.developerModeEnabled = true")) + let onboardingSecurityPicker = try Self.extract( + onboardingWizard, + from: "private var manualConnectionSecurityRows", + to: " private func onboardingLabeledContent") + let onboardingUnencryptedOption = try Self.extract( + onboardingSecurityPicker, + from: "Text(\"Unencrypted\")", + to: ".tag(false)") + let onboardingSecureOption = try Self.extract( + onboardingSecurityPicker, + from: "Text(\"Secure (TLS)\")", + to: ".tag(true)") + #expect(onboardingUnencryptedOption.contains(".font(OpenClawType.captionSemiBold)")) + #expect(onboardingSecureOption.contains(".font(OpenClawType.captionSemiBold)")) #expect(settingsSections.contains(".font(OpenClawType.body)")) #expect(settingsSections.contains("self.settingsToggle(\"Show Talk Control\", isOn: self.$talkButtonEnabled)")) @@ -253,6 +267,20 @@ struct OpenClawTypographyTests { #expect(gatewaySecureField.contains(".autocorrectionDisabled()")) #expect(settingsSections.contains("Picker(\"Default Agent\", selection: self.$selectedAgentPickerId)")) #expect(settingsSections.contains("Text(\"Default\")")) + let settingsSecurityPicker = try Self.extract( + settingsSections, + from: "Picker(\"Connection security\", selection: self.manualGatewayTLSBinding)", + to: " .pickerStyle(.segmented)") + let settingsUnencryptedOption = try Self.extract( + settingsSecurityPicker, + from: "Text(\"Unencrypted\")", + to: ".tag(false)") + let settingsSecureOption = try Self.extract( + settingsSecurityPicker, + from: "Text(\"Secure (TLS)\")", + to: ".tag(true)") + #expect(settingsUnencryptedOption.contains(".font(OpenClawType.captionSemiBold)")) + #expect(settingsSecureOption.contains(".font(OpenClawType.captionSemiBold)")) #expect(!privacyAccess.contains("DisclosureGroup(\"Privacy & Access\")")) #expect(privacyAccess.contains("Text(\"Privacy & Access\")")) diff --git a/apps/ios/Tests/RootTabsSourceGuardTests.swift b/apps/ios/Tests/RootTabsSourceGuardTests.swift index bec57c203f2..4aa346a9680 100644 --- a/apps/ios/Tests/RootTabsSourceGuardTests.swift +++ b/apps/ios/Tests/RootTabsSourceGuardTests.swift @@ -930,7 +930,7 @@ struct RootTabsSourceGuardTests { #expect(rootSource.contains("resetTitle: \"Reset onboarding\"")) #expect(rootSource.contains("GatewayOnboardingReset.reset(appModel: self.appModel, instanceId: instanceId)")) #expect(rootSource.contains("self.gatewayController.trustRotatedGatewayCertificate(from: problem)")) - #expect(rootSource.contains("GatewayProblemPrimaryAction.openProtocolMismatchHelpIfNeeded(problem)")) + #expect(rootSource.contains("GatewayProblemPrimaryAction.handleProtocolMismatchIfNeeded(problem)")) #expect(rootSource.contains("await self.gatewayController.connectActiveGateway()")) #expect(rootSource.contains("GatewayProblemDetailsSheet(")) diff --git a/apps/ios/UITests/OpenClawSnapshotUITests.swift b/apps/ios/UITests/OpenClawSnapshotUITests.swift index 13905725022..fe67145ec8a 100644 --- a/apps/ios/UITests/OpenClawSnapshotUITests.swift +++ b/apps/ios/UITests/OpenClawSnapshotUITests.swift @@ -689,11 +689,9 @@ final class OpenClawSnapshotUITests: XCTestCase { 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() - } + let unencrypted = app.buttons["Unencrypted"] + XCTAssertTrue(unencrypted.waitForExistence(timeout: 5)) + unencrypted.tap() app.buttons["Connect"].tap() let tokenField = app.secureTextFields["Gateway Auth Token"] diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift index 2474ec49f9e..88e95c304bf 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayConnectionProblem.swift @@ -807,6 +807,7 @@ public enum GatewayConnectionProblemMapper { let message: String let owner: GatewayConnectionProblem.Owner let actionLabel: String + let actionCommand: String? if let clientMax = authError.clientMaxProtocol, let expected = authError.expectedProtocol, clientMax < expected @@ -816,6 +817,7 @@ public enum GatewayConnectionProblemMapper { ?? "This app is older than the gateway. Update OpenClaw on this device, then retry." owner = .iphone actionLabel = authError.actionLabel ?? "Update app" + actionCommand = authError.actionCommand } else if let clientMin = authError.clientMinProtocol, let expected = authError.expectedProtocol, clientMin > expected @@ -824,13 +826,15 @@ public enum GatewayConnectionProblemMapper { message = authError.userMessageOverride ?? "The gateway is older than this app. Update OpenClaw on the gateway host, then retry." owner = .gateway - actionLabel = authError.actionLabel ?? "Update gateway" + actionLabel = authError.actionLabel ?? "Copy update command" + actionCommand = authError.actionCommand ?? "openclaw update" } else { title = authError.titleOverride ?? "OpenClaw update required" message = authError.userMessageOverride ?? "The app and gateway use incompatible protocol versions. Update OpenClaw on both, then retry." owner = .both actionLabel = authError.actionLabel ?? "Update OpenClaw" + actionCommand = authError.actionCommand } return self.problem( kind: .protocolMismatch, @@ -838,7 +842,7 @@ public enum GatewayConnectionProblemMapper { title: title, message: message, actionLabel: actionLabel, - actionCommand: authError.actionCommand, + actionCommand: actionCommand, docsURL: self.docsURL( authError.docsURLString, fallback: "https://docs.openclaw.ai/gateway/troubleshooting"), diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayErrorsTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayErrorsTests.swift index 19ec063be56..3988fa13226 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayErrorsTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/GatewayErrorsTests.swift @@ -88,6 +88,8 @@ struct GatewayErrorsTests { #expect(problem?.title == "Gateway update required") #expect(problem? .message == "The gateway is older than this app. Update OpenClaw on the gateway host, then retry.") + #expect(problem?.actionLabel == "Copy update command") + #expect(problem?.actionCommand == "openclaw update") #expect(problem?.retryable == false) #expect(problem?.pauseReconnect == true) }