feat: add provider login flow #4
@@ -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) }
|
||||||
|
pr-agent-bot
commented
状态失效 切换提供方前立即执行了 **状态失效**
切换提供方前立即执行了 `clearSensitiveState()`。当 `switchProvider()` 因 DataStore 写入等原因失败且活动提供方未改变时,`LaunchedEffect(profile.id)` 的键也不会变化,因此不会重新加载;`LoginViewModel` 内部的 `profile` 已被置空,之后点击登录会在 `submit()` 中静默返回。应仅在切换成功后清理,或在失败时恢复并重新加载当前 profile。
<!-- pr-agent-dedup: 6eb79e02e8da -->
<!-- pr-agent-key-issue-location: 18d2c966c6d1 -->
|
|||||||
|
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(),
|
||||||
|
|||||||
+3
-3
@@ -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())
|
||||||
|
pr-agent-bot
commented
读取容错 一次性读取 **读取容错**
一次性读取 `profiles()`、`activeProfile()` 和 `loginPreferences()` 时直接收集 `dataStore.data`,绕过了本类已有的 `safeData` 重试和 IOException 降级。存储首次读取发生临时 `IOException` 时,登录偏好加载会异常退出;`LoginViewModel` 未捕获该异常且仍保持 `isLoadingPreferences=true`,导致登录表单和返回操作持续禁用,默认协程异常处理下还可能使应用崩溃。请改用 `safeData.first()` 或在调用层恢复状态。
<!-- pr-agent-dedup: 0d6f549115d7 -->
<!-- pr-agent-key-issue-location: 9433a5573de7 -->
|
|||||||
|
|
||||||
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 ->
|
||||||
|
|||||||
+3
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user
敏感残留
LoginRoute直接调用hiltViewModel(),在当前没有导航条目作为 owner 的结构下,LoginViewModel会由 Activity 持有。切换提供方离开登录界面时不会清除它,而新增的clearSensitiveState()也没有调用点,因此已输入的明文密码会在提供方选择页期间继续保存在内存中,直到再次加载配置或 Activity 销毁。请在路由退出时显式清理敏感状态,或将 ViewModel 作用域绑定到登录路由。