From 2050c9d8b2124c03d7a807c222dc8d1e4499e2ff Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Mon, 6 Jul 2026 20:01:38 +0300 Subject: [PATCH] Fix auth issues Signed-off-by: denis0001-dev --- .../kotlin/ru/fromchat/api/ApiClient.kt | 6 +- .../api/local/db/store/ProfileCache.kt | 2 +- .../ru/fromchat/api/schema/user/User.kt | 2 +- .../kotlin/ru/fromchat/ui/WelcomeScreen.kt | 61 +++++++++++-------- .../kotlin/ru/fromchat/ui/auth/AuthScreen.kt | 30 +++++---- .../ru/fromchat/ui/auth/PasswordStep.kt | 10 +-- .../ru/fromchat/ui/auth/UsernameStep.kt | 8 +-- .../ui/auth/register/ConfirmPasswordStep.kt | 6 +- .../fromchat/ui/auth/register/ProfileStep.kt | 10 +-- .../ru/fromchat/ui/components/SnackbarHost.kt | 34 +++++++++++ 10 files changed, 106 insertions(+), 63 deletions(-) diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt index 47ab6d8..304c188 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/api/ApiClient.kt @@ -1406,7 +1406,11 @@ object ApiClient { } 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() } clearLocalSession() } diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/api/local/db/store/ProfileCache.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/api/local/db/store/ProfileCache.kt index 6056f6f..3372d0b 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/api/local/db/store/ProfileCache.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/api/local/db/store/ProfileCache.kt @@ -165,7 +165,7 @@ object ProfileCache { ?: existing?.profilePicture, bio = existing?.bio, 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, verified = user.verified ?: existing?.verified, verificationStatus = user.verificationStatus ?: existing?.verificationStatus, diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/api/schema/user/User.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/api/schema/user/User.kt index d284398..93225c9 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/api/schema/user/User.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/api/schema/user/User.kt @@ -8,7 +8,7 @@ import ru.fromchat.api.schema.user.profile.VerificationStatus data class User( val id: Int, val created_at: String, - val last_seen: String, + val last_seen: String? = null, val online: Boolean, val username: String, @SerialName("display_name") val displayName: String? = null, diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/WelcomeScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/WelcomeScreen.kt index acc3e10..eef5568 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/WelcomeScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/WelcomeScreen.kt @@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size 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.outlined.BugReport import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -68,33 +70,38 @@ fun WelcomeScreen( .fillMaxSize() .padding(innerPadding), ) { - IconButton( - onClick = { menuExpanded = true }, - modifier = Modifier.align(Alignment.TopEnd), - ) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(Res.string.more), - ) - } - DropdownMenu( - expanded = menuExpanded, - onDismissRequest = { menuExpanded = false }, - ) { - DropdownMenuItem( - text = { Text(stringResource(Res.string.about)) }, - onClick = { - menuExpanded = false - navController.navigate(SettingsRoutes.About) - }, - ) - DropdownMenuItem( - text = { Text(stringResource(Res.string.logs_title)) }, - onClick = { - menuExpanded = false - navController.navigate(SettingsRoutes.Logs) - }, - ) + Box(modifier = Modifier.align(Alignment.TopEnd)) { + IconButton(onClick = { menuExpanded = true }) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = stringResource(Res.string.more), + ) + } + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.about)) }, + leadingIcon = { + Icon(Icons.Default.Info, contentDescription = null) + }, + onClick = { + menuExpanded = false + navController.navigate(SettingsRoutes.About) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(Res.string.logs_title)) }, + leadingIcon = { + Icon(Icons.Outlined.BugReport, contentDescription = null) + }, + onClick = { + menuExpanded = false + navController.navigate(SettingsRoutes.Logs) + }, + ) + } } Column( diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/AuthScreen.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/AuthScreen.kt index 2810f73..c44fe0f 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/AuthScreen.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/AuthScreen.kt @@ -3,7 +3,6 @@ package ru.fromchat.ui.auth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Storage -import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable 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.TextCta 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 kotlin.time.Duration.Companion.milliseconds @@ -56,13 +55,13 @@ internal sealed interface PasswordStepResult { data object AdvanceToRegister : PasswordStepResult data class WrongPassword(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 { data object Success : 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 @@ -150,8 +149,8 @@ private suspend fun login( else -> PasswordStepResult.Error(parseClientError(e, unexpectedError)) } -} catch (_: Exception) { - PasswordStepResult.Error(unexpectedError) +} catch (e: Exception) { + PasswordStepResult.Error(unexpectedError, e) } internal suspend fun register( @@ -186,8 +185,8 @@ internal suspend fun register( } else { RegisterResult.Error(parseClientError(e, unexpectedError)) } -} catch (_: Exception) { - RegisterResult.Error(unexpectedError) +} catch (e: Exception) { + RegisterResult.Error(unexpectedError, e) } private suspend fun isUsernameTakenError(e: ClientRequestException) = @@ -232,14 +231,13 @@ fun AuthScreen( var displayName by remember { mutableStateOf("") } var bio by remember { mutableStateOf("") } - fun snackbar(text: String) { - scope.launch { - snackbarHostState.showReplacingSnackbar( - message = text, - withDismissAction = false, - duration = SnackbarDuration.Short, - ) - } + fun snackbar(text: String, cause: Throwable? = null) { + scope.showLoggedSnackbar( + hostState = snackbarHostState, + message = text, + logTag = "Auth", + cause = cause, + ) } val resetToUsername: () -> Unit = { diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/PasswordStep.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/PasswordStep.kt index 0f1b9f0..84ec93f 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/PasswordStep.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/PasswordStep.kt @@ -54,7 +54,7 @@ internal fun passwordStepPage( onPasswordChange: (String) -> Unit, onLoginSuccess: () -> Unit, onRegister: suspend () -> Unit, - onSnackbar: (String) -> Unit, + onSnackbar: (String, Throwable?) -> Unit, ): ExpressiveStepPage { val scope = rememberCoroutineScope() var busy by remember { mutableStateOf(false) } @@ -112,7 +112,7 @@ internal fun passwordStepPage( if (busy) return@ActionButton if (password.length !in 5..50) { - onSnackbar(pwdLen) + onSnackbar(pwdLen, null) return@ActionButton } @@ -138,15 +138,15 @@ internal fun passwordStepPage( } is PasswordStepResult.WrongPassword -> { - onSnackbar(result.message) + onSnackbar(result.message, null) } is PasswordStepResult.RateLimited -> { - onSnackbar(result.message) + onSnackbar(result.message, null) } is PasswordStepResult.Error -> { - onSnackbar(result.message) + onSnackbar(result.message, result.cause) } } } finally { diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/UsernameStep.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/UsernameStep.kt index 9d6ed9f..4f13c80 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/UsernameStep.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/UsernameStep.kt @@ -45,7 +45,7 @@ internal fun usernameStepPage( username: String, onUsernameChange: (String) -> Unit, onContinue: suspend () -> Unit, - onSnackbar: (String) -> Unit, + onSnackbar: (String, Throwable?) -> Unit, ): ExpressiveStepPage { val scope = rememberCoroutineScope() val colorScheme = MaterialTheme.colorScheme @@ -95,11 +95,11 @@ internal fun usernameStepPage( if (busy) return@ActionButton val trimmed = username.trim() if (trimmed.isBlank()) { - onSnackbar(fillAll) + onSnackbar(fillAll, null) return@ActionButton } if (trimmed.length !in 3..20) { - onSnackbar(usernameLenError) + onSnackbar(usernameLenError, null) return@ActionButton } onUsernameChange(trimmed) @@ -107,7 +107,7 @@ internal fun usernameStepPage( busy = true try { if (!probeCurrentServer()) { - onSnackbar(serverFail) + onSnackbar(serverFail, null) } else { onContinue() } diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ConfirmPasswordStep.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ConfirmPasswordStep.kt index 4a6479d..af69b6d 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ConfirmPasswordStep.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ConfirmPasswordStep.kt @@ -50,7 +50,7 @@ internal fun confirmPasswordStepPage( onConfirmPasswordChange: (String) -> Unit, password: String, onContinue: suspend () -> Unit, - onSnackbar: (String) -> Unit, + onSnackbar: (String, Throwable?) -> Unit, ): ExpressiveStepPage { val scope = rememberCoroutineScope() val colorScheme = MaterialTheme.colorScheme @@ -102,12 +102,12 @@ internal fun confirmPasswordStepPage( ActionButton( onClick = { if (confirmPassword.isBlank()) { - onSnackbar(fillAll) + onSnackbar(fillAll, null) return@ActionButton } if (confirmPassword != password) { - onSnackbar(pwdMatch) + onSnackbar(pwdMatch, null) return@ActionButton } diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ProfileStep.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ProfileStep.kt index 6c10e90..10ff070 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ProfileStep.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/auth/register/ProfileStep.kt @@ -57,7 +57,7 @@ internal fun profileStepPage( password: String, onRegisterSuccess: () -> Unit, onUsernameTaken: () -> Unit, - onSnackbar: (String) -> Unit, + onSnackbar: (String, Throwable?) -> Unit, ): ExpressiveStepPage { val scope = rememberCoroutineScope() val fieldColors = expressiveStepFieldColors() @@ -128,12 +128,12 @@ internal fun profileStepPage( if (busy) return@ActionButton if (displayName.isBlank() || displayName.trim().length > DISPLAY_NAME_MAX) { - onSnackbar(displayNameError) + onSnackbar(displayNameError, null) return@ActionButton } if (bio.trim().length > BIO_MAX) { - onSnackbar(unexpected) + onSnackbar(unexpected, null) return@ActionButton } @@ -155,12 +155,12 @@ internal fun profileStepPage( } is RegisterResult.UsernameTaken -> { - onSnackbar(usernameTaken) + onSnackbar(usernameTaken, null) onUsernameTaken() } is RegisterResult.Error -> { - onSnackbar(result.message) + onSnackbar(result.message, result.cause) } } } finally { diff --git a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/SnackbarHost.kt b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/SnackbarHost.kt index c078100..b79b8c4 100644 --- a/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/SnackbarHost.kt +++ b/app/shared/src/commonMain/kotlin/ru/fromchat/ui/components/SnackbarHost.kt @@ -10,6 +10,9 @@ import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier 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. @@ -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. */ suspend fun SnackbarHostState.showReplacingSnackbar( message: String,