fix: secure login route lifecycle

This commit is contained in:
2026-08-23 23:45:39 +08:00
parent 7cb0480a7a
commit 8788fe6ec5
3 changed files with 18 additions and 3 deletions
@@ -21,13 +21,17 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -47,6 +51,9 @@ fun LoginRoute(
viewModel: LoginViewModel = hiltViewModel(), viewModel: LoginViewModel = hiltViewModel(),
) { ) {
LaunchedEffect(profile.id) { viewModel.loadProfile(profile) } LaunchedEffect(profile.id) { viewModel.loadProfile(profile) }
DisposableEffect(profile.id) {
onDispose(viewModel::clearSensitiveState)
}
val state = viewModel.uiState.collectAsStateWithLifecycle().value val state = viewModel.uiState.collectAsStateWithLifecycle().value
LoginScreen( LoginScreen(
profile = profile, profile = profile,
@@ -146,6 +153,11 @@ fun LoginScreen(
onValueChange = onPasswordChange, onValueChange = onPasswordChange,
label = { Text(stringResource(R.string.login_password)) }, label = { Text(stringResource(R.string.login_password)) },
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
autoCorrect = false,
),
enabled = formEnabled, enabled = formEnabled,
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -43,12 +43,12 @@ class ProviderProfileStoreImpl(
override fun observeLoginPreferences(profileId: ProviderProfileId): Flow<LoginPreferences> = override fun observeLoginPreferences(profileId: ProviderProfileId): Flow<LoginPreferences> =
safeData.map { preferencesFrom(it, profileId) } safeData.map { preferencesFrom(it, profileId) }
override suspend fun profiles(): List<ProviderProfile> = profilesFrom(dataStore.data.first()) override suspend fun profiles(): List<ProviderProfile> = profilesFrom(safeData.first())
override suspend fun activeProfile(): ProviderProfile? = activeProfileFrom(dataStore.data.first()) override suspend fun activeProfile(): ProviderProfile? = activeProfileFrom(safeData.first())
override suspend fun loginPreferences(profileId: ProviderProfileId): LoginPreferences = override suspend fun loginPreferences(profileId: ProviderProfileId): LoginPreferences =
preferencesFrom(dataStore.data.first(), profileId) preferencesFrom(safeData.first(), profileId)
override suspend fun saveProfile(profile: ProviderProfile): DomainResult<Unit> = operation { override suspend fun saveProfile(profile: ProviderProfile): DomainResult<Unit> = operation {
dataStore.edit { preferences -> dataStore.edit { preferences ->
@@ -19,6 +19,9 @@ class ProviderProfileStoreFailureTest {
assertEquals(emptyList<Any>(), store.observeProfiles().first()) assertEquals(emptyList<Any>(), store.observeProfiles().first())
assertEquals(null, store.observeActiveProfile().first()) assertEquals(null, store.observeActiveProfile().first())
assertEquals(emptyList<Any>(), store.profiles())
assertEquals(null, store.activeProfile())
assertEquals(false, store.loginPreferences(com.flagship.abox.manager.domain.ProviderProfileId("profile")).autoLoginEnabled)
} }
private class FailingReadDataStore : DataStore<Preferences> { private class FailingReadDataStore : DataStore<Preferences> {