fix(#424): fully fix logout→login cycle — clear ViewModel state + manage WS lifecycle (#429)

* fix(#424): fully fix logout→login cycle — clear ViewModel state + manage WS lifecycle

The previous fix (PR #427) fixed button responsiveness after logout
but left two issues that kept login-after-logout broken:

1. Stale ViewModel state — AuthLoginViewModel is Activity-scoped,
   so  stayed  from the first login. When
   the user returned to the login screen, LaunchedEffect fired
   instantly with the stale value and bounced them to ChatScreen
   without showing the form.

   Fix: DisposableEffect(onDispose) clears connectionSuccess,
   errorMessage, and isLoading when the screen leaves composition.

2. WebSocket lifecycle — logout() never disconnected the WS, so
   it stayed open with a null/stale token. After re-login, no one
   called connect() with the fresh credentials.

   Fix: HermesWsClient.disconnect() on logout,
   HermesWsClient.connect() after successful re-login.

* fix(#424): route landing screen clicks through navigateTo

Avoid direct backStack.add() calls to prevent duplicate screen overlay and unresponsive buttons.

* fix(#424): close drawer when gestures are disabled

Ensure the drawer is closed when transitioning to LandingScreen or LoginScreen to prevent the scrim overlay from intercepting clicks.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-30 17:16:17 +03:00
1 parent d80e4dc7b1
commit 546dc86a98
4 files changed
+27 -2

No files matched your search

@@ -66,12 +66,13 @@ private fun appEntryProvider(
openDrawer: () -> Unit,
) = entryProvider {
entry<LandingScreen> {
// B7 (Jun 30 2026, kanban t_424): route landing screen buttons through navigateTo to prevent duplicate screens
LandingScreenContent(
onAuthLogin = {
NavigationController.backStack?.add(AuthLoginScreen)
NavigationController.navigateTo(AuthLoginScreen)
},
onPairingLogin = {
NavigationController.backStack?.add(PairingCodeEntryScreen)
NavigationController.navigateTo(PairingCodeEntryScreen)
},
)
}
@@ -134,6 +135,13 @@ fun MainNavigation(sessionId: String? = null) {
val gesturesEnabled = currentScreen in DRAWER_GESTURE_SCREENS
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
// B7 (Jun 30 2026, kanban t_424): close drawer if gestures are disabled to dismiss scrim
LaunchedEffect(gesturesEnabled) {
if (!gesturesEnabled && drawerState.isOpen) {
drawerState.snapTo(DrawerValue.Closed)
}
}
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = gesturesEnabled,
@@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -69,6 +70,13 @@ fun AuthLoginScreen(
}
}
// Clear ephemeral connection state when screen leaves composition
// (ViewModel is Activity-scoped so it survives navigation — without
// this, stale connectionSuccess would auto-bounce the user on re-entry)
DisposableEffect(Unit) {
onDispose { viewModel.clearConnectionState() }
}
var passwordVisible by remember { mutableStateOf(false) }
Box(
@@ -9,6 +9,7 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.data.ws.HermesWsClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -65,6 +66,11 @@ class AuthLoginViewModel(
_uiState.update { it.copy(host = value.trim(), errorMessage = null, authMode = null) }
}
/** Reset ephemeral connection state (called when screen leaves composition). */
fun clearConnectionState() {
_uiState.update { it.copy(connectionSuccess = false, errorMessage = null, isLoading = false) }
}
fun onPortChange(value: String) {
_uiState.update {
it.copy(
@@ -271,6 +277,7 @@ class AuthLoginViewModel(
AuthManager.setWsAuthParam("token")
}
ApiClient.rebuild()
HermesWsClient.connect()
_uiState.update { it.copy(isLoading = false, connectionSuccess = true) }
}
}
@@ -7,6 +7,7 @@ import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.theme.BottomNavDisplayMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
@@ -299,6 +300,7 @@ class SettingsViewModel(
AuthManager.setToken(null)
AuthManager.setSessionCookie(null)
AuthManager.setWsAuthParam("token")
HermesWsClient.disconnect()
// Don't rebuild ApiClient here — let the navigation complete first
}