feat: add provider login flow #4
@@ -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) {
|
||||
|
|
||||
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,
|
||||
)
|
||||
|
||||
+29
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user
本地化缺失
toUiMessage()将认证失败、网络不可用和会话过期等用户可见错误硬编码为英文,因此在zh-rCN环境中登录失败仍会显示英文。应返回可本地化的错误标识,并在 UI 层通过字符串资源解析。