feat: edit auth screen

This commit is contained in:
2025-11-30 22:22:20 +03:00
parent 783f25ced6
commit dbc4830418
6 changed files with 135 additions and 27 deletions

View File

@@ -4,7 +4,6 @@ import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.Dispatchers
@@ -29,14 +28,21 @@ object NetworkDataSource {
}
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())
HttpStatusCode.Unauthorized -> error("Код не существует")
HttpStatusCode.BadRequest -> error("Что-то пошло не так")
else -> error("Неизвестная ошибка: ${response.status}")
}
}.mapCatching { success ->
success
}.recoverCatching { _ ->
throw Exception("Не удалось соединиться с сервером")
}
}
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
}

View File

@@ -31,6 +31,12 @@ import androidx.navigation.NavController
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.MainScreenDestination
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@Composable
fun AuthScreen(
@@ -48,50 +54,89 @@ fun AuthScreen(
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 24.dp),
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
when (val currentState = state) {
is AuthState.Data -> Content(viewModel, currentState)
when (state) {
is AuthState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
CircularProgressIndicator(modifier = Modifier.size(64.dp))
}
else -> {
Content(viewModel, state)
}
}
}
}
@Composable
private fun Content(
viewModel: AuthViewModel,
state: AuthState.Data
state: AuthState
) {
var inputText by remember { mutableStateOf("") }
val isButtonEnabled =
inputText.length == 4 && inputText.all { it.isLetterOrDigit() }
Text(
text = stringResource(R.string.auth_title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(16.dp))
TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Auth.CODE_INPUT),
value = inputText,
onValueChange = {
inputText = it
if (it.length <= 4 && it.all { ch -> ch.isLetterOrDigit() }) {
inputText = it
}
viewModel.onIntent(AuthIntent.TextInput(it))
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
viewModel.onIntent(AuthIntent.Send(inputText))
}
),
singleLine = true,
label = { Text(stringResource(R.string.auth_label)) }
)
Spacer(modifier = Modifier.size(12.dp))
AnimatedVisibility(
visible = state is AuthState.Error,
enter = fadeIn(),
exit = fadeOut()
) {
Text(
text = (state as AuthState.Error).message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
}
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Auth.SIGN_BUTTON),
onClick = {
viewModel.onIntent(AuthIntent.Send(inputText))
},
enabled = true
enabled = isButtonEnabled
) {
Text(stringResource(R.string.auth_sign_in))
}

View File

@@ -1,6 +1,7 @@
package ru.myitschool.work.ui.screen.auth
sealed interface AuthState {
object Loading: AuthState
object Data: AuthState
sealed class AuthState {
data object Data : AuthState()
data object Loading : AuthState()
data class Error(val message: String) : AuthState()
}

View File

@@ -13,31 +13,39 @@ import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
class AuthViewModel : ViewModel() {
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
class AuthViewModel() : ViewModel() {
private val checkAndSaveAuthCodeUseCase by lazy {
CheckAndSaveAuthCodeUseCase(AuthRepository)
}
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
private val _actionFlow = MutableSharedFlow<Unit>()
val actionFlow: SharedFlow<Unit> = _actionFlow
fun onIntent(intent: AuthIntent) {
when (intent) {
is AuthIntent.Send -> {
viewModelScope.launch(Dispatchers.Default) {
_uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
viewModelScope.launch(Dispatchers.IO) {
_uiState.value = AuthState.Loading
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
onSuccess = {
_actionFlow.emit(Unit)
},
onFailure = { error ->
error.printStackTrace()
_actionFlow.emit(Unit)
onFailure = { throwable ->
val errorMessage = throwable.message ?: "Неизвестная ошибка"
_uiState.value = AuthState.Error(errorMessage)
}
)
}
}
is AuthIntent.TextInput -> Unit
is AuthIntent.TextInput -> {
_uiState.value = AuthState.Data
}
}
}
}

View File

@@ -31,6 +31,28 @@ fun BookScreen(
}
}
/*
Экран бронирования
На данном экране необходимо вывести возможные даты и места для бронирования.
Элементы, которые должны присутствовать на экране:
Группа вкладок. Каждая вкладка (book_date_pos_{индекс}) содержит текстовое поле (book_date) с датой бронирования в формате dd.MM.
В зависимости от выбранной даты необходимо отобразить группу с единственным выбором (пояснения на изображении ниже). Каждый элемент группы (book_place_pos_{индекс}) кликабелен и содержит:
Текстовое поле (book_place_text), в котором содержится место доступное для брони.
Селектор (book_place_selector), который отображает, выбран элемент или нет. У данного элемента обязательно наличие: (Modifier.selectable)
Кнопка (book_book_button) для бронирования.
Кнопка (book_back_button) для возвращения на предыдущий экран.
По умолчанию неотображаемое текстовое поле с ошибкой (book_error). Отметим, что это поле не должно рендериться.
По умолчанию неотображаемая кнопка обновить (book_refresh_button).
По умолчанию неотображаемый текст “Всё забронировано” (book_empty).
Требования к компонентам:
По умолчанию выбирается самая ранняя доступная дата (например, из набора "5 января", "6 января", "9 января" будет показана дата "5 января").
Список дат отсортирован по возрастанию. Даты без доступных мест для бронирования необходимо не отображать.
Если нет доступных для бронирования дат, необходимо скрыть все элементы, кроме элементов из п. 4 и 7.
В случае ошибки при получении данных о доступном бронировании в запросе api/<CODE>/booking, необходимо отобразить элемент из п. 4, 5 и 6 с возможностью обновить данные.
При успешном бронировании нужно закрыть текущий экран и вернуться на главный, обновив его.
*/
Column(
modifier = Modifier
.fillMaxSize()

View File

@@ -31,6 +31,32 @@ fun MainScreen(
}
}
/*
Главный экран
Данный экран содержит информацию о пользователе и его текущие бронирования. Если пользователь авторизован, данный экран должен быть отображен при запуске приложения.
Элементы, которые должны присутствовать на экране:
Текстовое поле (main_name), в котором написано имя пользователя.
Изображение (main_photo), на котором отображено фото пользователя.
Кнопка (main_logout_button) для выхода пользователя из аккаунта.
Кнопка (main_refresh_button) для принудительного обновления данных.
Кнопка (main_add_button) для бронирования.
Список, содержащий однотипные элементы (main_book_pos_{индекс}), со следующим содержимым:
Текстовое поле (main_item_date) с датой бронирования в формате dd.MM.yyyy.
Текстовое поле (main_item_place) с местом, которое забронировано.
По умолчанию скрытое текстовое поле с ошибкой (main_error).
Требования к компонентам:
В случае любой ошибки необходимо скрыть все элементы, кроме текстового поля с ошибкой и кнопки обновления данных.
Для получения данных необходимо использовать сетевой запрос /api/<CODE>/info.
При нажатии на кнопку для выхода, все сохранённые данные пользователя должны быть очищены, а приложение должно открыть экран авторизации.
При нажатии кнопки бронирования необходимо открыть экран бронирования.
При нажатии на кнопку обновления данных — необходимо повторно вызывать сетевой запрос для получения актуальных данных.
Список бронирований должен быть отсортирован в порядке увеличения даты (например, 5 января -> 6 января -> 9 января).
*/
Column(
modifier = Modifier
.fillMaxSize()