feat: add provider login flow #4
@@ -55,11 +55,13 @@ fun LoginRoute(
|
|||||||
onDispose(viewModel::clearSensitiveState)
|
onDispose(viewModel::clearSensitiveState)
|
||||||
}
|
}
|
||||||
val state = viewModel.uiState.collectAsStateWithLifecycle().value
|
val state = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
|
val errorText = state.errorMessage?.let { stringResource(it.stringResource()) }
|
||||||
|
?: actionError
|
||||||
|
?: bootstrapError
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
profile = profile,
|
profile = profile,
|
||||||
state = state.copy(
|
state = state,
|
||||||
errorMessage = state.errorMessage ?: actionError ?: bootstrapError,
|
errorText = errorText,
|
||||||
),
|
|
||||||
onUsernameChange = viewModel::setUsername,
|
onUsernameChange = viewModel::setUsername,
|
||||||
onPasswordChange = viewModel::setPassword,
|
onPasswordChange = viewModel::setPassword,
|
||||||
onRememberUsernameChange = viewModel::setRememberUsername,
|
onRememberUsernameChange = viewModel::setRememberUsername,
|
||||||
@@ -75,6 +77,7 @@ fun LoginRoute(
|
|||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
profile: ProviderProfile,
|
profile: ProviderProfile,
|
||||||
state: LoginUiState,
|
state: LoginUiState,
|
||||||
|
errorText: String? = null,
|
||||||
onUsernameChange: (String) -> Unit,
|
onUsernameChange: (String) -> Unit,
|
||||||
onPasswordChange: (String) -> Unit,
|
onPasswordChange: (String) -> Unit,
|
||||||
onRememberUsernameChange: (Boolean) -> Unit,
|
onRememberUsernameChange: (Boolean) -> Unit,
|
||||||
@@ -174,7 +177,7 @@ fun LoginScreen(
|
|||||||
enabled = formEnabled,
|
enabled = formEnabled,
|
||||||
onCheckedChange = onAutoLoginChange,
|
onCheckedChange = onAutoLoginChange,
|
||||||
)
|
)
|
||||||
state.errorMessage?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
errorText?.let { Text(it, color = MaterialTheme.colorScheme.error) }
|
||||||
ABoxButton(
|
ABoxButton(
|
||||||
onClick = onSubmit,
|
onClick = onSubmit,
|
||||||
enabled = formEnabled && !switchingProvider,
|
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
|
@Composable
|
||||||
private fun providerName(type: ProviderType): String = stringResource(
|
private fun providerName(type: ProviderType): String = stringResource(
|
||||||
when (type) {
|
when (type) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
if (profile?.id == value.id) {
|
if (profile?.id == value.id) {
|
||||||
_uiState.value = _uiState.value.copy(
|
_uiState.value = _uiState.value.copy(
|
||||||
isLoadingPreferences = false,
|
isLoadingPreferences = false,
|
||||||
errorMessage = "Unable to load saved login settings",
|
errorMessage = LoginErrorMessage.PREFERENCES_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -126,7 +126,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
state.isSubmitting ||
|
state.isSubmitting ||
|
||||||
state.isLoadingPreferences
|
state.isLoadingPreferences
|
||||||
) {
|
) {
|
||||||
_uiState.value = state.copy(errorMessage = "Username and password are required")
|
_uiState.value = state.copy(errorMessage = LoginErrorMessage.REQUIRED_FIELDS)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
submitJob = viewModelScope.launch {
|
submitJob = viewModelScope.launch {
|
||||||
@@ -149,14 +149,23 @@ class LoginViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DomainError.toUiMessage(): String = when (this) {
|
private fun DomainError.toUiMessage(): LoginErrorMessage = when (this) {
|
||||||
is DomainError.Authentication -> "Invalid username or password"
|
is DomainError.Authentication -> LoginErrorMessage.INVALID_CREDENTIALS
|
||||||
is DomainError.Network -> "Network unavailable"
|
is DomainError.Network -> LoginErrorMessage.NETWORK_UNAVAILABLE
|
||||||
is DomainError.TokenInvalid -> "Session expired"
|
is DomainError.TokenInvalid -> LoginErrorMessage.SESSION_EXPIRED
|
||||||
else -> detail ?: "Login failed"
|
else -> LoginErrorMessage.LOGIN_FAILED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class LoginErrorMessage {
|
||||||
|
REQUIRED_FIELDS,
|
||||||
|
INVALID_CREDENTIALS,
|
||||||
|
NETWORK_UNAVAILABLE,
|
||||||
|
SESSION_EXPIRED,
|
||||||
|
PREFERENCES_UNAVAILABLE,
|
||||||
|
LOGIN_FAILED,
|
||||||
|
}
|
||||||
|
|
||||||
data class LoginUiState(
|
data class LoginUiState(
|
||||||
val username: String = "",
|
val username: String = "",
|
||||||
val password: String = "",
|
val password: String = "",
|
||||||
@@ -164,5 +173,5 @@ data class LoginUiState(
|
|||||||
val autoLogin: Boolean = false,
|
val autoLogin: Boolean = false,
|
||||||
val isLoadingPreferences: Boolean = false,
|
val isLoadingPreferences: Boolean = false,
|
||||||
val isSubmitting: 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.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
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(
|
private fun stateFor(
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
<string name="login_password">密码</string>
|
<string name="login_password">密码</string>
|
||||||
<string name="login_remember_username">记住用户名</string>
|
<string name="login_remember_username">记住用户名</string>
|
||||||
<string name="login_auto_login">自动登录</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_login">登录</string>
|
||||||
<string name="action_logout">退出登录</string>
|
<string name="action_logout">退出登录</string>
|
||||||
<string name="action_switch_provider">切换提供方</string>
|
<string name="action_switch_provider">切换提供方</string>
|
||||||
|
|||||||
@@ -19,6 +19,12 @@
|
|||||||
<string name="login_password">Password</string>
|
<string name="login_password">Password</string>
|
||||||
<string name="login_remember_username">Remember username</string>
|
<string name="login_remember_username">Remember username</string>
|
||||||
<string name="login_auto_login">Sign in automatically</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_login">Sign in</string>
|
||||||
<string name="action_logout">Sign out</string>
|
<string name="action_logout">Sign out</string>
|
||||||
<string name="action_switch_provider">Switch provider</string>
|
<string name="action_switch_provider">Switch provider</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user