From 304c630d1d2064c154f56be68226043485c3b366 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 15 Jul 2024 19:02:49 +0300 Subject: [PATCH] improvements for previewing validation & captcha screens --- .../app/fast/common/util/AndroidUtils.kt | 5 -- .../meloda/app/fast/ui/basic/ContentAlpha.kt | 4 +- .../com/meloda/app/fast/ui/theme/AppTheme.kt | 15 ++++-- .../com/meloda/app/fast/ui/util/Extensions.kt | 26 --------- .../captcha/presentation/CaptchaScreen.kt | 11 ++++ .../meloda/fast/auth/login/LoginViewModel.kt | 3 +- .../login/model/LoginValidationArguments.kt | 3 +- .../com/meloda/app/fast/auth/AuthGraph.kt | 3 +- .../auth/validation/ValidationViewModel.kt | 53 +++++++------------ .../validation/model/ValidationArguments.kt | 3 +- .../validation/model/ValidationScreenState.kt | 15 ++---- .../auth/validation/model/ValidationType.kt | 21 ++------ .../presentation/ValidationScreen.kt | 40 +++++++++++--- 13 files changed, 87 insertions(+), 115 deletions(-) diff --git a/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt index 43c11b0c..dc31b740 100644 --- a/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt +++ b/core/common/src/main/kotlin/com/meloda/app/fast/common/util/AndroidUtils.kt @@ -7,7 +7,6 @@ import android.content.Intent import android.graphics.Bitmap import android.net.Uri import android.os.Build -import android.os.PowerManager import android.provider.Settings import android.widget.Toast import androidx.core.content.FileProvider @@ -121,10 +120,6 @@ object AndroidUtils { return intent } - fun isBatterySaverOn(context: Context): Boolean { - return (context.getSystemService(Context.POWER_SERVICE) as? PowerManager)?.isPowerSaveMode == true - } - fun getImageToShare(context: Context, existingFile: File): Uri? { val imageFolder = File(context.cacheDir, "images") diff --git a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/basic/ContentAlpha.kt b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/basic/ContentAlpha.kt index 044cff4b..951638f7 100644 --- a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/basic/ContentAlpha.kt +++ b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/basic/ContentAlpha.kt @@ -22,7 +22,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf 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. @@ -79,7 +79,7 @@ object ContentAlpha { lowContrastAlpha: Float ): Float { val contentColor = LocalContentColor.current - return if (!isUsingDarkTheme()) { + return if (!LocalIsDarkTheme.current) { if (contentColor.luminance() > 0.5) highContrastAlpha else lowContrastAlpha } else { if (contentColor.luminance() < 0.5) highContrastAlpha else lowContrastAlpha diff --git a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/theme/AppTheme.kt b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/theme/AppTheme.kt index 8841b650..f11ef1e5 100644 --- a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/theme/AppTheme.kt +++ b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/theme/AppTheme.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.Color @@ -115,6 +116,8 @@ val LocalTheme = compositionLocalOf { ) } +val LocalIsDarkTheme = compositionLocalOf { false } + val LocalHazeState = compositionLocalOf { HazeState() } @@ -178,9 +181,11 @@ fun AppTheme( } } - MaterialTheme( - colorScheme = predefinedColorScheme ?: colorScheme, - typography = typography, - content = content - ) + CompositionLocalProvider(LocalIsDarkTheme provides useDarkTheme) { + MaterialTheme( + colorScheme = predefinedColorScheme ?: colorScheme, + typography = typography, + content = content + ) + } } diff --git a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/Extensions.kt b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/Extensions.kt index 1aa83481..a6af1ba3 100644 --- a/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/Extensions.kt +++ b/core/ui/src/main/kotlin/com/meloda/app/fast/ui/util/Extensions.kt @@ -1,6 +1,5 @@ package com.meloda.app.fast.ui.util -import android.content.res.Configuration import android.view.KeyEvent import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable @@ -11,34 +10,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.onKeyEvent -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource 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 fun UiText?.getString(): String? { diff --git a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt index 3f633acc..d1961716 100644 --- a/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt +++ b/feature/auth/captcha/src/main/kotlin/com/meloda/app/fast/auth/captcha/presentation/CaptchaScreen.kt @@ -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" + ) + ) +} diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt index 9594022e..59fd7e95 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/LoginViewModel.kt @@ -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 } } diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationArguments.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationArguments.kt index 12ac5b46..f41cf592 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationArguments.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/model/LoginValidationArguments.kt @@ -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 diff --git a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt index 560bf72e..086bc224 100644 --- a/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt +++ b/feature/auth/src/main/kotlin/com/meloda/app/fast/auth/AuthGraph.kt @@ -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 ) ) }, diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/ValidationViewModel.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/ValidationViewModel.kt index 5ed99c28..dd969b0c 100644 --- a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/ValidationViewModel.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/ValidationViewModel.kt @@ -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 + val validationType: StateFlow + val isNeedToOpenLogin: StateFlow fun onCodeInputChanged(newCode: String) @@ -47,10 +48,11 @@ class ValidationViewModelImpl( override val screenState = MutableStateFlow(ValidationScreenState.EMPTY) + override val validationType = MutableStateFlow(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) - } - } } diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationArguments.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationArguments.kt index 16ab7831..d3d6c41c 100644 --- a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationArguments.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationArguments.kt @@ -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 diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationScreenState.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationScreenState.kt index 5e56fe1c..cf9fab77 100644 --- a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationScreenState.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationScreenState.kt @@ -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 = "" ) } } diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationType.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationType.kt index 871a62b9..8e1c0114 100644 --- a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationType.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/model/ValidationType.kt @@ -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") } } diff --git a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/presentation/ValidationScreen.kt b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/presentation/ValidationScreen.kt index 56bcdef8..0ae55021 100644 --- a/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/presentation/ValidationScreen.kt +++ b/feature/auth/validation/src/main/kotlin/com/meloda/app/fast/auth/validation/presentation/ValidationScreen.kt @@ -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 + ) +}