diff --git a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt index b4a6f042312..0e27b801a49 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt @@ -91,6 +91,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { val manualPort: StateFlow = prefs.manualPort val manualTls: StateFlow = prefs.manualTls val gatewayToken: StateFlow = prefs.gatewayToken + val gatewayBootstrapToken: StateFlow = prefs.gatewayBootstrapToken val onboardingCompleted: StateFlow = prefs.onboardingCompleted val canvasDebugStatusEnabled: StateFlow = prefs.canvasDebugStatusEnabled val speakerEnabled: StateFlow = prefs.speakerEnabled diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt index 2bb32ae2515..4b483ba28f5 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/ConnectTabScreen.kt @@ -72,6 +72,7 @@ fun ConnectTabScreen(viewModel: MainViewModel) { val manualTls by viewModel.manualTls.collectAsState() val manualEnabled by viewModel.manualEnabled.collectAsState() val gatewayToken by viewModel.gatewayToken.collectAsState() + val gatewayBootstrapToken by viewModel.gatewayBootstrapToken.collectAsState() val pendingTrust by viewModel.pendingGatewayTrust.collectAsState() var advancedOpen by rememberSaveable { mutableStateOf(false) } @@ -244,6 +245,7 @@ fun ConnectTabScreen(viewModel: MainViewModel) { manualHost = manualHostInput, manualPort = manualPortInput, manualTls = manualTlsInput, + fallbackBootstrapToken = gatewayBootstrapToken, fallbackToken = gatewayToken, fallbackPassword = passwordInput, ) 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 fa0aea7de00..adfc32f1cab 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 @@ -40,6 +40,7 @@ internal fun resolveGatewayConnectConfig( manualHost: String, manualPort: String, manualTls: Boolean, + fallbackBootstrapToken: String, fallbackToken: String, fallbackPassword: String, ): GatewayConnectConfig? { @@ -75,7 +76,12 @@ internal fun resolveGatewayConnectConfig( host = parsed.host, port = parsed.port, tls = parsed.tls, - bootstrapToken = "", + bootstrapToken = + if (fallbackToken.isBlank() && fallbackPassword.isBlank()) { + fallbackBootstrapToken.trim() + } else { + "" + }, token = fallbackToken.trim(), password = fallbackPassword.trim(), ) 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 47f5ef09796..d6c6c22a8fb 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 @@ -158,6 +158,7 @@ class GatewayConfigResolverTest { manualHost = "", manualPort = "", manualTls = true, + fallbackBootstrapToken = "", fallbackToken = "shared-token", fallbackPassword = "shared-password", ) @@ -182,6 +183,7 @@ class GatewayConfigResolverTest { manualHost = "", manualPort = "", manualTls = true, + fallbackBootstrapToken = "", fallbackToken = "shared-token", fallbackPassword = "shared-password", ) @@ -194,6 +196,47 @@ class GatewayConfigResolverTest { assertNull(resolved?.password?.takeIf { it.isNotEmpty() }) } + @Test + fun resolveGatewayConnectConfigManualPreservesBootstrapTokenWhenNoReplacementAuthExists() { + val resolved = + resolveGatewayConnectConfig( + useSetupCode = false, + setupCode = "", + manualHost = "192.168.31.100", + manualPort = "18789", + manualTls = false, + fallbackBootstrapToken = "bootstrap-1", + fallbackToken = "", + fallbackPassword = "", + ) + + assertEquals("192.168.31.100", resolved?.host) + assertEquals(18789, resolved?.port) + assertEquals(false, resolved?.tls) + assertEquals("bootstrap-1", resolved?.bootstrapToken) + assertEquals("", resolved?.token) + assertEquals("", resolved?.password) + } + + @Test + fun resolveGatewayConnectConfigManualDropsBootstrapTokenWhenReplacementPasswordExists() { + val resolved = + resolveGatewayConnectConfig( + useSetupCode = false, + setupCode = "", + manualHost = "192.168.31.100", + manualPort = "18789", + manualTls = false, + fallbackBootstrapToken = "bootstrap-1", + fallbackToken = "", + fallbackPassword = "password-1", + ) + + assertEquals("", resolved?.bootstrapToken) + assertEquals("", resolved?.token) + assertEquals("password-1", resolved?.password) + } + private fun encodeSetupCode(payloadJson: String): String { return Base64.getUrlEncoder().withoutPadding().encodeToString(payloadJson.toByteArray(Charsets.UTF_8)) }