main #8
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/.idea
|
/.idea
|
||||||
/.gradle
|
/.gradle
|
||||||
/build
|
/build
|
||||||
|
/local.properties
|
||||||
|
|||||||
BIN
Client_v1.apk
Normal file
BIN
Client_v1.apk
Normal file
Binary file not shown.
BIN
Design_v0.penpot
Normal file
BIN
Design_v0.penpot
Normal file
Binary file not shown.
BIN
Design_v1.penpot
Normal file
BIN
Design_v1.penpot
Normal file
Binary file not shown.
2
README.md
Normal file
2
README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Penpot Доска
|
||||||
|
https://pp.sicampus.ru/#/workspace?team-id=14a6b474-d5fa-807f-8007-9f077cdd8786&file-id=14a6b474-d5fa-807f-8007-9f541769571d&page-id=14a6b474-d5fa-807f-8007-9f541769571e
|
||||||
@ -34,6 +34,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("androidx.compose.foundation:foundation:1.10.3")
|
||||||
defaultComposeLibrary()
|
defaultComposeLibrary()
|
||||||
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
||||||
@ -47,4 +48,8 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
|
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
|
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview:1.10.3")
|
||||||
|
implementation("androidx.compose.runtime:runtime-tracing:1.10.3")
|
||||||
|
implementation("androidx.compose.runtime:runtime-tracing:1.10.3")
|
||||||
|
debugImplementation ("androidx.compose.ui:ui-tooling:1.10.3")
|
||||||
}
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
package ru.myitschool.work.core
|
package ru.myitschool.work.core
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val HOST = "http://localhost:8090"
|
const val HOST = "http://10.0.0.14:49183"
|
||||||
const val AUTH_URL = "/auth"
|
const val AUTH_URL = "/auth"
|
||||||
|
|
||||||
|
//TODO заменить на /auth
|
||||||
const val INFO_URL = "/info"
|
const val INFO_URL = "/info"
|
||||||
const val BOOKING_URL = "/booking"
|
const val BOOKING_URL = "/booking"
|
||||||
const val BOOK_URL = "/book"
|
const val BOOK_URL = "/book"
|
||||||
|
|||||||
@ -4,7 +4,8 @@ object TestIds {
|
|||||||
object Auth {
|
object Auth {
|
||||||
const val ERROR = "auth_error"
|
const val ERROR = "auth_error"
|
||||||
const val SIGN_BUTTON = "auth_sign_button"
|
const val SIGN_BUTTON = "auth_sign_button"
|
||||||
const val CODE_INPUT = "auth_code_input"
|
const val LOGIN_INPUT = "auth_login_input"
|
||||||
|
const val PASSWORD_INPUT = "auth_password_input"
|
||||||
}
|
}
|
||||||
object Main {
|
object Main {
|
||||||
const val ERROR = "main_error"
|
const val ERROR = "main_error"
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
package ru.myitschool.work.data.source
|
package ru.myitschool.work.data.source
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.client.request.basicAuth
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.header
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.request.setBody
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
import io.ktor.http.HttpHeaders.Authorization
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
@ -19,6 +24,7 @@ import ru.myitschool.work.core.Constants
|
|||||||
import ru.myitschool.work.data.dto.PlaceDto
|
import ru.myitschool.work.data.dto.PlaceDto
|
||||||
import ru.myitschool.work.data.dto.BookRequestDto
|
import ru.myitschool.work.data.dto.BookRequestDto
|
||||||
import ru.myitschool.work.data.dto.UserDto
|
import ru.myitschool.work.data.dto.UserDto
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
|
||||||
object NetworkDataSource {
|
object NetworkDataSource {
|
||||||
private val client by lazy {
|
private val client by lazy {
|
||||||
@ -36,9 +42,25 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
suspend fun checkAuth(text: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
/*
|
||||||
|
val response = client.post(
|
||||||
|
getUrl(Constants.AUTH_URL))
|
||||||
|
*/
|
||||||
|
|
||||||
|
val response = client.post(
|
||||||
|
urlString = "http://10.0.0.177:49183/api/auth" //getUrl(Constants.AUTH_URL)
|
||||||
|
) {
|
||||||
|
header(key = Authorization, value = basicAuth(text.split(Regex(":"))[0], text.split(Regex(":"))[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// "Basic ${Base64.encode(text.toByteArray(), )}" "Basic ${Base64.encode(text.toByteArray())}"
|
||||||
|
//val response = client.get(getUrl(Constants.AUTH_URL)) {}
|
||||||
|
Log.d("RESPONSE", response.toString())
|
||||||
|
Log.d("RESPONSE_STATUS", response.status.toString())
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> true
|
HttpStatusCode.OK -> true
|
||||||
else -> false
|
else -> false
|
||||||
@ -49,7 +71,7 @@ object NetworkDataSource {
|
|||||||
suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
println("!!!!!!!!!!!!!! getInfo $code")
|
println("!!!!!!!!!!!!!! getInfo $code")
|
||||||
val response = client.get(getUrl(code, Constants.INFO_URL))
|
val response = client.get(getUrl(Constants.INFO_URL))
|
||||||
if (response.status == HttpStatusCode.OK) {
|
if (response.status == HttpStatusCode.OK) {
|
||||||
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
|
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
|
||||||
response.body<UserDto>()
|
response.body<UserDto>()
|
||||||
@ -62,7 +84,7 @@ object NetworkDataSource {
|
|||||||
|
|
||||||
suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
|
suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
val response = client.get(getUrl(Constants.BOOKING_URL))
|
||||||
if (response.status == HttpStatusCode.OK) {
|
if (response.status == HttpStatusCode.OK) {
|
||||||
response.body<Map<String, List<PlaceDto>>>()
|
response.body<Map<String, List<PlaceDto>>>()
|
||||||
} else {
|
} else {
|
||||||
@ -73,7 +95,7 @@ object NetworkDataSource {
|
|||||||
|
|
||||||
suspend fun addBook(code: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
|
suspend fun addBook(code: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
val response = client.post(getUrl(Constants.BOOK_URL)) {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(data)
|
setBody(data)
|
||||||
}
|
}
|
||||||
@ -85,5 +107,5 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
private fun getUrl(targetUrl: String) = "${Constants.HOST}/api$targetUrl"
|
||||||
}
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package ru.myitschool.work.domain.auth
|
|
||||||
|
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
|
||||||
|
|
||||||
class CheckAndSaveAuthCodeUseCase(
|
|
||||||
private val repository: AuthRepository
|
|
||||||
) {
|
|
||||||
suspend operator fun invoke(
|
|
||||||
text: String
|
|
||||||
): Result<Unit> {
|
|
||||||
return repository.checkAndSave(text).mapCatching { success ->
|
|
||||||
if (!success) error("Code is incorrect")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
|
class CheckAndSaveAuthDataUseCase(
|
||||||
|
private val repository: AuthRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
textLogin: String,
|
||||||
|
textPassword: String
|
||||||
|
): Result<Unit> {
|
||||||
|
return repository.checkAndSave("$textLogin:$textPassword").mapCatching { success ->
|
||||||
|
if (!success) {
|
||||||
|
error("Login or password is incorrect")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package ru.myitschool.work.domain.auth
|
|
||||||
|
|
||||||
class CheckCodeFormatUseCase {
|
|
||||||
operator fun invoke(
|
|
||||||
text: String
|
|
||||||
): Boolean {
|
|
||||||
return text.length == 4 && text.all { char ->
|
|
||||||
char.isLetterOrDigit() &&
|
|
||||||
((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
class CheckLoginFormatUseCase {
|
||||||
|
operator fun invoke(
|
||||||
|
text: String
|
||||||
|
): Boolean {
|
||||||
|
return text.all { char ->
|
||||||
|
(char.isLetterOrDigit() &&
|
||||||
|
((char in ('A'..'Z')) || (char in ('a'..'z')) || char.isDigit()))
|
||||||
|
|| char == '.'
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Locale.getDefault
|
||||||
|
|
||||||
|
class CheckPasswordFormatUseCase {
|
||||||
|
operator fun invoke(
|
||||||
|
textLogin: String,
|
||||||
|
textPassword: String
|
||||||
|
):
|
||||||
|
Boolean {
|
||||||
|
val lowerCasePassword = textPassword.lowercase(getDefault())
|
||||||
|
val lowerCaseLogin = textLogin.lowercase(getDefault())
|
||||||
|
val intersect = lowerCasePassword.toList().intersect(lowerCaseLogin.toList());
|
||||||
|
return (textPassword.length >= 8) && (textPassword.all { char ->
|
||||||
|
textPassword.count { it == char } < 3
|
||||||
|
&& intersect.size < 3
|
||||||
|
}) && !("[A-Za-z0-9]+".toRegex().matches(textPassword))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package ru.myitschool.work.ui.root
|
package ru.myitschool.work.ui.root
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
@ -10,11 +11,17 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import ru.myitschool.work.ui.screen.AppNavHost
|
import ru.myitschool.work.ui.screen.AppNavHost
|
||||||
import ru.myitschool.work.ui.theme.WorkTheme
|
import ru.myitschool.work.ui.theme.WorkTheme
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
class RootActivity : ComponentActivity() {
|
class RootActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
|
// Hide the status bar.
|
||||||
|
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
|
actionBar?.hide()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
WorkTheme {
|
WorkTheme {
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
sealed interface AuthIntent {
|
sealed interface AuthIntent {
|
||||||
data class Send(val text: String): AuthIntent
|
data class Send(val textLogin: String, val textPassword: String): AuthIntent
|
||||||
data class TextInput(val text: String): AuthIntent
|
data class TextInput(val textLogin: String, val textPassword: String): AuthIntent
|
||||||
}
|
}
|
||||||
@ -3,21 +3,23 @@ package ru.myitschool.work.ui.screen.auth
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@ -28,8 +30,18 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
import ru.myitschool.work.R
|
import ru.myitschool.work.R
|
||||||
import ru.myitschool.work.core.TestIds
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthScreen(
|
fun AuthScreen(
|
||||||
@ -38,6 +50,7 @@ fun AuthScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.actionFlow.collect { action ->
|
viewModel.actionFlow.collect { action ->
|
||||||
when (action) {
|
when (action) {
|
||||||
@ -51,15 +64,11 @@ fun AuthScreen(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(all = 24.dp),
|
.padding(all = 44.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.auth_title),
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
when (val currentState = state) {
|
when (val currentState = state) {
|
||||||
is AuthState.Data -> Content(viewModel, currentState)
|
is AuthState.Data -> Content(viewModel, currentState)
|
||||||
is AuthState.Loading -> {
|
is AuthState.Loading -> {
|
||||||
@ -72,37 +81,108 @@ fun AuthScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Content(
|
private fun Content
|
||||||
|
(
|
||||||
viewModel: AuthViewModel,
|
viewModel: AuthViewModel,
|
||||||
state: AuthState.Data
|
state: AuthState.Data
|
||||||
) {
|
) {
|
||||||
var inputText by remember { mutableStateOf("") }
|
var inputLogin by rememberSaveable { mutableStateOf("") }
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
val googleSans = FontFamily(
|
||||||
TextField(
|
Font(R.font.googlesans_regular, FontWeight.Medium, FontStyle.Normal)
|
||||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
|
||||||
value = inputText,
|
|
||||||
onValueChange = {
|
|
||||||
inputText = it
|
|
||||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.auth_label)) }
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(5.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auth_title),
|
||||||
|
fontFamily = googleSans,
|
||||||
|
color = Color(0xFF284777),
|
||||||
|
fontSize = 35.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontStyle = FontStyle.Normal,
|
||||||
|
modifier = Modifier.fillMaxWidth(1f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(5.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auth_label_login),
|
||||||
|
fontFamily = googleSans,
|
||||||
|
color = Color(0xFF74777f),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontStyle = FontStyle.Normal,
|
||||||
|
modifier = Modifier.fillMaxWidth(1f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.testTag(TestIds.Auth.LOGIN_INPUT).fillMaxWidth(),
|
||||||
|
value = inputLogin,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = TextFieldDefaults.colors(unfocusedContainerColor = Color(0xFFededf4)),
|
||||||
|
onValueChange = {
|
||||||
|
inputLogin = it
|
||||||
|
viewModel.onIntent(AuthIntent.TextInput(
|
||||||
|
it,
|
||||||
|
textPassword = ""
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
var inputPassword by rememberSaveable { mutableStateOf("") }
|
||||||
|
Spacer(modifier = Modifier.size(20.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.auth_label_password),
|
||||||
|
fontFamily = googleSans,
|
||||||
|
color = Color(0xFF74777f),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontStyle = FontStyle.Normal,
|
||||||
|
modifier = Modifier.fillMaxWidth(1f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(10.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.testTag(TestIds.Auth.PASSWORD_INPUT).fillMaxWidth(),
|
||||||
|
value = inputPassword,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
colors = TextFieldDefaults.colors(unfocusedContainerColor = Color(0xFFededf4)),
|
||||||
|
onValueChange = {
|
||||||
|
inputPassword = it
|
||||||
|
viewModel.onIntent(AuthIntent.TextInput(
|
||||||
|
inputLogin,
|
||||||
|
textPassword = it
|
||||||
|
))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(35.dp))
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
viewModel.onIntent(AuthIntent.Send(inputLogin, inputPassword))
|
||||||
},
|
},
|
||||||
enabled = state.isEnabledSend
|
enabled = state.isEnabledSend
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.auth_sign_in))
|
Text(
|
||||||
|
text = "Войти",
|
||||||
|
modifier = Modifier.padding(10.dp),
|
||||||
|
fontFamily = googleSans,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontStyle = FontStyle.Normal,
|
||||||
|
fontSize = 18.sp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (state.error != null) {
|
if (state.error != null) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||||
text = state.error,
|
text = if (viewModel.incorrectAttemptNum >= 5) {
|
||||||
|
"Превышен лимит попыток входа"
|
||||||
|
} else {
|
||||||
|
state.error
|
||||||
|
},
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = Color.Red,
|
color = Color.Red,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AuthView() {
|
||||||
|
AuthScreen(navController = rememberNavController())
|
||||||
|
}
|
||||||
@ -10,19 +10,25 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
import ru.myitschool.work.domain.auth.CheckAndSaveAuthDataUseCase
|
||||||
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase
|
import ru.myitschool.work.domain.auth.CheckLoginFormatUseCase
|
||||||
|
import ru.myitschool.work.domain.auth.CheckPasswordFormatUseCase
|
||||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
class AuthViewModel : ViewModel() {
|
||||||
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() }
|
private val checkLoginFormatUseCase by lazy { CheckLoginFormatUseCase() }
|
||||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
private val checkAndSaveAuthDataUseCase by lazy { CheckAndSaveAuthDataUseCase(AuthRepository) }
|
||||||
|
private val checkPasswordFormatUseCase by lazy { CheckPasswordFormatUseCase() }
|
||||||
|
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow<AuthState>(
|
private val _uiState = MutableStateFlow<AuthState>(
|
||||||
AuthState.Data(
|
AuthState.Data(
|
||||||
isEnabledSend = false,
|
isEnabledSend = false,
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var incorrectAttemptNum = 0
|
||||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
||||||
@ -32,7 +38,7 @@ class AuthViewModel : ViewModel() {
|
|||||||
when (intent) {
|
when (intent) {
|
||||||
is AuthIntent.Send -> {
|
is AuthIntent.Send -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
checkAndSaveAuthDataUseCase.invoke(intent.textLogin, intent.textPassword).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
_actionFlow.emit(AuthAction.Open(MainScreenDestination))
|
_actionFlow.emit(AuthAction.Open(MainScreenDestination))
|
||||||
},
|
},
|
||||||
@ -42,14 +48,20 @@ class AuthViewModel : ViewModel() {
|
|||||||
error = error.message
|
error = error.message
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (error.message == "Login or password is incorrect") {
|
||||||
|
incorrectAttemptNum += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is AuthIntent.TextInput -> {
|
is AuthIntent.TextInput -> {
|
||||||
updateStateIfData { oldState ->
|
updateStateIfData { oldState ->
|
||||||
oldState.copy(
|
oldState.copy(
|
||||||
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text),
|
isEnabledSend = (checkPasswordFormatUseCase.invoke(
|
||||||
|
intent.textLogin, intent.textPassword
|
||||||
|
) && checkLoginFormatUseCase.invoke(intent.textLogin) && incorrectAttemptNum <= 4),
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/main/res/font/googlesans_regular.ttf
Normal file
BIN
app/src/main/res/font/googlesans_regular.ttf
Normal file
Binary file not shown.
@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
<color name="purple_200">#415f91</color>
|
||||||
<color name="purple_500">#FF6200EE</color>
|
<color name="purple_500">#565F71</color>
|
||||||
<color name="purple_700">#FF3700B3</color>
|
<color name="purple_700">#33618D</color>
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
<color name="teal_200">#D6E3FF</color>
|
||||||
<color name="teal_700">#FF018786</color>
|
<color name="teal_700">#DAE2F9</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#44474E</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#F3F3FA</color>
|
||||||
</resources>
|
</resources>
|
||||||
@ -1,8 +1,9 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Work</string>
|
<string name="app_name">Work</string>
|
||||||
<string name="title_activity_root">RootActivity</string>
|
<string name="title_activity_root">Приложение</string>
|
||||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
<string name="auth_title">Авторизация</string>
|
||||||
<string name="auth_label">Код</string>
|
<string name="auth_label_login">Логин</string>
|
||||||
|
<string name="auth_label_password">Пароль</string>
|
||||||
<string name="auth_sign_in">Войти</string>
|
<string name="auth_sign_in">Войти</string>
|
||||||
|
|
||||||
<string name="main_refresh">Обновить</string>
|
<string name="main_refresh">Обновить</string>
|
||||||
|
|||||||
BIN
khBGPU.apk
Normal file
BIN
khBGPU.apk
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user