feat: add provider login flow #4

Merged
KremeCN merged 26 commits from feat/provider-login into main 2026-08-25 01:59:00 +08:00
5 changed files with 74 additions and 12 deletions
Showing only changes of commit d893a74bec - Show all commits
@@ -55,11 +55,13 @@ fun LoginRoute(
onDispose(viewModel::clearSensitiveState)
}
val state = viewModel.uiState.collectAsStateWithLifecycle().value
val errorText = state.errorMessage?.let { stringResource(it.stringResource()) }
?: actionError
?: bootstrapError
LoginScreen(
profile = profile,
state = state.copy(
errorMessage = state.errorMessage ?: actionError ?: bootstrapError,
),
state = state,
errorText = errorText,
onUsernameChange = viewModel::setUsername,
onPasswordChange = viewModel::setPassword,
onRememberUsernameChange = viewModel::setRememberUsername,
@@ -75,6 +77,7 @@ fun LoginRoute(
fun LoginScreen(
profile: ProviderProfile,
state: LoginUiState,
errorText: String? = null,
onUsernameChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onRememberUsernameChange: (Boolean) -> Unit,
@@ -174,7 +177,7 @@ fun LoginScreen(
enabled = formEnabled,
onCheckedChange = onAutoLoginChange,
)
state.errorMessage?.let { Text(it, color = MaterialTheme.colorScheme.error) }
errorText?.let { Text(it, color = MaterialTheme.colorScheme.error) }
ABoxButton(
onClick = onSubmit,
enabled = formEnabled && !switchingProvider,
@@ -210,6 +213,15 @@ private fun ToggleRow(
}
}
private fun LoginErrorMessage.stringResource(): Int = when (this) {
LoginErrorMessage.REQUIRED_FIELDS -> R.string.login_error_required
LoginErrorMessage.INVALID_CREDENTIALS -> R.string.login_error_invalid_credentials
LoginErrorMessage.NETWORK_UNAVAILABLE -> R.string.login_error_network
LoginErrorMessage.SESSION_EXPIRED -> R.string.login_error_session_expired
LoginErrorMessage.PREFERENCES_UNAVAILABLE -> R.string.login_error_preferences
LoginErrorMessage.LOGIN_FAILED -> R.string.login_error_failed
}
@Composable
private fun providerName(type: ProviderType): String = stringResource(
when (type) {
@@ -71,7 +71,7 @@ class LoginViewModel @Inject constructor(
if (profile?.id == value.id) {
_uiState.value = _uiState.value.copy(
isLoadingPreferences = false,
errorMessage = "Unable to load saved login settings",
errorMessage = LoginErrorMessage.PREFERENCES_UNAVAILABLE,
)
}
} finally {
@@ -126,7 +126,7 @@ class LoginViewModel @Inject constructor(
state.isSubmitting ||
state.isLoadingPreferences
) {
_uiState.value = state.copy(errorMessage = "Username and password are required")
_uiState.value = state.copy(errorMessage = LoginErrorMessage.REQUIRED_FIELDS)
return
}
submitJob = viewModelScope.launch {
@@ -149,14 +149,23 @@ class LoginViewModel @Inject constructor(
}
}
private fun DomainError.toUiMessage(): String = when (this) {
Outdated
Review

本地化缺失

toUiMessage() 将认证失败、网络不可用和会话过期等用户可见错误硬编码为英文,因此在 zh-rCN 环境中登录失败仍会显示英文。应返回可本地化的错误标识,并在 UI 层通过字符串资源解析。

**本地化缺失** `toUiMessage()` 将认证失败、网络不可用和会话过期等用户可见错误硬编码为英文,因此在 `zh-rCN` 环境中登录失败仍会显示英文。应返回可本地化的错误标识,并在 UI 层通过字符串资源解析。 <!-- pr-agent-dedup: 11d4b9053be7 --> <!-- pr-agent-key-issue-location: 781ccb65c991 -->
is DomainError.Authentication -> "Invalid username or password"
is DomainError.Network -> "Network unavailable"
is DomainError.TokenInvalid -> "Session expired"
else -> detail ?: "Login failed"
private fun DomainError.toUiMessage(): LoginErrorMessage = when (this) {
is DomainError.Authentication -> LoginErrorMessage.INVALID_CREDENTIALS
is DomainError.Network -> LoginErrorMessage.NETWORK_UNAVAILABLE
is DomainError.TokenInvalid -> LoginErrorMessage.SESSION_EXPIRED
else -> LoginErrorMessage.LOGIN_FAILED
}
}
enum class LoginErrorMessage {
REQUIRED_FIELDS,
INVALID_CREDENTIALS,
NETWORK_UNAVAILABLE,
SESSION_EXPIRED,
PREFERENCES_UNAVAILABLE,
LOGIN_FAILED,
}
data class LoginUiState(
val username: String = "",
val password: String = "",
@@ -164,5 +173,5 @@ data class LoginUiState(
val autoLogin: Boolean = false,
val isLoadingPreferences: Boolean = false,
val isSubmitting: Boolean = false,
val errorMessage: String? = null,
val errorMessage: LoginErrorMessage? = null,
)
@@ -16,6 +16,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -60,6 +62,33 @@ class AppSessionViewModel @Inject constructor(
)
}
}
viewModelScope.launch {
coordinator.observeActiveSession()
.map { it?.profileId }
.distinctUntilChanged()
.collect { sessionProfileId ->
if (sessionProfileId != null && bootstrapResult.value?.profileId == sessionProfileId) {
bootstrapResult.value = ProviderBootstrapResult(
profileId = sessionProfileId,
result = DomainResult.Success(null),
)
}
}
}
viewModelScope.launch {
profileStore.observeActiveProfile()
.map { it?.id }
.distinctUntilChanged()
.collect { activeProfileId ->
val currentBootstrap = bootstrapResult.value
if (currentBootstrap != null && currentBootstrap.profileId != activeProfileId) {
bootstrapResult.value = ProviderBootstrapResult(
profileId = activeProfileId,
result = DomainResult.Success(null),
)
}
}
}
}
private fun stateFor(
@@ -19,6 +19,12 @@
<string name="login_password">密码</string>
<string name="login_remember_username">记住用户名</string>
<string name="login_auto_login">自动登录</string>
<string name="login_error_required">请输入用户名和密码。</string>
<string name="login_error_invalid_credentials">用户名或密码不正确。</string>
<string name="login_error_network">网络不可用,请重试。</string>
<string name="login_error_session_expired">会话已失效,请重新登录。</string>
<string name="login_error_preferences">无法加载已保存的登录设置。</string>
<string name="login_error_failed">登录失败,请重试。</string>
<string name="action_login">登录</string>
<string name="action_logout">退出登录</string>
<string name="action_switch_provider">切换提供方</string>
@@ -19,6 +19,12 @@
<string name="login_password">Password</string>
<string name="login_remember_username">Remember username</string>
<string name="login_auto_login">Sign in automatically</string>
<string name="login_error_required">Username and password are required.</string>
<string name="login_error_invalid_credentials">Invalid username or password.</string>
<string name="login_error_network">Network unavailable. Try again.</string>
<string name="login_error_session_expired">Session expired. Sign in again.</string>
<string name="login_error_preferences">Unable to load saved login settings.</string>
<string name="login_error_failed">Sign-in failed. Try again.</string>
<string name="action_login">Sign in</string>
<string name="action_logout">Sign out</string>
<string name="action_switch_provider">Switch provider</string>