feat(auth): add user profile, phone login, and account deletion

This commit is contained in:
iu
2026-10-12 14:26:32 +03:00
parent 1cb9aaf198
commit 86aff695df
46 changed files with 2893 additions and 134 deletions
@@ -18,6 +18,7 @@ import com.galo.android.data.prefs.SettingsState
import com.galo.android.ui.album.AlbumScreen
import com.galo.android.ui.albums.AlbumsScreen
import com.galo.android.ui.auth.AuthScreen
import com.galo.android.ui.profile.ProfileScreen
import com.galo.android.ui.setup.SetupScreen
import com.galo.android.ui.upload.UploadScreen
import com.galo.android.ui.viewer.ViewerScreen
@@ -27,6 +28,7 @@ object Routes {
const val AUTH = "auth"
const val ALBUMS = "albums"
const val UPLOADS = "uploads"
const val PROFILE = "profile"
const val ALBUM = "album/{albumId}"
const val VIEWER = "album/{albumId}/viewer?index={index}"
@@ -82,6 +84,7 @@ fun GaloNavHost(app: GaloApplication) {
factory = factory,
onOpenAlbum = { id -> navController.navigate(Routes.album(id)) },
onOpenUploads = { navController.navigate(Routes.UPLOADS) },
onOpenProfile = { navController.navigate(Routes.PROFILE) },
onLoggedOut = {
navController.navigate(Routes.AUTH) {
popUpTo(Routes.ALBUMS) { inclusive = true }
@@ -89,6 +92,17 @@ fun GaloNavHost(app: GaloApplication) {
},
)
}
composable(Routes.PROFILE) {
ProfileScreen(
factory = factory,
onLoggedOut = {
navController.navigate(Routes.AUTH) {
popUpTo(Routes.ALBUMS) { inclusive = true }
}
},
onBack = { navController.popBackStack() },
)
}
composable(Routes.UPLOADS) {
UploadScreen(
factory = factory,
@@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModelProvider
import com.galo.android.GaloApplication
import com.galo.android.ui.albums.AlbumsViewModel
import com.galo.android.ui.auth.AuthViewModel
import com.galo.android.ui.profile.ProfileViewModel
import com.galo.android.ui.setup.SetupViewModel
import com.galo.android.ui.upload.UploadViewModel
@@ -19,6 +20,8 @@ class GaloViewModelFactory(private val app: GaloApplication) : ViewModelProvider
modelClass.isAssignableFrom(AuthViewModel::class.java) -> AuthViewModel(container)
modelClass.isAssignableFrom(AlbumsViewModel::class.java) -> AlbumsViewModel(container)
modelClass.isAssignableFrom(UploadViewModel::class.java) -> UploadViewModel(container)
modelClass.isAssignableFrom(ProfileViewModel::class.java) ->
ProfileViewModel(container.api, container.tokenStore)
else -> throw IllegalArgumentException("Unknown ViewModel: ${modelClass.name}")
}
return viewModel as T
@@ -15,6 +15,7 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
@@ -50,6 +51,7 @@ fun AlbumsScreen(
factory: GaloViewModelFactory,
onOpenAlbum: (String) -> Unit,
onOpenUploads: () -> Unit,
onOpenProfile: () -> Unit,
onLoggedOut: () -> Unit,
) {
val viewModel: AlbumsViewModel = viewModel(factory = factory)
@@ -58,6 +60,7 @@ fun AlbumsScreen(
state = state,
onOpenAlbum = onOpenAlbum,
onOpenUploads = onOpenUploads,
onOpenProfile = onOpenProfile,
onRefresh = viewModel::refresh,
onLogout = { viewModel.logout(onLoggedOut) },
)
@@ -69,6 +72,7 @@ fun AlbumsContent(
state: AlbumsViewModel.UiState,
onOpenAlbum: (String) -> Unit,
onOpenUploads: () -> Unit,
onOpenProfile: () -> Unit,
onRefresh: () -> Unit,
onLogout: () -> Unit,
) {
@@ -77,6 +81,9 @@ fun AlbumsContent(
TopAppBar(
title = { Text("Альбомы") },
actions = {
IconButton(onClick = onOpenProfile) {
Icon(Icons.Filled.Person, contentDescription = "Профиль")
}
IconButton(onClick = onOpenUploads) {
Icon(Icons.Filled.CloudUpload, contentDescription = "Загрузки")
}
@@ -22,10 +22,13 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.galo.android.ui.GaloViewModelFactory
const val TAG_AUTH_IDENTIFIER = "auth_identifier"
const val TAG_AUTH_EMAIL = "auth_email"
const val TAG_AUTH_PHONE = "auth_phone"
const val TAG_AUTH_PASSWORD = "auth_password"
const val TAG_AUTH_NAME = "auth_name"
const val TAG_AUTH_SUBMIT = "auth_submit"
const val TAG_AUTH_TOGGLE = "auth_toggle"
@Composable
fun AuthScreen(
@@ -39,6 +42,37 @@ fun AuthScreen(
viewModel.authenticated.collect { onAuthenticated() }
}
AuthContent(
state = state,
onIdentifierChange = viewModel::onIdentifierChange,
onEmailChange = viewModel::onEmailChange,
onPhoneChange = viewModel::onPhoneChange,
onPasswordChange = viewModel::onPasswordChange,
onNameChange = viewModel::onNameChange,
onSubmit = viewModel::submit,
onToggleMode = {
viewModel.setMode(
if (state.mode == AuthViewModel.Mode.LOGIN) {
AuthViewModel.Mode.REGISTER
} else {
AuthViewModel.Mode.LOGIN
},
)
},
)
}
@Composable
fun AuthContent(
state: AuthViewModel.UiState,
onIdentifierChange: (String) -> Unit,
onEmailChange: (String) -> Unit,
onPhoneChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onNameChange: (String) -> Unit,
onSubmit: () -> Unit,
onToggleMode: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
@@ -52,23 +86,44 @@ fun AuthScreen(
if (state.mode == AuthViewModel.Mode.REGISTER) {
OutlinedTextField(
value = state.name,
onValueChange = viewModel::onNameChange,
onValueChange = onNameChange,
label = { Text("Имя") },
singleLine = true,
modifier = Modifier.fillMaxWidth().testTag(TAG_AUTH_NAME),
)
OutlinedTextField(
value = state.email,
onValueChange = onEmailChange,
label = { Text("Email (необязательно)") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth().testTag(TAG_AUTH_EMAIL),
)
OutlinedTextField(
value = state.phone,
onValueChange = onPhoneChange,
label = { Text("Телефон (необязательно)") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier.fillMaxWidth().testTag(TAG_AUTH_PHONE),
)
Text(
"Нужен хотя бы один из email или телефона",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
OutlinedTextField(
value = state.identifier,
onValueChange = onIdentifierChange,
label = { Text("Email или телефон") },
singleLine = true,
modifier = Modifier.fillMaxWidth().testTag(TAG_AUTH_IDENTIFIER),
)
}
OutlinedTextField(
value = state.email,
onValueChange = viewModel::onEmailChange,
label = { Text("Email") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth().testTag(TAG_AUTH_EMAIL),
)
OutlinedTextField(
value = state.password,
onValueChange = viewModel::onPasswordChange,
onValueChange = onPasswordChange,
label = { Text("Пароль") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
@@ -79,7 +134,7 @@ fun AuthScreen(
Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyMedium)
}
Button(
onClick = viewModel::submit,
onClick = onSubmit,
enabled = !state.loading,
modifier = Modifier.fillMaxWidth().testTag(TAG_AUTH_SUBMIT),
) {
@@ -93,15 +148,8 @@ fun AuthScreen(
}
Row {
TextButton(
onClick = {
viewModel.setMode(
if (state.mode == AuthViewModel.Mode.LOGIN) {
AuthViewModel.Mode.REGISTER
} else {
AuthViewModel.Mode.LOGIN
},
)
},
onClick = onToggleMode,
modifier = Modifier.testTag(TAG_AUTH_TOGGLE),
) {
Text(
if (state.mode == AuthViewModel.Mode.LOGIN) {
@@ -18,7 +18,9 @@ class AuthViewModel(private val container: AppContainer) : ViewModel() {
data class UiState(
val mode: Mode = Mode.LOGIN,
val identifier: String = "",
val email: String = "",
val phone: String = "",
val password: String = "",
val name: String = "",
val loading: Boolean = false,
@@ -31,8 +33,13 @@ class AuthViewModel(private val container: AppContainer) : ViewModel() {
private val _authenticated = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val authenticated = _authenticated.asSharedFlow()
fun onIdentifierChange(value: String) =
_state.update { it.copy(identifier = value, error = null) }
fun onEmailChange(value: String) = _state.update { it.copy(email = value, error = null) }
fun onPhoneChange(value: String) = _state.update { it.copy(phone = value, error = null) }
fun onPasswordChange(value: String) = _state.update { it.copy(password = value, error = null) }
fun onNameChange(value: String) = _state.update { it.copy(name = value, error = null) }
@@ -41,25 +48,30 @@ class AuthViewModel(private val container: AppContainer) : ViewModel() {
fun submit() {
val current = _state.value
val identifier = current.identifier.trim()
val email = current.email.trim()
when {
email.isEmpty() || current.password.isEmpty() -> {
_state.update { it.copy(error = "Заполните email и пароль") }
return
}
val phone = current.phone.trim()
current.mode == Mode.REGISTER && current.name.trim().isEmpty() -> {
_state.update { it.copy(error = "Укажите имя") }
return
}
val validationError = when {
current.password.isEmpty() -> "Введите пароль"
current.mode == Mode.LOGIN && identifier.isEmpty() -> "Введите email или телефон"
current.mode == Mode.REGISTER && email.isEmpty() && phone.isEmpty() ->
"Укажите email или телефон"
current.mode == Mode.REGISTER && current.name.trim().isEmpty() -> "Укажите имя"
else -> null
}
if (validationError != null) {
_state.update { it.copy(error = validationError) }
return
}
_state.update { it.copy(loading = true, error = null) }
viewModelScope.launch {
try {
val tokens = when (current.mode) {
Mode.LOGIN -> container.api.login(email, current.password)
Mode.REGISTER -> container.api.register(email, current.password, current.name.trim())
Mode.LOGIN -> container.api.login(identifier, current.password)
Mode.REGISTER ->
container.api.register(email, phone, current.password, current.name.trim())
}
container.tokenStore.save(tokens.accessToken, tokens.refreshToken)
_state.update { it.copy(loading = false) }
@@ -0,0 +1,246 @@
package com.galo.android.ui.profile
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.galo.android.ui.GaloViewModelFactory
const val TAG_PROFILE_EMAIL = "profile_email"
const val TAG_PROFILE_PHONE = "profile_phone"
const val TAG_PROFILE_NAME = "profile_name"
const val TAG_PROFILE_SAVE_NAME = "profile_save_name"
const val TAG_PROFILE_NEW_EMAIL = "profile_new_email"
const val TAG_PROFILE_NEW_PHONE = "profile_new_phone"
const val TAG_PROFILE_ADD_ID = "profile_add_identifier"
const val TAG_PROFILE_CURRENT_PASSWORD = "profile_current_password"
const val TAG_PROFILE_NEW_PASSWORD = "profile_new_password"
const val TAG_PROFILE_CHANGE_PASSWORD = "profile_change_password"
const val TAG_PROFILE_DELETE_PASSWORD = "profile_delete_password"
const val TAG_PROFILE_DELETE = "profile_delete"
@Composable
fun ProfileScreen(
factory: GaloViewModelFactory,
onLoggedOut: () -> Unit,
onBack: () -> Unit,
) {
val viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = factory)
val state by viewModel.state.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.loggedOut.collect { onLoggedOut() }
}
ProfileContent(
state = state,
onNameChange = viewModel::onNameChange,
onSaveName = viewModel::saveName,
onNewEmailChange = viewModel::onNewEmailChange,
onNewPhoneChange = viewModel::onNewPhoneChange,
onAddIdentifier = viewModel::addIdentifier,
onCurrentPasswordChange = viewModel::onCurrentPasswordChange,
onNewPasswordChange = viewModel::onNewPasswordChange,
onChangePassword = viewModel::changePassword,
onDeletePasswordChange = viewModel::onDeletePasswordChange,
onDeleteAccount = viewModel::deleteAccount,
onBack = onBack,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProfileContent(
state: ProfileViewModel.UiState,
onNameChange: (String) -> Unit,
onSaveName: () -> Unit,
onNewEmailChange: (String) -> Unit,
onNewPhoneChange: (String) -> Unit,
onAddIdentifier: () -> Unit,
onCurrentPasswordChange: (String) -> Unit,
onNewPasswordChange: (String) -> Unit,
onChangePassword: () -> Unit,
onDeletePasswordChange: (String) -> Unit,
onDeleteAccount: () -> Unit,
onBack: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Профиль") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Назад")
}
},
)
},
) { padding ->
if (state.loading) {
Column(
modifier = Modifier.fillMaxSize().padding(padding),
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CircularProgressIndicator()
}
return@Scaffold
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text("Идентификаторы", style = MaterialTheme.typography.titleMedium)
Text(
"Email: ${state.email ?: "—"}",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.testTag(TAG_PROFILE_EMAIL),
)
Text(
"Телефон: ${state.phone ?: "—"}",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.testTag(TAG_PROFILE_PHONE),
)
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Имя", style = MaterialTheme.typography.titleMedium)
OutlinedTextField(
value = state.name,
onValueChange = onNameChange,
label = { Text("Отображаемое имя") },
singleLine = true,
modifier = Modifier.fillMaxWidth().testTag(TAG_PROFILE_NAME),
)
Button(
onClick = onSaveName,
modifier = Modifier.testTag(TAG_PROFILE_SAVE_NAME),
) {
Text("Сохранить")
}
}
if (state.email.isNullOrBlank() || state.phone.isNullOrBlank()) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Добавить идентификатор", style = MaterialTheme.typography.titleMedium)
if (state.email.isNullOrBlank()) {
OutlinedTextField(
value = state.newEmail,
onValueChange = onNewEmailChange,
label = { Text("Email") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
modifier = Modifier.fillMaxWidth().testTag(TAG_PROFILE_NEW_EMAIL),
)
}
if (state.phone.isNullOrBlank()) {
OutlinedTextField(
value = state.newPhone,
onValueChange = onNewPhoneChange,
label = { Text("Телефон (E.164)") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
modifier = Modifier.fillMaxWidth().testTag(TAG_PROFILE_NEW_PHONE),
)
}
Button(
onClick = onAddIdentifier,
modifier = Modifier.testTag(TAG_PROFILE_ADD_ID),
) {
Text("Добавить")
}
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Смена пароля", style = MaterialTheme.typography.titleMedium)
OutlinedTextField(
value = state.currentPassword,
onValueChange = onCurrentPasswordChange,
label = { Text("Текущий пароль") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth().testTag(TAG_PROFILE_CURRENT_PASSWORD),
)
OutlinedTextField(
value = state.newPassword,
onValueChange = onNewPasswordChange,
label = { Text("Новый пароль") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth().testTag(TAG_PROFILE_NEW_PASSWORD),
)
Button(
onClick = onChangePassword,
modifier = Modifier.testTag(TAG_PROFILE_CHANGE_PASSWORD),
) {
Text("Сменить пароль")
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
"Удаление аккаунта",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error,
)
OutlinedTextField(
value = state.deletePassword,
onValueChange = onDeletePasswordChange,
label = { Text("Подтвердите пароль") },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth().testTag(TAG_PROFILE_DELETE_PASSWORD),
)
OutlinedButton(
onClick = onDeleteAccount,
modifier = Modifier.testTag(TAG_PROFILE_DELETE),
) {
Text("Удалить аккаунт")
}
}
state.error?.let {
Text(it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodyMedium)
}
state.notice?.let {
Text(it, color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.bodyMedium)
}
}
}
}
@@ -0,0 +1,170 @@
package com.galo.android.ui.profile
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.galo.android.data.auth.TokenStore
import com.galo.android.data.network.ErrorMapper
import com.galo.android.data.network.ProfileApi
import com.galo.android.domain.model.ApiException
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
* Drives the profile screen. Password change and account deletion clear the
* local session and signal [loggedOut] so the caller returns to the auth screen.
*/
class ProfileViewModel(
private val api: ProfileApi,
private val tokenStore: TokenStore,
) : ViewModel() {
data class UiState(
val loading: Boolean = true,
val name: String = "",
val email: String? = null,
val phone: String? = null,
val newEmail: String = "",
val newPhone: String = "",
val currentPassword: String = "",
val newPassword: String = "",
val deletePassword: String = "",
val error: String? = null,
val notice: String? = null,
)
private val _state = MutableStateFlow(UiState())
val state = _state.asStateFlow()
private val _loggedOut = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val loggedOut = _loggedOut.asSharedFlow()
init {
refresh()
}
fun refresh() {
viewModelScope.launch {
_state.update { it.copy(loading = true, error = null) }
try {
val user = api.me()
_state.update {
it.copy(loading = false, name = user.name, email = user.email, phone = user.phone)
}
} catch (e: ApiException) {
_state.update { it.copy(loading = false, error = ErrorMapper.userMessage(e.error)) }
} catch (e: Exception) {
_state.update { it.copy(loading = false, error = e.message ?: "Не удалось загрузить профиль") }
}
}
}
fun onNameChange(value: String) = _state.update { it.copy(name = value, error = null, notice = null) }
fun onNewEmailChange(value: String) =
_state.update { it.copy(newEmail = value, error = null, notice = null) }
fun onNewPhoneChange(value: String) =
_state.update { it.copy(newPhone = value, error = null, notice = null) }
fun onCurrentPasswordChange(value: String) =
_state.update { it.copy(currentPassword = value, error = null) }
fun onNewPasswordChange(value: String) =
_state.update { it.copy(newPassword = value, error = null) }
fun onDeletePasswordChange(value: String) =
_state.update { it.copy(deletePassword = value, error = null) }
fun saveName() {
val name = _state.value.name.trim()
if (name.isEmpty()) {
_state.update { it.copy(error = "Укажите имя") }
return
}
mutate(notice = "Имя сохранено") { api.updateProfile(name) }
}
fun addIdentifier() {
val current = _state.value
val email = current.newEmail.trim()
val phone = current.newPhone.trim()
if (email.isEmpty() && phone.isEmpty()) {
_state.update { it.copy(error = "Укажите email или телефон") }
return
}
viewModelScope.launch {
_state.update { it.copy(error = null, notice = null) }
try {
val user = api.addIdentifier(email, phone)
_state.update {
it.copy(email = user.email, phone = user.phone, newEmail = "", newPhone = "", notice = "Идентификатор добавлен")
}
} catch (e: ApiException) {
_state.update { it.copy(error = ErrorMapper.userMessage(e.error)) }
} catch (e: Exception) {
_state.update { it.copy(error = e.message ?: "Не удалось добавить идентификатор") }
}
}
}
fun changePassword() {
val current = _state.value
if (current.newPassword.length < 8) {
_state.update { it.copy(error = "Пароль должен быть не короче 8 символов") }
return
}
viewModelScope.launch {
_state.update { it.copy(error = null) }
try {
api.changePassword(current.currentPassword, current.newPassword)
logout()
} catch (e: ApiException) {
_state.update { it.copy(error = ErrorMapper.userMessage(e.error)) }
} catch (e: Exception) {
_state.update { it.copy(error = e.message ?: "Не удалось сменить пароль") }
}
}
}
fun deleteAccount() {
val password = _state.value.deletePassword
if (password.isEmpty()) {
_state.update { it.copy(error = "Введите пароль") }
return
}
viewModelScope.launch {
_state.update { it.copy(error = null) }
try {
api.deleteAccount(password)
logout()
} catch (e: ApiException) {
_state.update { it.copy(error = ErrorMapper.userMessage(e.error)) }
} catch (e: Exception) {
_state.update { it.copy(error = e.message ?: "Не удалось удалить аккаунт") }
}
}
}
private fun logout() {
tokenStore.clear()
_loggedOut.tryEmit(Unit)
}
private inline fun mutate(notice: String, crossinline block: suspend () -> com.galo.android.data.network.dto.UserDto) {
viewModelScope.launch {
_state.update { it.copy(error = null, notice = null) }
try {
val user = block()
_state.update { it.copy(name = user.name, email = user.email, phone = user.phone, notice = notice) }
} catch (e: ApiException) {
_state.update { it.copy(error = ErrorMapper.userMessage(e.error)) }
} catch (e: Exception) {
_state.update { it.copy(error = e.message ?: "Ошибка сохранения") }
}
}
}
}