fix: 401 logout loop — sync state update, prevent re-navigation

Root cause: LoginViewModel.logout() was async (viewModelScope.launch),
so _uiState.isConnected remained true after navigating to login.
LoginScreen's LaunchedEffect immediately navigated back to dashboard,
where APIs failed with 401 again → infinite loop.

Fixes:
- SessionManager: add _invalidating flag to prevent concurrent 401 handling
- AuthInterceptor: check isSessionValid before invalidating
- LoginViewModel: update _uiState synchronously in logout(), clear token
  before launching coroutine for prefs.clear()
- LoginViewModel: call SessionManager.restoreSession() after successful login
- MainActivity: remove restoreSession() from logout handler (was incorrectly
  re-validating session right after invalidation)

Bump to v1.7.1 (versionCode 10)
This commit is contained in:
2026-06-11 21:12:39 -04:00
parent 4bb383f127
commit 1885693f8d
6 changed files with 14 additions and 8 deletions

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.devlab.app"
minSdk = 26
targetSdk = 37
versionCode = 9
versionName = "1.7.0"
versionCode = 10
versionName = "1.7.1"
}
buildTypes {

View File

@@ -158,7 +158,6 @@ fun AppRoot() {
popUpTo(0) { inclusive = true }
}
SessionManager.consumeLogoutRequest()
SessionManager.restoreSession()
}
}
}

View File

@@ -11,7 +11,7 @@ class AuthInterceptor(private val token: String) : Interceptor {
.build()
val response = chain.proceed(request)
if (response.code == 401 && token.isNotBlank()) {
if (response.code == 401 && token.isNotBlank() && SessionManager.isSessionValid.value) {
SessionManager.invalidateSession()
}

View File

@@ -12,15 +12,20 @@ object SessionManager {
private val _onLogoutRequest = MutableStateFlow(false)
val onLogoutRequest: StateFlow<Boolean> = _onLogoutRequest.asStateFlow()
fun invalidateSession() {
private val _invalidating = MutableStateFlow(false)
fun invalidateSession(): Boolean {
if (_invalidating.value) return false
_invalidating.value = true
_isSessionValid.value = false
_onLogoutRequest.value = true
RetrofitClient.setToken("")
return true
}
fun restoreSession() {
_isSessionValid.value = true
_onLogoutRequest.value = false
_invalidating.value = false
}
fun consumeLogoutRequest() {

View File

@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.devlab.app.ServerManagerApp
import com.devlab.app.data.api.RetrofitClient
import com.devlab.app.data.api.SessionManager
import com.devlab.app.data.repository.AuthRepository
import com.devlab.app.util.PreferencesManager
import kotlinx.coroutines.flow.MutableStateFlow
@@ -70,6 +71,7 @@ class LoginViewModel : ViewModel() {
loginResult.token, loginResult.userId,
loginResult.username, loginResult.email, loginResult.role
)
SessionManager.restoreSession()
_uiState.value = _uiState.value.copy(
isLoading = false,
isConnected = true,
@@ -87,10 +89,10 @@ class LoginViewModel : ViewModel() {
}
fun logout() {
_uiState.value = LoginUiState()
RetrofitClient.setToken("")
viewModelScope.launch {
prefs.clear()
RetrofitClient.setToken("")
_uiState.value = LoginUiState()
}
}
}

Binary file not shown.