fix(android): restore setup-code operator bootstrap connect

This commit is contained in:
Ayaan Zaidi 2026-03-30 18:58:45 +05:30
parent 2dced6b4a0
commit deead11dcd
No known key found for this signature in database
4 changed files with 29 additions and 28 deletions

View File

@ -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)
}

View File

@ -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<String> = prefs.lastDiscoveredStableId
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val notificationForwardingEnabled: StateFlow<Boolean> = 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 {

View File

@ -228,13 +228,8 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
var manualTls by rememberSaveable { mutableStateOf(false) }
var gatewayError by rememberSaveable { mutableStateOf<String?>(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 =

View File

@ -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))
}
}