* 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:
1 parent
d80e4dc7b1
commit
546dc86a98
4 files changed
+27
-2
No files matched your search
@@ -66,12 +66,13 @@ private fun appEntryProvider(
|
|||||||
openDrawer: () -> Unit,
|
openDrawer: () -> Unit,
|
||||||
) = entryProvider {
|
) = entryProvider {
|
||||||
entry<LandingScreen> {
|
entry<LandingScreen> {
|
||||||
|
// B7 (Jun 30 2026, kanban t_424): route landing screen buttons through navigateTo to prevent duplicate screens
|
||||||
LandingScreenContent(
|
LandingScreenContent(
|
||||||
onAuthLogin = {
|
onAuthLogin = {
|
||||||
NavigationController.backStack?.add(AuthLoginScreen)
|
NavigationController.navigateTo(AuthLoginScreen)
|
||||||
},
|
},
|
||||||
onPairingLogin = {
|
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 gesturesEnabled = currentScreen in DRAWER_GESTURE_SCREENS
|
||||||
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
|
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(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
gesturesEnabled = gesturesEnabled,
|
gesturesEnabled = gesturesEnabled,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.m57.hermescontrol.R
|
|||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||||
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -65,6 +66,11 @@ class AuthLoginViewModel(
|
|||||||
_uiState.update { it.copy(host = value.trim(), errorMessage = null, authMode = null) }
|
_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) {
|
fun onPortChange(value: String) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -271,6 +277,7 @@ class AuthLoginViewModel(
|
|||||||
AuthManager.setWsAuthParam("token")
|
AuthManager.setWsAuthParam("token")
|
||||||
}
|
}
|
||||||
ApiClient.rebuild()
|
ApiClient.rebuild()
|
||||||
|
HermesWsClient.connect()
|
||||||
_uiState.update { it.copy(isLoading = false, connectionSuccess = true) }
|
_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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.BottomNavDisplayMode
|
||||||
import com.m57.hermescontrol.theme.ThemePreference
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
import com.m57.hermescontrol.theme.ThemePreset
|
import com.m57.hermescontrol.theme.ThemePreset
|
||||||
@@ -299,6 +300,7 @@ class SettingsViewModel(
|
|||||||
AuthManager.setToken(null)
|
AuthManager.setToken(null)
|
||||||
AuthManager.setSessionCookie(null)
|
AuthManager.setSessionCookie(null)
|
||||||
AuthManager.setWsAuthParam("token")
|
AuthManager.setWsAuthParam("token")
|
||||||
|
HermesWsClient.disconnect()
|
||||||
// Don't rebuild ApiClient here — let the navigation complete first
|
// Don't rebuild ApiClient here — let the navigation complete first
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user