Upstream changes (#23)

This commit is contained in:
2024-07-11 02:12:32 +03:00
committed by GitHub
parent 8a6378f509
commit 3503ecffab
906 changed files with 23577 additions and 24115 deletions
+1
View File
@@ -0,0 +1 @@
/build
+60
View File
@@ -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
View File
@@ -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
}
@@ -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
@@ -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 = ""
)
}
}
@@ -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()
}
@@ -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
}
@@ -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)
}
}
}
}
@@ -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)
}
@@ -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))
}
}
}
}
}
@@ -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
}
}
}