From 546dc86a98da9c416935ae40b577e4801cccbe19 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:16:17 +0300 Subject: [PATCH] =?UTF-8?q?fix(#424):=20fully=20fix=20logout=E2=86=92login?= =?UTF-8?q?=20cycle=20=E2=80=94=20clear=20ViewModel=20state=20+=20manage?= =?UTF-8?q?=20WS=20lifecycle=20(#429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../main/java/com/m57/hermescontrol/Navigation.kt | 12 ++++++++++-- .../hermescontrol/ui/authlogin/AuthLoginScreen.kt | 8 ++++++++ .../hermescontrol/ui/authlogin/AuthLoginViewModel.kt | 7 +++++++ .../hermescontrol/ui/settings/SettingsViewModel.kt | 2 ++ 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/Navigation.kt b/app/src/main/java/com/m57/hermescontrol/Navigation.kt index f380942f..25848b12 100644 --- a/app/src/main/java/com/m57/hermescontrol/Navigation.kt +++ b/app/src/main/java/com/m57/hermescontrol/Navigation.kt @@ -66,12 +66,13 @@ private fun appEntryProvider( openDrawer: () -> Unit, ) = entryProvider { entry { + // 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, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginScreen.kt index 695bbe51..dded5df7 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginScreen.kt @@ -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( diff --git a/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginViewModel.kt index 028f5e34..e03ad361 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/authlogin/AuthLoginViewModel.kt @@ -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) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt index 8e8b4570..baadca5a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/settings/SettingsViewModel.kt @@ -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 }