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() {
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()
}
@@ -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,
@@ -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,
@@ -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(
@@ -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 = {
@@ -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 {
@@ -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()
}
@@ -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
}
@@ -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 {
@@ -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,