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 b60e9ed837c..f99154c67ad 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 @@ -204,10 +204,6 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { prefs.setOnboardingCompleted(value) } - fun hasStoredNodeDeviceToken(): Boolean { - return ensureRuntime().hasStoredNodeDeviceToken() - } - fun setCanvasDebugStatusEnabled(value: Boolean) { prefs.setCanvasDebugStatusEnabled(value) } 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 4ef9041b1e9..9a2f0bf0214 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 @@ -535,10 +535,6 @@ class NodeRuntime( fun setGatewayBootstrapToken(value: String) = prefs.setGatewayBootstrapToken(value) fun setGatewayPassword(value: String) = prefs.setGatewayPassword(value) fun setOnboardingCompleted(value: Boolean) = prefs.setOnboardingCompleted(value) - fun hasStoredNodeDeviceToken(): Boolean { - val deviceId = identityStore.loadOrCreate().deviceId - return !deviceAuthStore.loadToken(deviceId, "node").isNullOrBlank() - } val lastDiscoveredStableId: StateFlow = prefs.lastDiscoveredStableId val canvasDebugStatusEnabled: StateFlow = prefs.canvasDebugStatusEnabled val notificationForwardingEnabled: StateFlow = prefs.notificationForwardingEnabled @@ -783,8 +779,9 @@ class NodeRuntime( val bootstrapToken = prefs.loadGatewayBootstrapToken() val password = prefs.loadGatewayPassword() val tls = connectionManager.resolveTlsParams(endpoint) - val bootstrapOnly = isBootstrapOnlyGatewayAuth(token, bootstrapToken, password) - if (bootstrapOnly) { + val connectOperator = + shouldConnectOperatorSession(token, bootstrapToken, password, loadStoredRoleDeviceToken("operator")) + if (!connectOperator) { operatorConnected = false operatorStatusText = "Offline" operatorSession.disconnect() @@ -807,7 +804,7 @@ class NodeRuntime( connectionManager.buildNodeConnectOptions(), tls, ) - if (!bootstrapOnly) { + if (connectOperator) { operatorSession.reconnect() } nodeSession.reconnect() @@ -835,8 +832,9 @@ class NodeRuntime( val token = prefs.loadGatewayToken() val bootstrapToken = prefs.loadGatewayBootstrapToken() val password = prefs.loadGatewayPassword() - val bootstrapOnly = isBootstrapOnlyGatewayAuth(token, bootstrapToken, password) - if (bootstrapOnly) { + val connectOperator = + shouldConnectOperatorSession(token, bootstrapToken, password, loadStoredRoleDeviceToken("operator")) + if (!connectOperator) { operatorConnected = false operatorStatusText = "Offline" operatorSession.disconnect() @@ -890,6 +888,11 @@ class NodeRuntime( connect(GatewayEndpoint.manual(host = host, port = port)) } + private fun loadStoredRoleDeviceToken(role: String): String? { + val deviceId = identityStore.loadOrCreate().deviceId + return deviceAuthStore.loadToken(deviceId, role) + } + fun disconnect() { connectedEndpoint = null _pendingGatewayTrust.value = null @@ -1219,12 +1222,18 @@ class NodeRuntime( } -internal fun isBootstrapOnlyGatewayAuth( +internal fun shouldConnectOperatorSession( token: String?, bootstrapToken: String?, password: String?, + storedOperatorToken: String?, ): Boolean { - return !bootstrapToken.isNullOrBlank() && token.isNullOrBlank() && password.isNullOrBlank() + return ( + !token.isNullOrBlank() || + !bootstrapToken.isNullOrBlank() || + !password.isNullOrBlank() || + !storedOperatorToken.isNullOrBlank() + ) } private enum class HomeCanvasGatewayState { 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 4781d33986f..3868936ae5e 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 @@ -228,13 +228,8 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) { var manualTls by rememberSaveable { mutableStateOf(false) } var gatewayError by rememberSaveable { mutableStateOf(null) } var attemptedConnect by rememberSaveable { mutableStateOf(false) } - val canRecoverBootstrapRetry = - gatewayInputMode == GatewayInputMode.SetupCode && - attemptedConnect && - statusText.contains("bootstrap token invalid or expired", ignoreCase = true) && - viewModel.hasStoredNodeDeviceToken() val canFinishOnboarding = - isConnected || (gatewayInputMode == GatewayInputMode.SetupCode && (isNodeConnected || canRecoverBootstrapRetry)) + isConnected || (gatewayInputMode == GatewayInputMode.SetupCode && isNodeConnected) val lifecycleOwner = LocalLifecycleOwner.current val qrScannerOptions = 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 8b501bb913d..acf470808f2 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 @@ -6,15 +6,16 @@ import org.junit.Test class GatewayBootstrapAuthTest { @Test - fun detectsBootstrapOnlyGatewayAuth() { - assertTrue(isBootstrapOnlyGatewayAuth(token = "", bootstrapToken = "bootstrap-1", password = "")) - assertTrue(isBootstrapOnlyGatewayAuth(token = null, bootstrapToken = "bootstrap-1", password = null)) + fun connectsOperatorSessionWhenBootstrapAuthExists() { + assertTrue(shouldConnectOperatorSession(token = "", bootstrapToken = "bootstrap-1", password = "", storedOperatorToken = "")) + assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = "bootstrap-1", password = null, storedOperatorToken = null)) } @Test - fun rejectsBootstrapOnlyGatewayAuthWhenSharedCredentialsExist() { - assertFalse(isBootstrapOnlyGatewayAuth(token = "shared-token", bootstrapToken = "bootstrap-1", password = null)) - assertFalse(isBootstrapOnlyGatewayAuth(token = null, bootstrapToken = "bootstrap-1", password = "shared-password")) - assertFalse(isBootstrapOnlyGatewayAuth(token = null, bootstrapToken = "", password = null)) + fun skipsOperatorSessionOnlyWhenNoSharedBootstrapOrStoredAuthExists() { + assertTrue(shouldConnectOperatorSession(token = "shared-token", bootstrapToken = "bootstrap-1", password = null, storedOperatorToken = null)) + assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = "bootstrap-1", password = "shared-password", storedOperatorToken = null)) + assertTrue(shouldConnectOperatorSession(token = null, bootstrapToken = null, password = null, storedOperatorToken = "stored-token")) + assertFalse(shouldConnectOperatorSession(token = null, bootstrapToken = "", password = null, storedOperatorToken = null)) } }