From d893a74becad827debdc97b8cad16dce1cdeeba2 Mon Sep 17 00:00:00 2001
From: KremeCN <3265494849@qq.com>
Date: Mon, 24 Aug 2026 01:27:00 +0800
Subject: [PATCH] fix: expire and localize login errors
---
.../abox/manager/ui/login/LoginScreen.kt | 20 ++++++++++---
.../abox/manager/ui/login/LoginViewModel.kt | 25 +++++++++++-----
.../manager/ui/session/AppSessionViewModel.kt | 29 +++++++++++++++++++
.../src/main/res/values-zh-rCN/strings.xml | 6 ++++
android/app/src/main/res/values/strings.xml | 6 ++++
5 files changed, 74 insertions(+), 12 deletions(-)
diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt
index daae812..704bdbd 100644
--- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt
+++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginScreen.kt
@@ -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) {
diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt
index a96a46b..5d96021 100644
--- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt
+++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/login/LoginViewModel.kt
@@ -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,
)
diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt
index 7525220..34da97a 100644
--- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt
+++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/session/AppSessionViewModel.kt
@@ -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(
diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml
index 3a812f4..90e8af8 100644
--- a/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -19,6 +19,12 @@
密码
记住用户名
自动登录
+ 请输入用户名和密码。
+ 用户名或密码不正确。
+ 网络不可用,请重试。
+ 会话已失效,请重新登录。
+ 无法加载已保存的登录设置。
+ 登录失败,请重试。
登录
退出登录
切换提供方
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 0a63752..a2b1d2d 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -19,6 +19,12 @@
Password
Remember username
Sign in automatically
+ Username and password are required.
+ Invalid username or password.
+ Network unavailable. Try again.
+ Session expired. Sign in again.
+ Unable to load saved login settings.
+ Sign-in failed. Try again.
Sign in
Sign out
Switch provider