forked from Olympic/NTO-2025-Android-TeamTask
authscreen-changed
This commit is contained in:
@@ -2,11 +2,11 @@ package ru.myitschool.work
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
class App: Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
AuthRepository.initialize(this)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
|
||||
object AuthRepository {
|
||||
|
||||
private const val PREFS_NAME = "auth_prefs"
|
||||
private const val KEY_AUTH_CODE = "auth_code"
|
||||
|
||||
private var codeCache: String? = null
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
fun initialize(context: Context) {
|
||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
codeCache = prefs.getString(KEY_AUTH_CODE, null)
|
||||
}
|
||||
|
||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
return withContext(Dispatchers.IO) {
|
||||
NetworkDataSource.checkAuth(code = text).onSuccess { success ->
|
||||
if (success) {
|
||||
codeCache = text
|
||||
saveCode(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveCode(code: String) {
|
||||
codeCache = code
|
||||
prefs.edit().putString(KEY_AUTH_CODE, code).apply()
|
||||
}
|
||||
|
||||
fun getSavedCode(): String? = codeCache
|
||||
|
||||
suspend fun clearSavedCode() {
|
||||
codeCache = null
|
||||
withContext(Dispatchers.IO) {
|
||||
prefs.edit().remove(KEY_AUTH_CODE).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,38 @@ package ru.myitschool.work.ui.root
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.view.WindowCompat
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
import ru.myitschool.work.ui.screen.AppNavHost
|
||||
import ru.myitschool.work.ui.theme.WorkTheme
|
||||
|
||||
class RootActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
// Определяем стартовый экран
|
||||
val startDestination = if (AuthRepository.getSavedCode() != null) {
|
||||
MainScreenDestination
|
||||
} else {
|
||||
AuthScreenDestination
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
WorkTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
AppNavHost(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(innerPadding),
|
||||
startDestination = startDestination
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,38 +11,39 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import ru.myitschool.work.ui.nav.AppDestination
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
import ru.myitschool.work.ui.screen.auth.AuthScreen
|
||||
import ru.myitschool.work.ui.screen.main.MainScreen
|
||||
|
||||
@Composable
|
||||
fun AppNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavHostController = rememberNavController()
|
||||
navController: NavHostController = rememberNavController(),
|
||||
startDestination: AppDestination = AuthScreenDestination
|
||||
) {
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
navController = navController,
|
||||
startDestination = AuthScreenDestination,
|
||||
startDestination = startDestination,
|
||||
) {
|
||||
composable<AuthScreenDestination> {
|
||||
AuthScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable<MainScreenDestination> {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "Hello")
|
||||
}
|
||||
MainScreen(navController = navController)
|
||||
}
|
||||
|
||||
composable<BookScreenDestination> {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "Hello")
|
||||
Text(text = "Экран бронирования (будет реализован позже)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -38,6 +23,7 @@ fun AuthScreen(
|
||||
navController: NavController
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val error by viewModel.error.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.actionFlow.collect {
|
||||
@@ -57,13 +43,14 @@ fun AuthScreen(
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
when (val currentState = state) {
|
||||
is AuthState.Data -> Content(viewModel, currentState)
|
||||
is AuthState.Loading -> {
|
||||
|
||||
// Исправление: Loading теперь внутри AuthState.Data
|
||||
if (state is AuthState.Data && (state as AuthState.Data).isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
} else if (state is AuthState.Data) {
|
||||
Content(viewModel, state as AuthState.Data, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,26 +58,44 @@ fun AuthScreen(
|
||||
@Composable
|
||||
private fun Content(
|
||||
viewModel: AuthViewModel,
|
||||
state: AuthState.Data
|
||||
state: AuthState.Data,
|
||||
error: String?
|
||||
) {
|
||||
var inputText by remember { mutableStateOf("") }
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
// Показать ошибку, если есть
|
||||
if (error != null) {
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.ERROR)
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
TextField(
|
||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
||||
value = inputText,
|
||||
value = state.inputText,
|
||||
onValueChange = {
|
||||
inputText = it
|
||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
||||
},
|
||||
label = { Text(stringResource(R.string.auth_label)) }
|
||||
label = { Text(stringResource(R.string.auth_label)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
isError = error != null
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
||||
onClick = {
|
||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
||||
viewModel.onIntent(AuthIntent.Send(state.inputText))
|
||||
},
|
||||
enabled = true
|
||||
enabled = state.isValid && !state.isLoading // Добавил проверку валидности
|
||||
) {
|
||||
Text(stringResource(R.string.auth_sign_in))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
sealed interface AuthState {
|
||||
object Loading: AuthState
|
||||
object Data: AuthState
|
||||
data class Data(
|
||||
val inputText: String = "",
|
||||
val error: String? = null,
|
||||
val isValid: Boolean = false,
|
||||
val isLoading: Boolean = false
|
||||
) : AuthState
|
||||
}
|
||||
@@ -3,41 +3,72 @@ package ru.myitschool.work.ui.screen.auth
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.*
|
||||
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) }
|
||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
||||
|
||||
private val checkAndSaveAuthCodeUseCase by lazy {
|
||||
CheckAndSaveAuthCodeUseCase(AuthRepository)
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow<AuthState>(
|
||||
AuthState.Data(inputText = "", isValid = false, isLoading = false)
|
||||
)
|
||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||
|
||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||
val actionFlow: SharedFlow<Unit> = _actionFlow.asSharedFlow()
|
||||
|
||||
private val _error = MutableStateFlow<String?>(null)
|
||||
val error: StateFlow<String?> = _error.asStateFlow()
|
||||
|
||||
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.update {
|
||||
if (it is AuthState.Data) it.copy(isLoading = true) else it
|
||||
}
|
||||
|
||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(Unit)
|
||||
_error.value = null
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTrace()
|
||||
_actionFlow.emit(Unit)
|
||||
_error.value = error.message ?: "Ошибка авторизации"
|
||||
_uiState.update {
|
||||
if (it is AuthState.Data) it.copy(isLoading = false) else it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is AuthIntent.TextInput -> Unit
|
||||
|
||||
is AuthIntent.TextInput -> {
|
||||
if (_error.value != null) {
|
||||
_error.value = null
|
||||
}
|
||||
|
||||
val isValid = isValidCode(intent.text)
|
||||
_uiState.update {
|
||||
if (it is AuthState.Data) it.copy(
|
||||
inputText = intent.text,
|
||||
isValid = isValid
|
||||
) else it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidCode(text: String): Boolean {
|
||||
if (text.isEmpty() || text.length != 4) return false
|
||||
|
||||
return text.all { char ->
|
||||
char in 'A'..'Z' || char in 'a'..'z' || char in '0'..'9'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = viewModel(),
|
||||
navController: NavController
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
// При первом открытии загружаем данные
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
viewModel.loadUserInfo()
|
||||
}
|
||||
|
||||
// Проверяем авторизацию
|
||||
LaunchedEffect(key1 = viewModel.isUserAuthorized) {
|
||||
if (!viewModel.isUserAuthorized) {
|
||||
navController.navigate(AuthScreenDestination) {
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = "Главная") }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (state) {
|
||||
is MainState.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
is MainState.Error -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = (state as MainState.Error).message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.testTag("main_error"),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is MainState.Success -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Главный экран",
|
||||
style = MaterialTheme.typography.headlineMedium
|
||||
)
|
||||
Text(
|
||||
text = "Здесь будет информация о пользователе и бронированиях",
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||
|
||||
val isUserAuthorized: Boolean
|
||||
get() = AuthRepository.getSavedCode() != null
|
||||
|
||||
fun loadUserInfo() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = MainState.Loading
|
||||
|
||||
try {
|
||||
val code = AuthRepository.getSavedCode()
|
||||
if (code == null) {
|
||||
_uiState.value = MainState.Error("Пользователь не авторизован")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// TODO: Реальный запрос к API
|
||||
// Пока заглушка
|
||||
_uiState.value = MainState.Success(
|
||||
userInfo = UserInfo("Иван Иванов", ""),
|
||||
bookings = emptyList()
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = MainState.Error("Ошибка загрузки: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
AuthRepository.clearSavedCode()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
loadUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface MainState {
|
||||
object Loading : MainState
|
||||
data class Error(val message: String) : MainState
|
||||
data class Success(
|
||||
val userInfo: UserInfo,
|
||||
val bookings: List<Booking>
|
||||
) : MainState
|
||||
}
|
||||
|
||||
// Временные модели (потом перенесем в data/model)
|
||||
data class UserInfo(
|
||||
val name: String,
|
||||
val photoUrl: String
|
||||
)
|
||||
|
||||
data class Booking(
|
||||
val date: String,
|
||||
val place: String
|
||||
)
|
||||
Reference in New Issue
Block a user