Upstream changes (#23)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.meloda.app.fast.auth.captcha
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaScreenState
|
||||
import com.meloda.app.fast.auth.captcha.navigation.Captcha
|
||||
import com.meloda.app.fast.auth.captcha.validation.CaptchaValidator
|
||||
import com.meloda.app.fast.common.extensions.setValue
|
||||
import com.meloda.app.fast.common.extensions.updateValue
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
interface CaptchaViewModel {
|
||||
|
||||
val screenState: StateFlow<CaptchaScreenState>
|
||||
|
||||
fun onCodeInputChanged(newCode: String)
|
||||
|
||||
fun onTextFieldDoneClicked()
|
||||
fun onDoneButtonClicked()
|
||||
|
||||
fun setArguments(arguments: CaptchaArguments)
|
||||
|
||||
fun onNavigatedToLogin()
|
||||
}
|
||||
|
||||
class CaptchaViewModelImpl(
|
||||
private val validator: CaptchaValidator,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : CaptchaViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
|
||||
|
||||
init {
|
||||
val arguments = Captcha.from(savedStateHandle).arguments
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
captchaSid = arguments.captchaSid,
|
||||
captchaImage = arguments.captchaImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCodeInputChanged(newCode: String) {
|
||||
val newState = screenState.value.copy(captchaCode = newCode.trim())
|
||||
screenState.update { newState }
|
||||
processValidation()
|
||||
}
|
||||
|
||||
override fun onTextFieldDoneClicked() {
|
||||
onDoneButtonClicked()
|
||||
}
|
||||
|
||||
override fun onDoneButtonClicked() {
|
||||
if (!processValidation()) return
|
||||
|
||||
screenState.updateValue(screenState.value.copy(isNeedToOpenLogin = true))
|
||||
}
|
||||
|
||||
override fun setArguments(arguments: CaptchaArguments) {
|
||||
// screenState.updateValue(
|
||||
// screenState.value.copy(
|
||||
// captchaSid = arguments.captchaSid,
|
||||
// captchaImage = arguments.captchaImage
|
||||
// )
|
||||
// )
|
||||
}
|
||||
|
||||
override fun onNavigatedToLogin() {
|
||||
screenState.updateValue(CaptchaScreenState.EMPTY)
|
||||
}
|
||||
|
||||
private fun processValidation(): Boolean {
|
||||
val isValid = validator.validate(screenState.value).isValid()
|
||||
screenState.updateValue(screenState.value.copy(codeError = !isValid))
|
||||
return isValid
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.meloda.app.fast.auth.captcha.di
|
||||
|
||||
import com.meloda.app.fast.auth.captcha.CaptchaViewModel
|
||||
import com.meloda.app.fast.auth.captcha.CaptchaViewModelImpl
|
||||
import com.meloda.app.fast.auth.captcha.validation.CaptchaValidator
|
||||
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 captchaModule = module {
|
||||
singleOf(::CaptchaValidator)
|
||||
viewModelOf(::CaptchaViewModelImpl) bind CaptchaViewModel::class
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.meloda.app.fast.auth.captcha.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class CaptchaArguments(
|
||||
val captchaSid: String,
|
||||
val captchaImage: String
|
||||
) : Parcelable
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.meloda.app.fast.auth.captcha.model
|
||||
|
||||
data class CaptchaScreenState(
|
||||
val captchaSid: String,
|
||||
val captchaImage: String,
|
||||
val captchaCode: String,
|
||||
val codeError: Boolean,
|
||||
val isNeedToOpenLogin: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = CaptchaScreenState(
|
||||
captchaSid = "",
|
||||
captchaImage = "",
|
||||
captchaCode = "",
|
||||
codeError = false,
|
||||
isNeedToOpenLogin = false
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.meloda.app.fast.auth.captcha.model
|
||||
|
||||
sealed class CaptchaValidationResult {
|
||||
data object Empty : CaptchaValidationResult()
|
||||
data object Valid : CaptchaValidationResult()
|
||||
|
||||
fun isValid() = this == Valid
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.meloda.app.fast.auth.captcha.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.captcha.model.CaptchaArguments
|
||||
import com.meloda.app.fast.auth.captcha.presentation.CaptchaScreen
|
||||
import com.meloda.app.fast.common.customNavType
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@Serializable
|
||||
data class Captcha(val arguments: CaptchaArguments) {
|
||||
|
||||
companion object {
|
||||
val typeMap = mapOf(typeOf<CaptchaArguments>() to customNavType<CaptchaArguments>())
|
||||
|
||||
fun from(savedStateHandle: SavedStateHandle) =
|
||||
savedStateHandle.toRoute<Captcha>(typeMap)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun NavGraphBuilder.captchaRoute(
|
||||
onBack: () -> Unit,
|
||||
onResult: (String) -> Unit
|
||||
) {
|
||||
composable<Captcha>(
|
||||
typeMap = Captcha.typeMap
|
||||
) {
|
||||
CaptchaScreen(
|
||||
onBack = onBack,
|
||||
onResult = onResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToCaptcha(arguments: CaptchaArguments) {
|
||||
this.navigate(Captcha(arguments))
|
||||
}
|
||||
|
||||
fun NavController.setCaptchaResult(code: String) {
|
||||
this.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("captchacode", code)
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package com.meloda.app.fast.auth.captcha.presentation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.border
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.meloda.app.fast.auth.captcha.CaptchaViewModel
|
||||
import com.meloda.app.fast.auth.captcha.CaptchaViewModelImpl
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@Composable
|
||||
fun CaptchaScreen(
|
||||
onBack: () -> Unit,
|
||||
onResult: (String) -> Unit,
|
||||
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
var confirmedExit by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
var showExitAlert by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (confirmedExit) {
|
||||
onBack()
|
||||
}
|
||||
|
||||
BackHandler(enabled = !confirmedExit) {
|
||||
if (!confirmedExit) {
|
||||
showExitAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
if (showExitAlert) {
|
||||
MaterialDialog(
|
||||
onDismissAction = { showExitAlert = false },
|
||||
title = UiText.Simple("Confirmation"),
|
||||
text = UiText.Simple("Are you sure? Captcha process will be cancelled."),
|
||||
confirmText = UiText.Resource(UiR.string.yes),
|
||||
confirmAction = {
|
||||
confirmedExit = true
|
||||
},
|
||||
cancelText = UiText.Resource(UiR.string.no)
|
||||
)
|
||||
}
|
||||
|
||||
if (screenState.isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
onResult(screenState.captchaCode)
|
||||
}
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Scaffold { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(30.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = onBack,
|
||||
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 = "Captcha",
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Spacer(modifier = Modifier.height(38.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "To proceed with your action, enter a code from the picture",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.weight(0.5f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
|
||||
val imageModifier = Modifier
|
||||
.border(
|
||||
2.dp,
|
||||
MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(10.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.height(48.dp)
|
||||
.width(130.dp)
|
||||
|
||||
if (LocalView.current.isInEditMode) {
|
||||
Image(
|
||||
painter = painterResource(id = UiR.drawable.test_captcha),
|
||||
contentDescription = "Captcha image",
|
||||
modifier = imageModifier
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(screenState.captchaImage)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = "Captcha image",
|
||||
contentScale = ContentScale.FillBounds,
|
||||
modifier = imageModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.captchaCode)) }
|
||||
val showError = screenState.codeError
|
||||
|
||||
TextField(
|
||||
value = code,
|
||||
onValueChange = { newText ->
|
||||
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 (showError) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
},
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onTextFieldDoneClicked()
|
||||
}
|
||||
),
|
||||
isError = showError
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = showError) {
|
||||
TextFieldErrorText(text = "Field must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = viewModel::onDoneButtonClicked,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Done,
|
||||
contentDescription = "Done icon",
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.meloda.app.fast.auth.captcha.validation
|
||||
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaScreenState
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaValidationResult
|
||||
|
||||
class CaptchaValidator {
|
||||
|
||||
fun validate(screenState: CaptchaScreenState): CaptchaValidationResult {
|
||||
return when {
|
||||
screenState.captchaCode.isEmpty() -> CaptchaValidationResult.Empty
|
||||
else -> CaptchaValidationResult.Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user