forked from melod1n/fast-messenger
Upstream changes (#23)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,60 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose.compiler)
|
||||
alias(libs.plugins.org.jetbrains.kotlin.plugin.parcelize)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.twofa"
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = Configs.minSdk
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = Configs.java
|
||||
targetCompatibility = Configs.java
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = Configs.java.toString()
|
||||
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
useLiveLiterals = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.data)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.nanokt.android)
|
||||
implementation(libs.nanokt.jvm)
|
||||
implementation(libs.nanokt)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlin.serialization)
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.meloda.app.fast.auth.twofa
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.model.api.responses.SendSmsResponse
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AuthUseCase {
|
||||
|
||||
fun sendSms(
|
||||
validationSid: String
|
||||
): Flow<State<SendSmsResponse>>
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.meloda.app.fast.auth.twofa
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.data.api.auth.AuthRepository
|
||||
import com.meloda.app.fast.model.api.responses.SendSmsResponse
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class AuthUseCaseImpl(
|
||||
private val authRepository: AuthRepository
|
||||
) : AuthUseCase {
|
||||
|
||||
// TODO: 05/05/2024, Danil Nikolaev: implement
|
||||
override fun sendSms(validationSid: String): Flow<State<SendSmsResponse>> = flow {
|
||||
// emit(State.Loading)
|
||||
//
|
||||
// val newState = authRepository.sendSms(validationSid)
|
||||
// .fold(
|
||||
// onSuccess = { response -> State.Success(response) },
|
||||
// onNetworkFailure = { State.Error.ConnectionError },
|
||||
// onUnknownFailure = { State.UNKNOWN_ERROR },
|
||||
// onHttpFailure = { result -> result.error.toStateApiError() },
|
||||
// onApiFailure = { result -> result.error.toStateApiError() }
|
||||
// )
|
||||
// emit(newState)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package com.meloda.app.fast.auth.twofa
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaScreenState
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaValidationType
|
||||
import com.meloda.app.fast.auth.twofa.navigation.TwoFa
|
||||
import com.meloda.app.fast.auth.twofa.validation.TwoFaValidator
|
||||
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
|
||||
import com.meloda.app.fast.common.extensions.updateValue
|
||||
import com.meloda.app.fast.data.processState
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
interface TwoFaViewModel {
|
||||
|
||||
val screenState: StateFlow<TwoFaScreenState>
|
||||
|
||||
fun onCodeInputChanged(newCode: String)
|
||||
|
||||
fun onBackButtonClicked()
|
||||
fun onCancelButtonClicked()
|
||||
fun onRequestSmsButtonClicked()
|
||||
fun onTextFieldDoneClicked()
|
||||
fun onDoneButtonClicked()
|
||||
|
||||
fun onNavigatedToLogin()
|
||||
|
||||
fun setArguments(arguments: TwoFaArguments)
|
||||
}
|
||||
|
||||
class TwoFaViewModelImpl(
|
||||
private val validator: TwoFaValidator,
|
||||
private val authUseCase: AuthUseCase,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : TwoFaViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY)
|
||||
|
||||
private var delayJob: Job? = null
|
||||
|
||||
init {
|
||||
// TODO: 08/07/2024, Danil Nikolaev: use when fixed
|
||||
//savedStateHandle.toRoute<TwoFa>().arguments
|
||||
|
||||
val arguments = TwoFa.from(savedStateHandle).arguments
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
twoFaSid = arguments.validationSid,
|
||||
canResendSms = arguments.canResendSms,
|
||||
codeError = arguments.wrongCodeError,
|
||||
twoFaText = getTwoFaText(TwoFaValidationType.parse(arguments.validationType)),
|
||||
phoneMask = arguments.phoneMask
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCodeInputChanged(newCode: String) {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(
|
||||
twoFaCode = newCode.trim(),
|
||||
codeError = null
|
||||
)
|
||||
)
|
||||
|
||||
if (newCode.length == 6) {
|
||||
viewModelScope.launch {
|
||||
delay(250)
|
||||
onDoneButtonClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackButtonClicked() {
|
||||
onCancelButtonClicked()
|
||||
}
|
||||
|
||||
override fun onCancelButtonClicked() {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(
|
||||
twoFaCode = null,
|
||||
isNeedToOpenLogin = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRequestSmsButtonClicked() {
|
||||
sendValidationCode()
|
||||
}
|
||||
|
||||
override fun onTextFieldDoneClicked() {
|
||||
onDoneButtonClicked()
|
||||
}
|
||||
|
||||
override fun onDoneButtonClicked() {
|
||||
if (!processValidation()) return
|
||||
|
||||
screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
|
||||
}
|
||||
|
||||
override fun onNavigatedToLogin() {
|
||||
screenState.updateValue(TwoFaScreenState.EMPTY)
|
||||
}
|
||||
|
||||
override fun setArguments(arguments: TwoFaArguments) {
|
||||
Log.d("TwoFaViewModel", "TwoFaArguments: $arguments")
|
||||
|
||||
// screenState.updateValue(
|
||||
// screenState.value.copy(
|
||||
// twoFaSid = arguments.validationSid,
|
||||
// canResendSms = arguments.canResendSms,
|
||||
// codeError = arguments.wrongCodeError,
|
||||
// twoFaText = getTwoFaText(TwoFaValidationType.parse(arguments.validationType)),
|
||||
// phoneMask = arguments.phoneMask
|
||||
// )
|
||||
// )
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
)
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
private fun sendValidationCode() {
|
||||
val validationSid = screenState.value.twoFaSid
|
||||
|
||||
authUseCase.sendSms(validationSid)
|
||||
.listenValue { state ->
|
||||
state.processState(
|
||||
error = { error -> },
|
||||
success = { response ->
|
||||
val newValidationType = response.validationType
|
||||
val newCanResendSms = response.validationResend == "sms"
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
canResendSms = newCanResendSms,
|
||||
twoFaText = getTwoFaText(
|
||||
TwoFaValidationType.parse(newValidationType.orEmpty())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
startTickTimer(response.delay)
|
||||
}
|
||||
)
|
||||
|
||||
if (state.isLoading()) {
|
||||
screenState.emit(screenState.value.copy(canResendSms = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startTickTimer(delay: Int?) {
|
||||
if (delay == null || delayJob?.isActive == true) return
|
||||
|
||||
delayJob = createTimerFlow(
|
||||
time = delay,
|
||||
onStartAction = {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(canResendSms = false)
|
||||
)
|
||||
},
|
||||
onTickAction = { remainedTime ->
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(delayTime = remainedTime)
|
||||
)
|
||||
},
|
||||
onTimeoutAction = {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(
|
||||
canResendSms = true
|
||||
)
|
||||
)
|
||||
},
|
||||
).launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun getTwoFaText(validationType: TwoFaValidationType): UiText {
|
||||
return when (validationType) {
|
||||
TwoFaValidationType.Sms -> {
|
||||
UiText.Simple("SMS with the code is sent to ${screenState.value.phoneMask}")
|
||||
}
|
||||
|
||||
TwoFaValidationType.TwoFaApp -> {
|
||||
UiText.Simple("Enter the code from the code generator application")
|
||||
}
|
||||
|
||||
is TwoFaValidationType.Another -> UiText.Simple(validationType.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.meloda.app.fast.auth.twofa.di
|
||||
|
||||
import com.meloda.app.fast.auth.twofa.AuthUseCase
|
||||
import com.meloda.app.fast.auth.twofa.AuthUseCaseImpl
|
||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModel
|
||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModelImpl
|
||||
import com.meloda.app.fast.auth.twofa.validation.TwoFaValidator
|
||||
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val twoFaModule = module {
|
||||
singleOf(::TwoFaValidator)
|
||||
viewModelOf(::TwoFaViewModelImpl) bind TwoFaViewModel::class
|
||||
singleOf(::AuthUseCaseImpl) bind AuthUseCase::class
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class TwoFaArguments(
|
||||
val validationSid: String,
|
||||
val redirectUri: String,
|
||||
val phoneMask: String,
|
||||
val validationType: String,
|
||||
val canResendSms: Boolean,
|
||||
val wrongCodeError: String?,
|
||||
) : Parcelable
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
|
||||
import com.meloda.app.fast.common.UiText
|
||||
|
||||
data class TwoFaScreenState(
|
||||
val twoFaSid: String,
|
||||
val twoFaCode: String?,
|
||||
val twoFaText: UiText,
|
||||
val canResendSms: Boolean,
|
||||
val codeError: String?,
|
||||
val delayTime: Int,
|
||||
val isNeedToOpenLogin: Boolean,
|
||||
val phoneMask: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = TwoFaScreenState(
|
||||
twoFaSid = "",
|
||||
twoFaCode = null,
|
||||
twoFaText = UiText.Simple(""),
|
||||
canResendSms = false,
|
||||
codeError = null,
|
||||
delayTime = 0,
|
||||
isNeedToOpenLogin = false,
|
||||
phoneMask = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
|
||||
sealed class TwoFaUiAction {
|
||||
data class CodeResult(val code: String) : TwoFaUiAction()
|
||||
data object BackClicked : TwoFaUiAction()
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
|
||||
sealed class TwoFaValidationResult {
|
||||
data object Empty : TwoFaValidationResult()
|
||||
data object Valid : TwoFaValidationResult()
|
||||
|
||||
fun isValid() = this == Valid
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
|
||||
sealed class TwoFaValidationType(val value: String) {
|
||||
|
||||
data object Sms : TwoFaValidationType(TYPE_SMS)
|
||||
|
||||
data object TwoFaApp : TwoFaValidationType(TYPE_TWO_FA_APP)
|
||||
|
||||
data class Another(val type: String) : TwoFaValidationType(type)
|
||||
|
||||
companion object {
|
||||
private const val TYPE_SMS = "sms"
|
||||
private const val TYPE_TWO_FA_APP = "2fa_app"
|
||||
|
||||
fun parse(validationType: String): TwoFaValidationType {
|
||||
return when (validationType) {
|
||||
TYPE_SMS -> Sms
|
||||
TYPE_TWO_FA_APP -> TwoFaApp
|
||||
else -> Another(validationType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.meloda.app.fast.auth.twofa.navigation
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaUiAction
|
||||
import com.meloda.app.fast.auth.twofa.presentation.TwoFaScreen
|
||||
import com.meloda.app.fast.common.customNavType
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@Serializable
|
||||
data class TwoFa(val arguments: TwoFaArguments) {
|
||||
companion object {
|
||||
val typeMap = mapOf(typeOf<TwoFaArguments>() to customNavType<TwoFaArguments>())
|
||||
|
||||
fun from(savedStateHandle: SavedStateHandle) =
|
||||
savedStateHandle.toRoute<TwoFa>(typeMap)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.twoFaRoute(
|
||||
onBack: () -> Unit,
|
||||
onResult: (String) -> Unit
|
||||
) {
|
||||
composable<TwoFa>(typeMap = TwoFa.typeMap) {
|
||||
TwoFaScreen(
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
TwoFaUiAction.BackClicked -> onBack()
|
||||
is TwoFaUiAction.CodeResult -> onResult(action.code)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToTwoFa(arguments: TwoFaArguments) {
|
||||
this.navigate(TwoFa(arguments))
|
||||
}
|
||||
|
||||
fun NavController.setTwoFaResult(code: String) {
|
||||
this.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("twofacode", code)
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
package com.meloda.app.fast.auth.twofa.presentation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModel
|
||||
import com.meloda.app.fast.auth.twofa.TwoFaViewModelImpl
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaUiAction
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
||||
import com.meloda.app.fast.designsystem.getString
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
private typealias OnAction = (TwoFaUiAction) -> Unit
|
||||
|
||||
@Composable
|
||||
fun TwoFaScreen(
|
||||
onAction: OnAction,
|
||||
viewModel: TwoFaViewModel = koinViewModel<TwoFaViewModelImpl>(),
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
var confirmedExit by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
var showExitAlert by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (confirmedExit) {
|
||||
onAction(TwoFaUiAction.BackClicked)
|
||||
}
|
||||
|
||||
BackHandler(enabled = !confirmedExit) {
|
||||
if (!confirmedExit) {
|
||||
showExitAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
if (showExitAlert) {
|
||||
MaterialDialog(
|
||||
onDismissAction = { showExitAlert = false },
|
||||
title = UiText.Simple("Confirmation"),
|
||||
text = UiText.Simple("Are you sure? Authorization process will be cancelled."),
|
||||
confirmText = UiText.Resource(UiR.string.yes),
|
||||
confirmAction = {
|
||||
confirmedExit = true
|
||||
},
|
||||
cancelText = UiText.Resource(UiR.string.no)
|
||||
)
|
||||
}
|
||||
|
||||
if (screenState.isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
|
||||
val code = screenState.twoFaCode
|
||||
if (code == null) {
|
||||
onAction(TwoFaUiAction.BackClicked)
|
||||
} else {
|
||||
onAction(TwoFaUiAction.CodeResult(code = code))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(30.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = { onAction(TwoFaUiAction.BackClicked) },
|
||||
text = {
|
||||
Text(
|
||||
text = "Cancel",
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Close,
|
||||
contentDescription = "Close icon",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = "Two-Factor\nAuthentication",
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Spacer(modifier = Modifier.height(38.dp))
|
||||
Text(
|
||||
text = screenState.twoFaText.getString().orEmpty(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
val delayRemainedTime = screenState.delayTime
|
||||
AnimatedVisibility(visible = delayRemainedTime > 0) {
|
||||
Text(
|
||||
text = "Can resend after $delayRemainedTime seconds",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
|
||||
val codeError = screenState.codeError
|
||||
|
||||
TextField(
|
||||
value = code,
|
||||
onValueChange = { newText ->
|
||||
if (newText.text.length > 6) return@TextField
|
||||
|
||||
code = newText
|
||||
viewModel.onCodeInputChanged((newText.text))
|
||||
},
|
||||
label = { Text(text = "Code") },
|
||||
placeholder = { Text(text = "Code") },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp)),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.round_qr_code_24),
|
||||
contentDescription = "QR Code icon",
|
||||
tint = if (codeError != null) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onTextFieldDoneClicked()
|
||||
}
|
||||
),
|
||||
isError = codeError != null
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = codeError != null) {
|
||||
TextFieldErrorText(text = codeError.orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
val canResendSms = screenState.canResendSms
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = canResendSms,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = viewModel::onRequestSmsButtonClicked,
|
||||
text = {
|
||||
Text(
|
||||
text = "Request SMS",
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.round_sms_24),
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
contentDescription = "SMS icon"
|
||||
)
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = viewModel::onDoneButtonClicked,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Done,
|
||||
contentDescription = "Done icon",
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = !canResendSms,
|
||||
exit = shrinkHorizontally()
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.meloda.app.fast.auth.twofa.validation
|
||||
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaScreenState
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaValidationResult
|
||||
|
||||
class TwoFaValidator {
|
||||
|
||||
fun validate(screenState: TwoFaScreenState): TwoFaValidationResult {
|
||||
return when {
|
||||
screenState.twoFaCode.isNullOrEmpty() -> TwoFaValidationResult.Empty
|
||||
else -> TwoFaValidationResult.Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user