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
@@ -7,7 +7,6 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@@ -121,10 +120,6 @@ object AndroidUtils {
return intent return intent
} }
fun isBatterySaverOn(context: Context): Boolean {
return (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode == true
}
fun getImageToShare(context: Context, existingFile: File): Uri? { fun getImageToShare(context: Context, existingFile: File): Uri? {
val imageFolder = File(context.cacheDir, "images") val imageFolder = File(context.cacheDir, "images")
@@ -22,7 +22,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import com.meloda.app.fast.ui.util.isUsingDarkTheme import com.meloda.app.fast.ui.theme.LocalIsDarkTheme
/** /**
* Default alpha levels used by Material components. * Default alpha levels used by Material components.
@@ -79,7 +79,7 @@ object ContentAlpha {
lowContrastAlpha: Float lowContrastAlpha: Float
): Float { ): Float {
val contentColor = LocalContentColor.current val contentColor = LocalContentColor.current
return if (!isUsingDarkTheme()) { return if (!LocalIsDarkTheme.current) {
if (contentColor.luminance() > 0.5) highContrastAlpha else lowContrastAlpha if (contentColor.luminance() > 0.5) highContrastAlpha else lowContrastAlpha
} else { } else {
if (contentColor.luminance() < 0.5) highContrastAlpha else lowContrastAlpha if (contentColor.luminance() < 0.5) highContrastAlpha else lowContrastAlpha
@@ -9,6 +9,7 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -115,6 +116,8 @@ val LocalTheme = compositionLocalOf {
) )
} }
val LocalIsDarkTheme = compositionLocalOf { false }
val LocalHazeState = compositionLocalOf { val LocalHazeState = compositionLocalOf {
HazeState() HazeState()
} }
@@ -178,9 +181,11 @@ fun AppTheme(
} }
} }
MaterialTheme( CompositionLocalProvider(LocalIsDarkTheme provides useDarkTheme) {
colorScheme = predefinedColorScheme ?: colorScheme, MaterialTheme(
typography = typography, colorScheme = predefinedColorScheme ?: colorScheme,
content = content typography = typography,
) content = content
)
}
} }
@@ -1,6 +1,5 @@
package com.meloda.app.fast.ui.util package com.meloda.app.fast.ui.util
import android.content.res.Configuration
import android.view.KeyEvent import android.view.KeyEvent
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -11,34 +10,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.meloda.app.fast.common.UiText import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.common.util.AndroidUtils
@Composable
fun isUsingDarkTheme(): Boolean {
// val nightThemeMode = AppCompatDelegate.MODE_NIGHT_YES
// SettingsController.getInt(
// SettingsKeys.KEY_APPEARANCE_DARK_THEME,
// SettingsKeys.DEFAULT_VALUE_APPEARANCE_DARK_THEME
// )
// val appForceDarkMode = nightThemeMode == AppCompatDelegate.MODE_NIGHT_YES
// val appBatterySaver = nightThemeMode == AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
val context = LocalContext.current
val systemUiNightMode = context.resources.configuration.uiMode
val isSystemBatterySaver = AndroidUtils.isBatterySaverOn(context)
val isSystemUsingDarkTheme =
systemUiNightMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
return true
// return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && nightThemeMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
@Composable @Composable
fun UiText?.getString(): String? { fun UiText?.getString(): String? {
@@ -43,6 +43,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage 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, redirectUri = error.redirectUri,
phoneMask = error.phoneMask, phoneMask = error.phoneMask,
validationType = error.validationType.value, validationType = error.validationType.value,
canResendSms = error.validationResend == "sms", canResendSms = error.validationResend == "sms"
wrongCodeError = null
) )
validationArguments.update { arguments } validationArguments.update { arguments }
} }
@@ -11,6 +11,5 @@ data class LoginValidationArguments(
val redirectUri: String, val redirectUri: String,
val phoneMask: String, val phoneMask: String,
val validationType: String, val validationType: String,
val canResendSms: Boolean, val canResendSms: Boolean
val wrongCodeError: String?,
) : Parcelable ) : Parcelable
@@ -43,8 +43,7 @@ fun NavGraphBuilder.authNavGraph(
redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"), redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
phoneMask = arguments.phoneMask, phoneMask = arguments.phoneMask,
validationType = arguments.validationType, validationType = arguments.validationType,
canResendSms = arguments.canResendSms, canResendSms = arguments.canResendSms
wrongCodeError = arguments.wrongCodeError
) )
) )
}, },
@@ -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.model.ValidationType
import com.meloda.app.fast.auth.validation.navigation.Validation import com.meloda.app.fast.auth.validation.navigation.Validation
import com.meloda.app.fast.auth.validation.validation.ValidationValidator 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.createTimerFlow
import com.meloda.app.fast.common.extensions.listenValue import com.meloda.app.fast.common.extensions.listenValue
import com.meloda.app.fast.common.extensions.setValue import com.meloda.app.fast.common.extensions.setValue
@@ -26,6 +25,8 @@ interface ValidationViewModel {
val screenState: StateFlow<ValidationScreenState> val screenState: StateFlow<ValidationScreenState>
val validationType: StateFlow<ValidationType?>
val isNeedToOpenLogin: StateFlow<Boolean> val isNeedToOpenLogin: StateFlow<Boolean>
fun onCodeInputChanged(newCode: String) fun onCodeInputChanged(newCode: String)
@@ -47,10 +48,11 @@ class ValidationViewModelImpl(
override val screenState = MutableStateFlow(ValidationScreenState.EMPTY) override val screenState = MutableStateFlow(ValidationScreenState.EMPTY)
override val validationType = MutableStateFlow<ValidationType?>(null)
override val isNeedToOpenLogin = MutableStateFlow(false) override val isNeedToOpenLogin = MutableStateFlow(false)
private var validationSid: String? = null private var validationSid: String? = null
private var delayJob: Job? = null private var delayJob: Job? = null
init { init {
@@ -61,11 +63,13 @@ class ValidationViewModelImpl(
validationSid = arguments.validationSid validationSid = arguments.validationSid
validationType.setValue {
ValidationType.parse(arguments.validationType)
}
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
isSmsButtonVisible = arguments.canResendSms, isSmsButtonVisible = arguments.canResendSms,
codeError = arguments.wrongCodeError,
validationText = getValidationText(ValidationType.parse(arguments.validationType)),
phoneMask = arguments.phoneMask phoneMask = arguments.phoneMask
) )
} }
@@ -75,7 +79,7 @@ class ValidationViewModelImpl(
screenState.updateValue( screenState.updateValue(
screenState.value.copy( screenState.value.copy(
code = newCode.trim(), code = newCode.trim(),
codeError = null codeError = false
) )
) )
@@ -118,34 +122,29 @@ class ValidationViewModelImpl(
private fun processValidation(): Boolean { private fun processValidation(): Boolean {
val isValid = validator.validate(screenState.value).isValid() val isValid = validator.validate(screenState.value).isValid()
screenState.updateValue( screenState.setValue { old -> old.copy(codeError = !isValid) }
screenState.value.copy(
codeError = if (isValid) null
else "Field must not be empty"
)
)
return isValid return isValid
} }
private fun sendValidationCode() { private fun sendValidationCode() {
authUseCase.validatePhone(validationSid.orEmpty()) val sid = validationSid ?: return
authUseCase.validatePhone(sid)
.listenValue { state -> .listenValue { state ->
state.processState( state.processState(
error = { error -> error = { error ->
}, },
success = { response -> success = { response ->
val newValidationType = response.validationType response.validationType?.let { newValidationType ->
validationType.setValue { ValidationType.parse(newValidationType) }
}
val newCanResendSms = response.validationResend == "sms" val newCanResendSms = response.validationResend == "sms"
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(isSmsButtonVisible = newCanResendSms)
isSmsButtonVisible = newCanResendSms,
validationText = getValidationText(
ValidationType.parse(newValidationType.orEmpty())
)
)
} }
startTickTimer(response.delay) 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 if (delay == null || delayJob?.isActive == true) return
delayJob = createTimerFlow( delayJob = createTimerFlow(
@@ -182,18 +181,4 @@ class ValidationViewModelImpl(
}, },
).launchIn(viewModelScope) ).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 redirectUri: String,
val phoneMask: String, val phoneMask: String,
val validationType: String, val validationType: String,
val canResendSms: Boolean, val canResendSms: Boolean
val wrongCodeError: String?,
) : Parcelable ) : Parcelable
@@ -1,27 +1,20 @@
package com.meloda.app.fast.auth.validation.model package com.meloda.app.fast.auth.validation.model
import com.meloda.app.fast.common.UiText
data class ValidationScreenState( data class ValidationScreenState(
val code: String?, val code: String?,
val codeError: String?, val codeError: Boolean,
val isSmsButtonVisible: Boolean, val isSmsButtonVisible: Boolean,
val delayTime: Int, val delayTime: Int,
val phoneMask: String, val phoneMask: String
// TODO: 13/07/2024, Danil Nikolaev: check wtf is this
val validationText: UiText,
) { ) {
companion object { companion object {
val EMPTY = ValidationScreenState( val EMPTY = ValidationScreenState(
code = null, code = null,
codeError = null, codeError = false,
isSmsButtonVisible = false, isSmsButtonVisible = false,
delayTime = 0, delayTime = 0,
phoneMask = "", phoneMask = ""
validationText = UiText.Simple("")
) )
} }
} }
@@ -1,23 +1,10 @@
package com.meloda.app.fast.auth.validation.model package com.meloda.app.fast.auth.validation.model
sealed class ValidationType(val value: String) { enum class ValidationType(val value: String) {
SMS("sms"), APP("2fa_app");
data object Sms : ValidationType(TYPE_SMS)
data object App : ValidationType(TYPE_TWO_FA_APP)
data class Other(val type: String) : ValidationType(type)
companion object { companion object {
private const val TYPE_SMS = "sms" fun parse(value: String): ValidationType = entries.firstOrNull { it.value == value }
private const val TYPE_TWO_FA_APP = "2fa_app" ?: throw IllegalArgumentException("Unknown validation type with value: $value")
fun parse(validationType: String): ValidationType {
return when (validationType) {
TYPE_SMS -> Sms
TYPE_TWO_FA_APP -> App
else -> Other(validationType)
}
}
} }
} }
@@ -42,15 +42,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meloda.app.fast.auth.validation.ValidationViewModel import com.meloda.app.fast.auth.validation.ValidationViewModel
import com.meloda.app.fast.auth.validation.ValidationViewModelImpl 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.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.ActionInvokeDismiss
import com.meloda.app.fast.ui.components.MaterialDialog import com.meloda.app.fast.ui.components.MaterialDialog
import com.meloda.app.fast.ui.components.TextFieldErrorText import com.meloda.app.fast.ui.components.TextFieldErrorText
import com.meloda.app.fast.ui.util.getString
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import com.meloda.app.fast.ui.R as UiR import com.meloda.app.fast.ui.R as UiR
@@ -62,6 +63,7 @@ fun ValidationRoute(
) { ) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle() val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
val validationType by viewModel.validationType.collectAsStateWithLifecycle()
LaunchedEffect(isNeedToOpenLogin) { LaunchedEffect(isNeedToOpenLogin) {
if (isNeedToOpenLogin) { if (isNeedToOpenLogin) {
@@ -78,6 +80,7 @@ fun ValidationRoute(
ValidationScreen( ValidationScreen(
screenState = screenState, screenState = screenState,
validationType = validationType,
onBack = onBack, onBack = onBack,
onCodeInputChanged = viewModel::onCodeInputChanged, onCodeInputChanged = viewModel::onCodeInputChanged,
onTextFieldDoneAction = viewModel::onTextFieldDoneAction, onTextFieldDoneAction = viewModel::onTextFieldDoneAction,
@@ -89,6 +92,7 @@ fun ValidationRoute(
@Composable @Composable
fun ValidationScreen( fun ValidationScreen(
screenState: ValidationScreenState = ValidationScreenState.EMPTY, screenState: ValidationScreenState = ValidationScreenState.EMPTY,
validationType: ValidationType? = null,
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onCodeInputChanged: (String) -> Unit = {}, onCodeInputChanged: (String) -> Unit = {},
onTextFieldDoneAction: () -> Unit = {}, onTextFieldDoneAction: () -> Unit = {},
@@ -105,6 +109,17 @@ fun ValidationScreen(
mutableStateOf(false) 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) { LaunchedEffect(confirmedExit) {
if (confirmedExit) { if (confirmedExit) {
onBack() onBack()
@@ -130,7 +145,6 @@ fun ValidationScreen(
} }
var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) } var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) }
val codeError = screenState.codeError
Scaffold { padding -> Scaffold { padding ->
Column( Column(
@@ -167,7 +181,7 @@ fun ValidationScreen(
) )
Spacer(modifier = Modifier.height(38.dp)) Spacer(modifier = Modifier.height(38.dp))
Text( Text(
text = screenState.validationText.getString().orEmpty(), text = validationText.orEmpty(),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground color = MaterialTheme.colorScheme.onBackground
) )
@@ -201,7 +215,7 @@ fun ValidationScreen(
Icon( Icon(
painter = painterResource(id = UiR.drawable.round_qr_code_24), painter = painterResource(id = UiR.drawable.round_qr_code_24),
contentDescription = "QR Code icon", contentDescription = "QR Code icon",
tint = if (codeError != null) { tint = if (screenState.codeError) {
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
} else { } else {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
@@ -219,11 +233,11 @@ fun ValidationScreen(
onTextFieldDoneAction() onTextFieldDoneAction()
} }
), ),
isError = codeError != null isError = screenState.codeError
) )
AnimatedVisibility(visible = codeError != null) { AnimatedVisibility(screenState.codeError) {
TextFieldErrorText(text = codeError.orEmpty()) 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
)
}