fix: align session state across transitions

This commit is contained in:
2026-08-24 01:47:53 +08:00
parent d893a74bec
commit a0f55b041a
8 changed files with 26 additions and 5 deletions
@@ -16,6 +16,7 @@ import com.flagship.abox.manager.designsystem.ABoxSurface
import com.flagship.abox.manager.designsystem.ABoxTheme
import com.flagship.abox.manager.ui.authenticated.AuthenticatedPlaceholderScreen
import com.flagship.abox.manager.ui.login.LoginRoute
import com.flagship.abox.manager.ui.login.LoginViewModel
import com.flagship.abox.manager.ui.provider.ProviderSelectionRoute
import com.flagship.abox.manager.ui.session.AppSessionUiState
import com.flagship.abox.manager.ui.session.AppSessionViewModel
@@ -25,12 +26,16 @@ import com.flagship.abox.manager.ui.session.SessionActionsViewModel
fun ABoxManagerApp(
appSessionViewModel: AppSessionViewModel = hiltViewModel(),
sessionActionsViewModel: SessionActionsViewModel = hiltViewModel(),
loginViewModel: LoginViewModel = hiltViewModel(),
) {
val actionState = sessionActionsViewModel.uiState.collectAsStateWithLifecycle().value
val sessionState = appSessionViewModel.uiState.collectAsStateWithLifecycle().value
val screenKey = sessionState.screenKey()
LaunchedEffect(screenKey) {
sessionActionsViewModel.dismissError()
if (sessionState !is AppSessionUiState.Login) {
loginViewModel.clearSensitiveState()
}
}
ABoxTheme {
ABoxSurface(modifier = Modifier.fillMaxSize()) {
@@ -50,6 +55,7 @@ fun ABoxManagerApp(
actionError = actionState.errorMessage,
actionInProgress = actionState.inProgress,
onSwitchProvider = sessionActionsViewModel::switchProvider,
viewModel = loginViewModel,
)
is AppSessionUiState.Authenticated -> AuthenticatedPlaceholderScreen(
profile = state.profile,
@@ -21,7 +21,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -51,9 +50,6 @@ fun LoginRoute(
viewModel: LoginViewModel = hiltViewModel(),
) {
LaunchedEffect(profile.id) { viewModel.loadProfile(profile) }
DisposableEffect(profile.id) {
onDispose(viewModel::clearSensitiveState)
}
val state = viewModel.uiState.collectAsStateWithLifecycle().value
val errorText = state.errorMessage?.let { stringResource(it.stringResource()) }
?: actionError
@@ -98,7 +98,7 @@ class AppSessionViewModel @Inject constructor(
): AppSessionUiState = when {
profile == null -> AppSessionUiState.ProviderSelection
availability.forType(profile.type) is Availability.Disabled -> AppSessionUiState.ProviderSelection
session != null -> AppSessionUiState.Authenticated(profile)
session?.profileId == profile.id -> AppSessionUiState.Authenticated(profile)
else -> AppSessionUiState.Login(profile, bootstrapError)
}
}
@@ -40,6 +40,11 @@ class AndroidKeystoreCredentialCipher(
return cipher.doFinal(ciphertext)
}
override fun resetPermanentlyInvalidatedKey() {
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) }
if (keyStore.containsAlias(keyAlias)) keyStore.deleteEntry(keyAlias)
}
private fun secretKey(): SecretKey {
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) }
(keyStore.getKey(keyAlias, null) as? SecretKey)?.let { return it }
@@ -6,4 +6,6 @@ interface CredentialCipher {
fun encrypt(profileId: ProviderProfileId, plaintext: ByteArray): ByteArray
fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray
fun resetPermanentlyInvalidatedKey()
}
@@ -46,6 +46,9 @@ class KeystoreCredentialStore(
} catch (error: Exception) {
if (error.isPermanentCredentialFailure()) {
try {
if (error.hasPermanentlyInvalidatedKey()) {
cipher.resetPermanentlyInvalidatedKey()
}
dataStore.edit { it.remove(credentialKey(profileId)) }
DomainResult.Success(null)
} catch (cleanupError: CancellationException) {
@@ -122,6 +125,9 @@ private fun DataInputStream.readByteArray(maxSize: Int): ByteArray {
return ByteArray(length).also(::readFully)
}
private fun Throwable.hasPermanentlyInvalidatedKey(): Boolean =
this is KeyPermanentlyInvalidatedException || cause?.hasPermanentlyInvalidatedKey() == true
private fun Throwable.isPermanentCredentialFailure(): Boolean = when (this) {
is AEADBadTagException,
is KeyPermanentlyInvalidatedException,
@@ -39,5 +39,7 @@ class CredentialCleanupFailureTest {
override fun encrypt(profileId: ProviderProfileId, plaintext: ByteArray): ByteArray = plaintext
override fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray = encrypted
override fun resetPermanentlyInvalidatedKey() = Unit
}
}
@@ -69,6 +69,8 @@ class CredentialStoreTest {
override fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray {
throw IllegalArgumentException("Credential payload is truncated")
}
override fun resetPermanentlyInvalidatedKey() = Unit
}
private class ReversingCipher : CredentialCipher {
@@ -81,5 +83,7 @@ class CredentialStoreTest {
require(decoded.copyOfRange(0, delimiter).decodeToString() == profileId.value)
return decoded.copyOfRange(delimiter + 1, decoded.size)
}
override fun resetPermanentlyInvalidatedKey() = Unit
}
}