improvements for previewing validation & captcha screens

This commit is contained in:
2024-07-15 19:02:49 +03:00
parent ee7499f117
commit 304c630d1d
13 changed files with 87 additions and 115 deletions
@@ -43,6 +43,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
@@ -251,3 +252,13 @@ fun CaptchaScreen(
}
}
}
@Preview
@Composable
private fun CaptchaScreenPreview() {
CaptchaScreen(
screenState = CaptchaScreenState.EMPTY.copy(
code = "zcuecz"
)
)
}
@@ -278,8 +278,7 @@ class LoginViewModelImpl(
redirectUri = error.redirectUri,
phoneMask = error.phoneMask,
validationType = error.validationType.value,
canResendSms = error.validationResend == "sms",
wrongCodeError = null
canResendSms = error.validationResend == "sms"
)
validationArguments.update { arguments }
}
@@ -11,6 +11,5 @@ data class LoginValidationArguments(
val redirectUri: String,
val phoneMask: String,
val validationType: String,
val canResendSms: Boolean,
val wrongCodeError: String?,
val canResendSms: Boolean
) : Parcelable
@@ -43,8 +43,7 @@ fun NavGraphBuilder.authNavGraph(
redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
phoneMask = arguments.phoneMask,
validationType = arguments.validationType,
canResendSms = arguments.canResendSms,
wrongCodeError = arguments.wrongCodeError
canResendSms = arguments.canResendSms
)
)
},
@@ -7,7 +7,6 @@ import com.meloda.app.fast.auth.validation.model.ValidationScreenState
import com.meloda.app.fast.auth.validation.model.ValidationType
import com.meloda.app.fast.auth.validation.navigation.Validation
import com.meloda.app.fast.auth.validation.validation.ValidationValidator
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.extensions.createTimerFlow
import com.meloda.app.fast.common.extensions.listenValue
import com.meloda.app.fast.common.extensions.setValue
@@ -26,6 +25,8 @@ interface ValidationViewModel {
val screenState: StateFlow<ValidationScreenState>
val validationType: StateFlow<ValidationType?>
val isNeedToOpenLogin: StateFlow<Boolean>
fun onCodeInputChanged(newCode: String)
@@ -47,10 +48,11 @@ class ValidationViewModelImpl(
override val screenState = MutableStateFlow(ValidationScreenState.EMPTY)
override val validationType = MutableStateFlow<ValidationType?>(null)
override val isNeedToOpenLogin = MutableStateFlow(false)
private var validationSid: String? = null
private var delayJob: Job? = null
init {
@@ -61,11 +63,13 @@ class ValidationViewModelImpl(
validationSid = arguments.validationSid
validationType.setValue {
ValidationType.parse(arguments.validationType)
}
screenState.setValue { old ->
old.copy(
isSmsButtonVisible = arguments.canResendSms,
codeError = arguments.wrongCodeError,
validationText = getValidationText(ValidationType.parse(arguments.validationType)),
phoneMask = arguments.phoneMask
)
}
@@ -75,7 +79,7 @@ class ValidationViewModelImpl(
screenState.updateValue(
screenState.value.copy(
code = newCode.trim(),
codeError = null
codeError = false
)
)
@@ -118,34 +122,29 @@ class ValidationViewModelImpl(
private fun processValidation(): Boolean {
val isValid = validator.validate(screenState.value).isValid()
screenState.updateValue(
screenState.value.copy(
codeError = if (isValid) null
else "Field must not be empty"
)
)
screenState.setValue { old -> old.copy(codeError = !isValid) }
return isValid
}
private fun sendValidationCode() {
authUseCase.validatePhone(validationSid.orEmpty())
val sid = validationSid ?: return
authUseCase.validatePhone(sid)
.listenValue { state ->
state.processState(
error = { error ->
},
success = { response ->
val newValidationType = response.validationType
response.validationType?.let { newValidationType ->
validationType.setValue { ValidationType.parse(newValidationType) }
}
val newCanResendSms = response.validationResend == "sms"
screenState.setValue { old ->
old.copy(
isSmsButtonVisible = newCanResendSms,
validationText = getValidationText(
ValidationType.parse(newValidationType.orEmpty())
)
)
old.copy(isSmsButtonVisible = newCanResendSms)
}
startTickTimer(response.delay)
@@ -158,7 +157,7 @@ class ValidationViewModelImpl(
}
}
fun startTickTimer(delay: Int?) {
private fun startTickTimer(delay: Int?) {
if (delay == null || delayJob?.isActive == true) return
delayJob = createTimerFlow(
@@ -182,18 +181,4 @@ class ValidationViewModelImpl(
},
).launchIn(viewModelScope)
}
private fun getValidationText(validationType: ValidationType): UiText {
return when (validationType) {
ValidationType.Sms -> {
UiText.Simple("SMS with the code is sent to ${screenState.value.phoneMask}")
}
ValidationType.App -> {
UiText.Simple("Enter the code from the code generator application")
}
is ValidationType.Other -> UiText.Simple(validationType.type)
}
}
}
@@ -11,6 +11,5 @@ data class ValidationArguments(
val redirectUri: String,
val phoneMask: String,
val validationType: String,
val canResendSms: Boolean,
val wrongCodeError: String?,
val canResendSms: Boolean
) : Parcelable
@@ -1,27 +1,20 @@
package com.meloda.app.fast.auth.validation.model
import com.meloda.app.fast.common.UiText
data class ValidationScreenState(
val code: String?,
val codeError: String?,
val codeError: Boolean,
val isSmsButtonVisible: Boolean,
val delayTime: Int,
val phoneMask: String,
// TODO: 13/07/2024, Danil Nikolaev: check wtf is this
val validationText: UiText,
val phoneMask: String
) {
companion object {
val EMPTY = ValidationScreenState(
code = null,
codeError = null,
codeError = false,
isSmsButtonVisible = false,
delayTime = 0,
phoneMask = "",
validationText = UiText.Simple("")
phoneMask = ""
)
}
}
@@ -1,23 +1,10 @@
package com.meloda.app.fast.auth.validation.model
sealed class ValidationType(val value: String) {
data object Sms : ValidationType(TYPE_SMS)
data object App : ValidationType(TYPE_TWO_FA_APP)
data class Other(val type: String) : ValidationType(type)
enum class ValidationType(val value: String) {
SMS("sms"), APP("2fa_app");
companion object {
private const val TYPE_SMS = "sms"
private const val TYPE_TWO_FA_APP = "2fa_app"
fun parse(validationType: String): ValidationType {
return when (validationType) {
TYPE_SMS -> Sms
TYPE_TWO_FA_APP -> App
else -> Other(validationType)
}
}
fun parse(value: String): ValidationType = entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown validation type with value: $value")
}
}
@@ -42,15 +42,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meloda.app.fast.auth.validation.ValidationViewModel
import com.meloda.app.fast.auth.validation.ValidationViewModelImpl
import com.meloda.app.fast.auth.validation.model.ValidationScreenState
import com.meloda.app.fast.auth.validation.model.ValidationType
import com.meloda.app.fast.ui.components.ActionInvokeDismiss
import com.meloda.app.fast.ui.components.MaterialDialog
import com.meloda.app.fast.ui.components.TextFieldErrorText
import com.meloda.app.fast.ui.util.getString
import org.koin.androidx.compose.koinViewModel
import com.meloda.app.fast.ui.R as UiR
@@ -62,6 +63,7 @@ fun ValidationRoute(
) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
val validationType by viewModel.validationType.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToOpenLogin) {
if (isNeedToOpenLogin) {
@@ -78,6 +80,7 @@ fun ValidationRoute(
ValidationScreen(
screenState = screenState,
validationType = validationType,
onBack = onBack,
onCodeInputChanged = viewModel::onCodeInputChanged,
onTextFieldDoneAction = viewModel::onTextFieldDoneAction,
@@ -89,6 +92,7 @@ fun ValidationRoute(
@Composable
fun ValidationScreen(
screenState: ValidationScreenState = ValidationScreenState.EMPTY,
validationType: ValidationType? = null,
onBack: () -> Unit = {},
onCodeInputChanged: (String) -> Unit = {},
onTextFieldDoneAction: () -> Unit = {},
@@ -105,6 +109,17 @@ fun ValidationScreen(
mutableStateOf(false)
}
val validationText by remember(validationType) {
mutableStateOf(
when (validationType) {
ValidationType.SMS -> "SMS with the code is sent to ${screenState.phoneMask}"
ValidationType.APP -> "Enter the code from the code generator application"
null -> ""
}
)
}
LaunchedEffect(confirmedExit) {
if (confirmedExit) {
onBack()
@@ -130,7 +145,6 @@ fun ValidationScreen(
}
var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) }
val codeError = screenState.codeError
Scaffold { padding ->
Column(
@@ -167,7 +181,7 @@ fun ValidationScreen(
)
Spacer(modifier = Modifier.height(38.dp))
Text(
text = screenState.validationText.getString().orEmpty(),
text = validationText.orEmpty(),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground
)
@@ -201,7 +215,7 @@ fun ValidationScreen(
Icon(
painter = painterResource(id = UiR.drawable.round_qr_code_24),
contentDescription = "QR Code icon",
tint = if (codeError != null) {
tint = if (screenState.codeError) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
@@ -219,11 +233,11 @@ fun ValidationScreen(
onTextFieldDoneAction()
}
),
isError = codeError != null
isError = screenState.codeError
)
AnimatedVisibility(visible = codeError != null) {
TextFieldErrorText(text = codeError.orEmpty())
AnimatedVisibility(screenState.codeError) {
TextFieldErrorText(text = "Field must not be empty")
}
}
@@ -279,3 +293,15 @@ fun ValidationScreen(
}
}
}
@Preview
@Composable
private fun ValidationScreenPreview() {
ValidationScreen(
screenState = ValidationScreenState.EMPTY.copy(
phoneMask = "+7 (***) ***-**-21",
code = "222222"
),
validationType = ValidationType.SMS
)
}