Fix auth issues

Signed-off-by: denis0001-dev <denis0001.dev@ya.ru>
This commit is contained in:
2026-07-06 20:01:38 +03:00
Unverified
parent dec56b3d89
commit 2050c9d8b2
10 changed files with 106 additions and 63 deletions
@@ -1406,7 +1406,11 @@ object ApiClient {
} }
suspend fun logout() { suspend fun logout() {
runCatching { http.get("${ServerConfig.apiBaseUrl}/logout") } runCatching {
http.get("${ServerConfig.apiBaseUrl}/logout")
}.onFailure { e ->
ru.fromchat.Logger.e("ApiClient", "Server logout failed", e)
}
runCatching { unregisterFcmTokenFromServer() } runCatching { unregisterFcmTokenFromServer() }
clearLocalSession() clearLocalSession()
} }
@@ -165,7 +165,7 @@ object ProfileCache {
?: existing?.profilePicture, ?: existing?.profilePicture,
bio = existing?.bio, bio = existing?.bio,
online = user.online, online = user.online,
lastSeen = user.last_seen.takeIf { it.isNotBlank() } ?: existing?.lastSeen, lastSeen = user.last_seen?.takeIf { it.isNotBlank() } ?: existing?.lastSeen,
createdAt = user.created_at.takeIf { it.isNotBlank() } ?: existing?.createdAt, createdAt = user.created_at.takeIf { it.isNotBlank() } ?: existing?.createdAt,
verified = user.verified ?: existing?.verified, verified = user.verified ?: existing?.verified,
verificationStatus = user.verificationStatus ?: existing?.verificationStatus, verificationStatus = user.verificationStatus ?: existing?.verificationStatus,
@@ -8,7 +8,7 @@ import ru.fromchat.api.schema.user.profile.VerificationStatus
data class User( data class User(
val id: Int, val id: Int,
val created_at: String, val created_at: String,
val last_seen: String, val last_seen: String? = null,
val online: Boolean, val online: Boolean,
val username: String, val username: String,
@SerialName("display_name") val displayName: String? = null, @SerialName("display_name") val displayName: String? = null,
@@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -68,10 +70,8 @@ fun WelcomeScreen(
.fillMaxSize() .fillMaxSize()
.padding(innerPadding), .padding(innerPadding),
) { ) {
IconButton( Box(modifier = Modifier.align(Alignment.TopEnd)) {
onClick = { menuExpanded = true }, IconButton(onClick = { menuExpanded = true }) {
modifier = Modifier.align(Alignment.TopEnd),
) {
Icon( Icon(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(Res.string.more), contentDescription = stringResource(Res.string.more),
@@ -83,6 +83,9 @@ fun WelcomeScreen(
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(Res.string.about)) }, text = { Text(stringResource(Res.string.about)) },
leadingIcon = {
Icon(Icons.Default.Info, contentDescription = null)
},
onClick = { onClick = {
menuExpanded = false menuExpanded = false
navController.navigate(SettingsRoutes.About) navController.navigate(SettingsRoutes.About)
@@ -90,12 +93,16 @@ fun WelcomeScreen(
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(Res.string.logs_title)) }, text = { Text(stringResource(Res.string.logs_title)) },
leadingIcon = {
Icon(Icons.Outlined.BugReport, contentDescription = null)
},
onClick = { onClick = {
menuExpanded = false menuExpanded = false
navController.navigate(SettingsRoutes.Logs) navController.navigate(SettingsRoutes.Logs)
}, },
) )
} }
}
Column( Column(
modifier = Modifier modifier = Modifier
@@ -3,7 +3,6 @@ package ru.fromchat.ui.auth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -40,7 +39,7 @@ import ru.fromchat.ui.components.ExpressiveStepFlowScaffold
import ru.fromchat.ui.components.Text import ru.fromchat.ui.components.Text
import ru.fromchat.ui.components.TextCta import ru.fromchat.ui.components.TextCta
import ru.fromchat.ui.components.rememberExpressiveStepFlow import ru.fromchat.ui.components.rememberExpressiveStepFlow
import ru.fromchat.ui.components.showReplacingSnackbar import ru.fromchat.ui.components.showLoggedSnackbar
import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding import ru.fromchat.ui.main.settings.SettingsStepHorizontalPadding
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -56,13 +55,13 @@ internal sealed interface PasswordStepResult {
data object AdvanceToRegister : PasswordStepResult data object AdvanceToRegister : PasswordStepResult
data class WrongPassword(val message: String) : PasswordStepResult data class WrongPassword(val message: String) : PasswordStepResult
data class RateLimited(val message: String) : PasswordStepResult data class RateLimited(val message: String) : PasswordStepResult
data class Error(val message: String) : PasswordStepResult data class Error(val message: String, val cause: Throwable? = null) : PasswordStepResult
} }
internal sealed interface RegisterResult { internal sealed interface RegisterResult {
data object Success : RegisterResult data object Success : RegisterResult
data object UsernameTaken : RegisterResult data object UsernameTaken : RegisterResult
data class Error(val message: String) : RegisterResult data class Error(val message: String, val cause: Throwable? = null) : RegisterResult
} }
private const val AUTH_SERVER_PROBE_TIMEOUT_MS = 5_000L private const val AUTH_SERVER_PROBE_TIMEOUT_MS = 5_000L
@@ -150,8 +149,8 @@ private suspend fun login(
else -> PasswordStepResult.Error(parseClientError(e, unexpectedError)) else -> PasswordStepResult.Error(parseClientError(e, unexpectedError))
} }
} catch (_: Exception) { } catch (e: Exception) {
PasswordStepResult.Error(unexpectedError) PasswordStepResult.Error(unexpectedError, e)
} }
internal suspend fun register( internal suspend fun register(
@@ -186,8 +185,8 @@ internal suspend fun register(
} else { } else {
RegisterResult.Error(parseClientError(e, unexpectedError)) RegisterResult.Error(parseClientError(e, unexpectedError))
} }
} catch (_: Exception) { } catch (e: Exception) {
RegisterResult.Error(unexpectedError) RegisterResult.Error(unexpectedError, e)
} }
private suspend fun isUsernameTakenError(e: ClientRequestException) = private suspend fun isUsernameTakenError(e: ClientRequestException) =
@@ -232,15 +231,14 @@ fun AuthScreen(
var displayName by remember { mutableStateOf("") } var displayName by remember { mutableStateOf("") }
var bio by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") }
fun snackbar(text: String) { fun snackbar(text: String, cause: Throwable? = null) {
scope.launch { scope.showLoggedSnackbar(
snackbarHostState.showReplacingSnackbar( hostState = snackbarHostState,
message = text, message = text,
withDismissAction = false, logTag = "Auth",
duration = SnackbarDuration.Short, cause = cause,
) )
} }
}
val resetToUsername: () -> Unit = { val resetToUsername: () -> Unit = {
username = "" username = ""
@@ -54,7 +54,7 @@ internal fun passwordStepPage(
onPasswordChange: (String) -> Unit, onPasswordChange: (String) -> Unit,
onLoginSuccess: () -> Unit, onLoginSuccess: () -> Unit,
onRegister: suspend () -> Unit, onRegister: suspend () -> Unit,
onSnackbar: (String) -> Unit, onSnackbar: (String, Throwable?) -> Unit,
): ExpressiveStepPage { ): ExpressiveStepPage {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var busy by remember { mutableStateOf(false) } var busy by remember { mutableStateOf(false) }
@@ -112,7 +112,7 @@ internal fun passwordStepPage(
if (busy) return@ActionButton if (busy) return@ActionButton
if (password.length !in 5..50) { if (password.length !in 5..50) {
onSnackbar(pwdLen) onSnackbar(pwdLen, null)
return@ActionButton return@ActionButton
} }
@@ -138,15 +138,15 @@ internal fun passwordStepPage(
} }
is PasswordStepResult.WrongPassword -> { is PasswordStepResult.WrongPassword -> {
onSnackbar(result.message) onSnackbar(result.message, null)
} }
is PasswordStepResult.RateLimited -> { is PasswordStepResult.RateLimited -> {
onSnackbar(result.message) onSnackbar(result.message, null)
} }
is PasswordStepResult.Error -> { is PasswordStepResult.Error -> {
onSnackbar(result.message) onSnackbar(result.message, result.cause)
} }
} }
} finally { } finally {
@@ -45,7 +45,7 @@ internal fun usernameStepPage(
username: String, username: String,
onUsernameChange: (String) -> Unit, onUsernameChange: (String) -> Unit,
onContinue: suspend () -> Unit, onContinue: suspend () -> Unit,
onSnackbar: (String) -> Unit, onSnackbar: (String, Throwable?) -> Unit,
): ExpressiveStepPage { ): ExpressiveStepPage {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
@@ -95,11 +95,11 @@ internal fun usernameStepPage(
if (busy) return@ActionButton if (busy) return@ActionButton
val trimmed = username.trim() val trimmed = username.trim()
if (trimmed.isBlank()) { if (trimmed.isBlank()) {
onSnackbar(fillAll) onSnackbar(fillAll, null)
return@ActionButton return@ActionButton
} }
if (trimmed.length !in 3..20) { if (trimmed.length !in 3..20) {
onSnackbar(usernameLenError) onSnackbar(usernameLenError, null)
return@ActionButton return@ActionButton
} }
onUsernameChange(trimmed) onUsernameChange(trimmed)
@@ -107,7 +107,7 @@ internal fun usernameStepPage(
busy = true busy = true
try { try {
if (!probeCurrentServer()) { if (!probeCurrentServer()) {
onSnackbar(serverFail) onSnackbar(serverFail, null)
} else { } else {
onContinue() onContinue()
} }
@@ -50,7 +50,7 @@ internal fun confirmPasswordStepPage(
onConfirmPasswordChange: (String) -> Unit, onConfirmPasswordChange: (String) -> Unit,
password: String, password: String,
onContinue: suspend () -> Unit, onContinue: suspend () -> Unit,
onSnackbar: (String) -> Unit, onSnackbar: (String, Throwable?) -> Unit,
): ExpressiveStepPage { ): ExpressiveStepPage {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
@@ -102,12 +102,12 @@ internal fun confirmPasswordStepPage(
ActionButton( ActionButton(
onClick = { onClick = {
if (confirmPassword.isBlank()) { if (confirmPassword.isBlank()) {
onSnackbar(fillAll) onSnackbar(fillAll, null)
return@ActionButton return@ActionButton
} }
if (confirmPassword != password) { if (confirmPassword != password) {
onSnackbar(pwdMatch) onSnackbar(pwdMatch, null)
return@ActionButton return@ActionButton
} }
@@ -57,7 +57,7 @@ internal fun profileStepPage(
password: String, password: String,
onRegisterSuccess: () -> Unit, onRegisterSuccess: () -> Unit,
onUsernameTaken: () -> Unit, onUsernameTaken: () -> Unit,
onSnackbar: (String) -> Unit, onSnackbar: (String, Throwable?) -> Unit,
): ExpressiveStepPage { ): ExpressiveStepPage {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val fieldColors = expressiveStepFieldColors() val fieldColors = expressiveStepFieldColors()
@@ -128,12 +128,12 @@ internal fun profileStepPage(
if (busy) return@ActionButton if (busy) return@ActionButton
if (displayName.isBlank() || displayName.trim().length > DISPLAY_NAME_MAX) { if (displayName.isBlank() || displayName.trim().length > DISPLAY_NAME_MAX) {
onSnackbar(displayNameError) onSnackbar(displayNameError, null)
return@ActionButton return@ActionButton
} }
if (bio.trim().length > BIO_MAX) { if (bio.trim().length > BIO_MAX) {
onSnackbar(unexpected) onSnackbar(unexpected, null)
return@ActionButton return@ActionButton
} }
@@ -155,12 +155,12 @@ internal fun profileStepPage(
} }
is RegisterResult.UsernameTaken -> { is RegisterResult.UsernameTaken -> {
onSnackbar(usernameTaken) onSnackbar(usernameTaken, null)
onUsernameTaken() onUsernameTaken()
} }
is RegisterResult.Error -> { is RegisterResult.Error -> {
onSnackbar(result.message) onSnackbar(result.message, result.cause)
} }
} }
} finally { } finally {
@@ -10,6 +10,9 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import ru.fromchat.Logger
/** /**
* App-wide snackbar styling: elevated surface container instead of inverse surface. * App-wide snackbar styling: elevated surface container instead of inverse surface.
@@ -38,6 +41,37 @@ fun FromChatSnackbarHost(
} }
} }
/**
* Logs why a snackbar is shown, then displays it via [showReplacingSnackbar].
* Use this instead of calling [showReplacingSnackbar] directly when the message is an error.
*/
fun CoroutineScope.showLoggedSnackbar(
hostState: SnackbarHostState,
message: String,
logTag: String,
cause: Throwable? = null,
withDismissAction: Boolean = false,
duration: SnackbarDuration = SnackbarDuration.Short,
) {
if (cause != null) {
Logger.e(
logTag,
"Snackbar: $message (${cause::class.simpleName}: ${cause.message})",
cause,
)
} else {
Logger.i(logTag, "Snackbar: $message")
}
launch {
hostState.showReplacingSnackbar(
message = message,
withDismissAction = withDismissAction,
duration = duration,
)
}
}
/** Dismisses any visible snackbar, then shows [message] without queueing behind it. */ /** Dismisses any visible snackbar, then shows [message] without queueing behind it. */
suspend fun SnackbarHostState.showReplacingSnackbar( suspend fun SnackbarHostState.showReplacingSnackbar(
message: String, message: String,