Add client-side username check in auth flow

Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
2026-07-22 18:27:04 +03:00
Unverified
parent de21d7c217
commit f022abcf2a
3 changed files with 17 additions and 2 deletions
@@ -31,6 +31,7 @@
<string name="display_name_error">От 1 до 64 символов</string> <string name="display_name_error">От 1 до 64 символов</string>
<string name="fill_all_fields">Заполните все поля</string> <string name="fill_all_fields">Заполните все поля</string>
<string name="username_length_error">Имя пользователя — от 3 до 20 символов</string> <string name="username_length_error">Имя пользователя — от 3 до 20 символов</string>
<string name="username_chars_error">Имя пользователя может содержать только английские буквы, цифры, дефисы и подчеркивания</string>
<string name="password_length_error">Пароль — от 5 до 50 символов</string> <string name="password_length_error">Пароль — от 5 до 50 символов</string>
<string name="passwords_dont_match">Пароли не совпадают</string> <string name="passwords_dont_match">Пароли не совпадают</string>
<string name="auth_welcome_title">Добро пожаловать в FromChat</string> <string name="auth_welcome_title">Добро пожаловать в FromChat</string>
@@ -38,6 +38,7 @@
<!-- Validation Errors --> <!-- Validation Errors -->
<string name="fill_all_fields">Please fill in every field</string> <string name="fill_all_fields">Please fill in every field</string>
<string name="username_length_error">Username must be 3 to 20 characters</string> <string name="username_length_error">Username must be 3 to 20 characters</string>
<string name="username_chars_error">Username can only contain English letters, numbers, hyphens and underscores</string>
<string name="password_length_error">Password must be 5 to 50 characters</string> <string name="password_length_error">Password must be 5 to 50 characters</string>
<string name="passwords_dont_match">The two passwords dont match</string> <string name="passwords_dont_match">The two passwords dont match</string>
<string name="auth_welcome_title">Welcome to FromChat</string> <string name="auth_welcome_title">Welcome to FromChat</string>
@@ -42,8 +42,13 @@ import ru.fromchat.ui.components.expressiveStepFieldColors
import ru.fromchat.ui.components.trackImeScrollTarget import ru.fromchat.ui.components.trackImeScrollTarget
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
import ru.fromchat.username import ru.fromchat.username
import ru.fromchat.username_chars_error
import ru.fromchat.username_length_error import ru.fromchat.username_length_error
/** Matches backend `is_valid_username`: English letters, digits, hyphen, underscore. */
private fun isAllowedUsernameChar(ch: Char) =
ch in 'a'..'z' || ch in 'A'..'Z' || ch in '0'..'9' || ch == '_' || ch == '-'
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
internal fun usernameStepPage( internal fun usernameStepPage(
@@ -59,9 +64,11 @@ internal fun usernameStepPage(
val fillAll = stringResource(Res.string.fill_all_fields) val fillAll = stringResource(Res.string.fill_all_fields)
val usernameLenError = stringResource(Res.string.username_length_error) val usernameLenError = stringResource(Res.string.username_length_error)
val usernameCharsError = stringResource(Res.string.username_chars_error)
val serverFail = stringResource(Res.string.auth_server_connect_failed) val serverFail = stringResource(Res.string.auth_server_connect_failed)
val unexpected = stringResource(Res.string.error_unexpected) val unexpected = stringResource(Res.string.error_unexpected)
val nextLabel = stringResource(Res.string.settings_next) val nextLabel = stringResource(Res.string.settings_next)
val hasProhibitedChars = username.trim().any { !isAllowedUsernameChar(it) }
return ExpressiveStepPage( return ExpressiveStepPage(
hero = ExpressiveHeroSpec( hero = ExpressiveHeroSpec(
@@ -90,6 +97,12 @@ internal fun usernameStepPage(
.trackImeScrollTarget(imeScroll, ExpressiveStepLazyListIndices.STEPS_BODY) .trackImeScrollTarget(imeScroll, ExpressiveStepLazyListIndices.STEPS_BODY)
.padding(horizontal = SettingsStepHorizontalPadding), .padding(horizontal = SettingsStepHorizontalPadding),
singleLine = true, singleLine = true,
isError = hasProhibitedChars,
supportingText = if (hasProhibitedChars) {
{ Text(usernameCharsError) }
} else {
null
},
colors = expressiveStepFieldColors(), colors = expressiveStepFieldColors(),
shape = SettingsPasswordOutlineFieldShape, shape = SettingsPasswordOutlineFieldShape,
) )
@@ -98,7 +111,7 @@ internal fun usernameStepPage(
button = { button = {
ActionButton( ActionButton(
onClick = { onClick = {
if (busy) return@ActionButton if (busy || hasProhibitedChars) return@ActionButton
val trimmed = username.trim() val trimmed = username.trim()
if (trimmed.isBlank()) { if (trimmed.isBlank()) {
onSnackbar(fillAll, null) onSnackbar(fillAll, null)
@@ -136,7 +149,7 @@ internal fun usernameStepPage(
} }
} }
}, },
enabled = !busy, enabled = !busy && !hasProhibitedChars,
loading = busy, loading = busy,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {