forked from melod1n/fast-messenger
twoFa -> validation naming; fixes for preview for screens (separating view model from ui); some improvements & fixes
This commit is contained in:
@@ -81,7 +81,7 @@ dependencies {
|
||||
|
||||
implementation(projects.feature.auth.login)
|
||||
implementation(projects.feature.auth.captcha)
|
||||
implementation(projects.feature.auth.twofa)
|
||||
implementation(projects.feature.auth.validation)
|
||||
implementation(projects.feature.auth.userbanned)
|
||||
|
||||
implementation(libs.koin.androidx.compose)
|
||||
|
||||
+6
-8
@@ -18,7 +18,7 @@ interface CaptchaViewModel {
|
||||
|
||||
fun onCodeInputChanged(newCode: String)
|
||||
|
||||
fun onTextFieldDoneClicked()
|
||||
fun onTextFieldDoneAction()
|
||||
fun onDoneButtonClicked()
|
||||
|
||||
fun onNavigatedToLogin()
|
||||
@@ -32,24 +32,22 @@ class CaptchaViewModelImpl(
|
||||
override val screenState = MutableStateFlow(CaptchaScreenState.EMPTY)
|
||||
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||
|
||||
|
||||
init {
|
||||
val arguments = Captcha.from(savedStateHandle).arguments
|
||||
val captchaImage = Captcha.from(savedStateHandle).captchaImageUrl
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
captchaSid = arguments.captchaSid,
|
||||
captchaImage = URLDecoder.decode(arguments.captchaImage, "utf-8")
|
||||
)
|
||||
old.copy(captchaImageUrl = URLDecoder.decode(captchaImage, "utf-8"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCodeInputChanged(newCode: String) {
|
||||
val newState = screenState.value.copy(captchaCode = newCode.trim())
|
||||
val newState = screenState.value.copy(code = newCode.trim())
|
||||
screenState.update { newState }
|
||||
processValidation()
|
||||
}
|
||||
|
||||
override fun onTextFieldDoneClicked() {
|
||||
override fun onTextFieldDoneAction() {
|
||||
onDoneButtonClicked()
|
||||
}
|
||||
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
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
|
||||
+4
-6
@@ -1,17 +1,15 @@
|
||||
package com.meloda.app.fast.auth.captcha.model
|
||||
|
||||
data class CaptchaScreenState(
|
||||
val captchaSid: String,
|
||||
val captchaImage: String,
|
||||
val captchaCode: String,
|
||||
val captchaImageUrl: String,
|
||||
val code: String,
|
||||
val codeError: Boolean
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = CaptchaScreenState(
|
||||
captchaSid = "",
|
||||
captchaImage = "",
|
||||
captchaCode = "",
|
||||
captchaImageUrl = "",
|
||||
code = "",
|
||||
codeError = false
|
||||
)
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
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.presentation.CaptchaRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Captcha(val captchaImageUrl: String) {
|
||||
|
||||
companion object {
|
||||
fun from(savedStateHandle: SavedStateHandle) = savedStateHandle.toRoute<Captcha>()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun NavGraphBuilder.captchaScreen(
|
||||
onBack: () -> Unit,
|
||||
onResult: (String) -> Unit
|
||||
) {
|
||||
composable<Captcha> {
|
||||
CaptchaRoute(
|
||||
onBack = onBack,
|
||||
onResult = onResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToCaptcha(captchaImageUrl: String) {
|
||||
this.navigate(Captcha(captchaImageUrl))
|
||||
}
|
||||
|
||||
fun NavController.setCaptchaResult(code: String?) {
|
||||
this.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("captcha_code", code)
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
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)
|
||||
}
|
||||
+31
-13
@@ -49,6 +49,7 @@ 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.auth.captcha.model.CaptchaScreenState
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
||||
@@ -56,7 +57,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@Composable
|
||||
fun CaptchaScreen(
|
||||
fun CaptchaRoute(
|
||||
onBack: () -> Unit,
|
||||
onResult: (String) -> Unit,
|
||||
viewModel: CaptchaViewModel = koinViewModel<CaptchaViewModelImpl>()
|
||||
@@ -64,6 +65,30 @@ fun CaptchaScreen(
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isNeedToOpenLogin) {
|
||||
if (isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
onResult(screenState.code)
|
||||
}
|
||||
}
|
||||
|
||||
CaptchaScreen(
|
||||
screenState = screenState,
|
||||
onBack = onBack,
|
||||
onCodeInputChanged = viewModel::onCodeInputChanged,
|
||||
onTextFieldDoneAction = viewModel::onTextFieldDoneAction,
|
||||
onDoneButtonClicked = viewModel::onDoneButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CaptchaScreen(
|
||||
screenState: CaptchaScreenState = CaptchaScreenState.EMPTY,
|
||||
onBack: () -> Unit = {},
|
||||
onCodeInputChanged: (String) -> Unit = {},
|
||||
onTextFieldDoneAction: () -> Unit = {},
|
||||
onDoneButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
var confirmedExit by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@@ -97,13 +122,6 @@ fun CaptchaScreen(
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(isNeedToOpenLogin) {
|
||||
if (isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
onResult(screenState.captchaCode)
|
||||
}
|
||||
}
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Scaffold { padding ->
|
||||
@@ -171,7 +189,7 @@ fun CaptchaScreen(
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(screenState.captchaImage)
|
||||
.data(screenState.captchaImageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = "Captcha image",
|
||||
@@ -183,14 +201,14 @@ fun CaptchaScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(30.dp))
|
||||
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.captchaCode)) }
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.code)) }
|
||||
val showError = screenState.codeError
|
||||
|
||||
TextField(
|
||||
value = code,
|
||||
onValueChange = { newText ->
|
||||
code = newText
|
||||
viewModel.onCodeInputChanged(newText.text)
|
||||
onCodeInputChanged(newText.text)
|
||||
},
|
||||
label = { Text(text = "Code") },
|
||||
placeholder = { Text(text = "Code") },
|
||||
@@ -213,7 +231,7 @@ fun CaptchaScreen(
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onTextFieldDoneClicked()
|
||||
onTextFieldDoneAction()
|
||||
}
|
||||
),
|
||||
isError = showError
|
||||
@@ -225,7 +243,7 @@ fun CaptchaScreen(
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = viewModel::onDoneButtonClicked,
|
||||
onClick = onDoneButtonClicked,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ class CaptchaValidator {
|
||||
|
||||
fun validate(screenState: CaptchaScreenState): CaptchaValidationResult {
|
||||
return when {
|
||||
screenState.captchaCode.isEmpty() -> CaptchaValidationResult.Empty
|
||||
screenState.code.trim().isEmpty() -> CaptchaValidationResult.Empty
|
||||
else -> CaptchaValidationResult.Valid
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +91,7 @@ dependencies {
|
||||
implementation(libs.kotlin.serialization)
|
||||
|
||||
implementation(libs.rebugger)
|
||||
|
||||
androidTestImplementation(libs.compose.ui.test.junit4)
|
||||
debugImplementation(libs.compose.ui.test.manifest)
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.meloda.fast.auth.login
|
||||
|
||||
import androidx.compose.ui.test.assertHasClickAction
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import com.meloda.fast.auth.login.presentation.LoginScreen
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class LoginSignInTests {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun singInButton_isClickable() {
|
||||
composeTestRule.setContent {
|
||||
LoginScreen()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(testTag = "sing_in_fab").assertHasClickAction()
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,10 @@ import com.meloda.app.fast.data.db.AccountsRepository
|
||||
import com.meloda.app.fast.data.processState
|
||||
import com.meloda.app.fast.model.database.AccountEntity
|
||||
import com.meloda.app.fast.network.OAuthErrorDomain
|
||||
import com.meloda.fast.auth.login.model.LoginCaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginError
|
||||
import com.meloda.fast.auth.login.model.LoginScreenState
|
||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginValidationArguments
|
||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import com.meloda.fast.auth.login.model.LoginValidationResult
|
||||
import com.meloda.fast.auth.login.validation.LoginValidator
|
||||
@@ -36,10 +36,10 @@ interface LoginViewModel {
|
||||
val screenState: StateFlow<LoginScreenState>
|
||||
val loginError: StateFlow<LoginError?>
|
||||
|
||||
val twoFaCode: StateFlow<String?>
|
||||
val twoFaArguments: StateFlow<LoginTwoFaArguments?>
|
||||
val validationCode: StateFlow<String?>
|
||||
val validationArguments: StateFlow<LoginValidationArguments?>
|
||||
val captchaCode: StateFlow<String?>
|
||||
val captchaArguments: StateFlow<LoginCaptchaArguments?>
|
||||
val captchaArguments: StateFlow<CaptchaArguments?>
|
||||
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
||||
val isNeedToOpenMain: StateFlow<Boolean>
|
||||
|
||||
@@ -55,9 +55,9 @@ interface LoginViewModel {
|
||||
fun onNavigatedToMain()
|
||||
fun onNavigatedToUserBanned()
|
||||
fun onNavigatedToCaptcha()
|
||||
fun onNavigatedToTwoFa()
|
||||
fun onNavigatedToValidation()
|
||||
|
||||
fun onTwoFaCodeReceived(code: String)
|
||||
fun onValidationCodeReceived(code: String)
|
||||
fun onCaptchaCodeReceived(code: String)
|
||||
|
||||
fun onLogoLongClicked()
|
||||
@@ -73,10 +73,10 @@ class LoginViewModelImpl(
|
||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||
override val loginError = MutableStateFlow<LoginError?>(null)
|
||||
|
||||
override val twoFaCode = MutableStateFlow<String?>(null)
|
||||
override val twoFaArguments = MutableStateFlow<LoginTwoFaArguments?>(null)
|
||||
override val validationCode = MutableStateFlow<String?>(null)
|
||||
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||
override val captchaCode = MutableStateFlow<String?>(null)
|
||||
override val captchaArguments = MutableStateFlow<LoginCaptchaArguments?>(null)
|
||||
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||
|
||||
@@ -125,12 +125,12 @@ class LoginViewModelImpl(
|
||||
captchaArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onNavigatedToTwoFa() {
|
||||
twoFaArguments.update { null }
|
||||
override fun onNavigatedToValidation() {
|
||||
validationArguments.update { null }
|
||||
}
|
||||
|
||||
override fun onTwoFaCodeReceived(code: String) {
|
||||
twoFaCode.update { code }
|
||||
override fun onValidationCodeReceived(code: String) {
|
||||
validationCode.update { code }
|
||||
|
||||
login()
|
||||
}
|
||||
@@ -186,7 +186,7 @@ class LoginViewModelImpl(
|
||||
"LoginViewModel",
|
||||
"auth: login: ${currentState.login}; " +
|
||||
"password: ${currentState.password}; " +
|
||||
"2fa code: ${twoFaCode.value}; " +
|
||||
"2fa code: ${validationCode.value}; " +
|
||||
"captcha code: ${captchaCode.value}"
|
||||
)
|
||||
|
||||
@@ -197,7 +197,7 @@ class LoginViewModelImpl(
|
||||
login = currentState.login,
|
||||
password = currentState.password,
|
||||
forceSms = forceSms,
|
||||
twoFaCode = twoFaCode.value,
|
||||
validationCode = validationCode.value,
|
||||
captchaSid = captchaArguments.value?.captchaSid,
|
||||
captchaKey = captchaCode.value
|
||||
).listenValue { state ->
|
||||
@@ -205,7 +205,7 @@ class LoginViewModelImpl(
|
||||
error = { error ->
|
||||
Log.d("LoginViewModelImpl", "login: error: $error")
|
||||
|
||||
twoFaCode.update { null }
|
||||
validationCode.update { null }
|
||||
captchaCode.update { null }
|
||||
|
||||
parseError(error)
|
||||
@@ -229,7 +229,7 @@ class LoginViewModelImpl(
|
||||
userId = userId,
|
||||
accessToken = accessToken,
|
||||
fastToken = null,
|
||||
trustedHash = response.twoFaHash
|
||||
trustedHash = response.validationHash
|
||||
).also { account ->
|
||||
UserConfig.currentUserId = account.userId
|
||||
UserConfig.userId = account.userId
|
||||
@@ -243,8 +243,8 @@ class LoginViewModelImpl(
|
||||
captchaArguments.update { null }
|
||||
captchaCode.update { null }
|
||||
|
||||
twoFaArguments.update { null }
|
||||
twoFaCode.update { null }
|
||||
validationArguments.update { null }
|
||||
validationCode.update { null }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
@@ -265,7 +265,7 @@ class LoginViewModelImpl(
|
||||
is State.Error.OAuthError -> {
|
||||
when (val error = stateError.error) {
|
||||
is OAuthErrorDomain.ValidationRequiredError -> {
|
||||
val arguments = LoginTwoFaArguments(
|
||||
val arguments = LoginValidationArguments(
|
||||
validationSid = error.validationSid,
|
||||
redirectUri = error.redirectUri,
|
||||
phoneMask = error.phoneMask,
|
||||
@@ -273,13 +273,13 @@ class LoginViewModelImpl(
|
||||
canResendSms = error.validationResend == "sms",
|
||||
wrongCodeError = null
|
||||
)
|
||||
twoFaArguments.update { arguments }
|
||||
validationArguments.update { arguments }
|
||||
}
|
||||
|
||||
is OAuthErrorDomain.CaptchaRequiredError -> {
|
||||
val arguments = LoginCaptchaArguments(
|
||||
val arguments = CaptchaArguments(
|
||||
captchaSid = error.captchaSid,
|
||||
captchaImage = error.captchaImageUrl
|
||||
captchaImageUrl = error.captchaImageUrl
|
||||
)
|
||||
captchaArguments.update { arguments }
|
||||
}
|
||||
@@ -298,12 +298,12 @@ class LoginViewModelImpl(
|
||||
userBannedArguments.update { arguments }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongTwoFaCode -> {
|
||||
loginError.update { LoginError.WrongTwoFaCode }
|
||||
OAuthErrorDomain.WrongValidationCode -> {
|
||||
loginError.update { LoginError.WrongValidationCode }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.WrongTwoFaCodeFormat -> {
|
||||
loginError.update { LoginError.WrongTwoFaCodeFormat }
|
||||
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
||||
loginError.update { LoginError.WrongValidationCodeFormat }
|
||||
}
|
||||
|
||||
OAuthErrorDomain.TooManyTriesError -> {
|
||||
@@ -320,102 +320,6 @@ class LoginViewModelImpl(
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
||||
// return when (val error =
|
||||
// (stateError as? State.Error.OAuthError<*>)?.error) {
|
||||
// null -> false
|
||||
|
||||
// is CaptchaRequiredError -> {
|
||||
// val captchaArguments = CaptchaArguments(
|
||||
// captchaSid = error.captchaSid,
|
||||
// captchaImage = error.captchaImage,
|
||||
// )
|
||||
//
|
||||
// screenState.setValue { old ->
|
||||
// old.copy(
|
||||
// isNeedToOpenCaptcha = true,
|
||||
// captchaArguments = captchaArguments
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// is InvalidCredentialsError -> {
|
||||
// screenState.setValue { old -> old.copy(error = LoginError.WrongCredentials) }
|
||||
//
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// is UserBannedError -> {
|
||||
// val banInfo = error.banInfo
|
||||
//
|
||||
// val userBannedArguments = UserBannedArguments(
|
||||
// name = banInfo.memberName,
|
||||
// message = banInfo.message,
|
||||
// restoreUrl = banInfo.restoreUrl,
|
||||
// accessToken = banInfo.accessToken
|
||||
// )
|
||||
//
|
||||
// screenState.setValue { old ->
|
||||
// old.copy(
|
||||
// isNeedToOpenUserBanned = true,
|
||||
// userBannedArguments = userBannedArguments
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// is ValidationRequiredError -> {
|
||||
// val twoFaArguments = TwoFaArguments(
|
||||
// validationSid = error.validationSid,
|
||||
// redirectUri = error.redirectUri,
|
||||
// phoneMask = error.phoneMask,
|
||||
// validationType = error.validationType,
|
||||
// canResendSms = error.validationResend == "sms",
|
||||
// wrongCodeError = null
|
||||
// )
|
||||
//
|
||||
// screenState.setValue { old ->
|
||||
// old.copy(
|
||||
// isNeedToOpenTwoFa = true,
|
||||
// twoFaArguments = twoFaArguments
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// is WrongTwoFaCode -> {
|
||||
// screenState.setValue { old ->
|
||||
// old.copy(
|
||||
// isNeedToOpenTwoFa = true,
|
||||
// twoFaArguments = old.twoFaArguments?.copy(
|
||||
// wrongCodeError = UiText.Simple("Wrong code")
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// true
|
||||
// }
|
||||
//
|
||||
// is WrongTwoFaCodeFormat -> {
|
||||
// screenState.setValue { old ->
|
||||
// old.copy(
|
||||
// isNeedToOpenTwoFa = true,
|
||||
// twoFaArguments = old.twoFaArguments?.copy(
|
||||
// wrongCodeError = UiText.Simple("Wrong code format")
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// true
|
||||
// }
|
||||
|
||||
// else -> false
|
||||
// }
|
||||
}
|
||||
|
||||
private fun processValidation() {
|
||||
|
||||
@@ -10,7 +10,7 @@ interface OAuthUseCase {
|
||||
login: String,
|
||||
password: String,
|
||||
forceSms: Boolean,
|
||||
twoFaCode: String?,
|
||||
validationCode: String?,
|
||||
captchaSid: String?,
|
||||
captchaKey: String?
|
||||
): Flow<State<AuthInfo>>
|
||||
|
||||
@@ -18,7 +18,7 @@ class OAuthUseCaseImpl(
|
||||
login: String,
|
||||
password: String,
|
||||
forceSms: Boolean,
|
||||
twoFaCode: String?,
|
||||
validationCode: String?,
|
||||
captchaSid: String?,
|
||||
captchaKey: String?
|
||||
): Flow<State<AuthInfo>> = flow {
|
||||
@@ -27,7 +27,7 @@ class OAuthUseCaseImpl(
|
||||
val response = oAuthRepository.auth(
|
||||
login = login,
|
||||
password = password,
|
||||
twoFaCode = twoFaCode,
|
||||
validationCode = validationCode,
|
||||
captchaSid = captchaSid,
|
||||
captchaKey = captchaKey,
|
||||
forceSms = forceSms
|
||||
@@ -39,7 +39,7 @@ class OAuthUseCaseImpl(
|
||||
AuthInfo(
|
||||
userId = response.userId,
|
||||
accessToken = response.accessToken,
|
||||
twoFaHash = response.twoFaHash
|
||||
validationHash = response.validationHash
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -92,11 +92,11 @@ class OAuthUseCaseImpl(
|
||||
VkOAuthErrors.INVALID_REQUEST -> {
|
||||
when (response.errorType) {
|
||||
VkErrorTypes.WRONG_OTP -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongTwoFaCode)
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
|
||||
}
|
||||
|
||||
VkErrorTypes.WRONG_OTP_FORMAT -> {
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongTwoFaCodeFormat)
|
||||
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
@@ -3,5 +3,5 @@ package com.meloda.fast.auth.login.model
|
||||
data class AuthInfo(
|
||||
val userId: Int?,
|
||||
val accessToken: String?,
|
||||
val twoFaHash: String?
|
||||
val validationHash: String?
|
||||
)
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class LoginCaptchaArguments(
|
||||
data class CaptchaArguments(
|
||||
val captchaSid: String,
|
||||
val captchaImage: String
|
||||
val captchaImageUrl: String
|
||||
) : Parcelable
|
||||
@@ -7,6 +7,6 @@ sealed class LoginError {
|
||||
data object Unknown : LoginError()
|
||||
data object WrongCredentials : LoginError()
|
||||
data object TooManyTries : LoginError()
|
||||
data object WrongTwoFaCode : LoginError()
|
||||
data object WrongTwoFaCodeFormat : LoginError()
|
||||
data object WrongValidationCode : LoginError()
|
||||
data object WrongValidationCodeFormat : LoginError()
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class LoginTwoFaArguments(
|
||||
data class LoginValidationArguments(
|
||||
val validationSid: String,
|
||||
val redirectUri: String,
|
||||
val phoneMask: String,
|
||||
+16
-19
@@ -5,14 +5,13 @@ import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.meloda.app.fast.common.extensions.navigation.sharedViewModel
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.fast.auth.login.LoginViewModel
|
||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import com.meloda.fast.auth.login.model.LoginCaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
||||
import com.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginValidationArguments
|
||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import com.meloda.fast.auth.login.presentation.LoginScreen
|
||||
import com.meloda.fast.auth.login.presentation.LogoScreen
|
||||
import com.meloda.fast.auth.login.presentation.LoginRoute
|
||||
import com.meloda.fast.auth.login.presentation.LogoRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@@ -21,10 +20,9 @@ object Login
|
||||
@Serializable
|
||||
object Logo
|
||||
|
||||
fun NavGraphBuilder.loginRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
||||
fun NavGraphBuilder.loginScreen(
|
||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||
onNavigateToCredentials: () -> Unit,
|
||||
@@ -34,25 +32,24 @@ fun NavGraphBuilder.loginRoute(
|
||||
val viewModel: LoginViewModel =
|
||||
backStackEntry.sharedViewModel<LoginViewModelImpl>(navController = navController)
|
||||
|
||||
val twoFaCode = backStackEntry.getTwoFaResult()
|
||||
val validationCode = backStackEntry.getValidationResult()
|
||||
val captchaCode = backStackEntry.getCaptchaResult()
|
||||
|
||||
LoginScreen(
|
||||
onError = onError,
|
||||
LoginRoute(
|
||||
onNavigateToUserBanned = onNavigateToUserBanned,
|
||||
onNavigateToMain = onNavigateToMain,
|
||||
onNavigateToCaptcha = onNavigateToCaptcha,
|
||||
onNavigateToTwoFa = onNavigateToTwoFa,
|
||||
twoFaCode = twoFaCode,
|
||||
onNavigateToValidation = onNavigateToValidation,
|
||||
validationCode = validationCode,
|
||||
captchaCode = captchaCode,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
composable<Logo> {
|
||||
LogoScreen(
|
||||
LogoRoute(
|
||||
onNavigateToMain = onNavigateToMain,
|
||||
onShowCredentials = onNavigateToCredentials
|
||||
onGoNextButtonClicked = onNavigateToCredentials
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -61,10 +58,10 @@ fun NavController.navigateToLogin() {
|
||||
this.navigate(route = Login)
|
||||
}
|
||||
|
||||
fun NavBackStackEntry.getTwoFaResult(): String? {
|
||||
return savedStateHandle["twofacode"]
|
||||
fun NavBackStackEntry.getValidationResult(): String? {
|
||||
return savedStateHandle["validation_code"]
|
||||
}
|
||||
|
||||
fun NavBackStackEntry.getCaptchaResult(): String? {
|
||||
return savedStateHandle["captchacode"]
|
||||
return savedStateHandle["captcha_code"]
|
||||
}
|
||||
+58
-33
@@ -55,25 +55,23 @@ import com.meloda.app.fast.designsystem.connectNode
|
||||
import com.meloda.app.fast.designsystem.defaultFocusChangeAutoFill
|
||||
import com.meloda.app.fast.designsystem.handleEnterKey
|
||||
import com.meloda.app.fast.designsystem.handleTabKey
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.fast.auth.login.LoginViewModel
|
||||
import com.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import com.meloda.fast.auth.login.model.LoginCaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.CaptchaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginError
|
||||
import com.meloda.fast.auth.login.model.LoginTwoFaArguments
|
||||
import com.meloda.fast.auth.login.model.LoginScreenState
|
||||
import com.meloda.fast.auth.login.model.LoginValidationArguments
|
||||
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
fun LoginRoute(
|
||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||
onNavigateToMain: () -> Unit,
|
||||
onNavigateToCaptcha: (LoginCaptchaArguments) -> Unit,
|
||||
onNavigateToTwoFa: (LoginTwoFaArguments) -> Unit,
|
||||
twoFaCode: String?,
|
||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||
validationCode: String?,
|
||||
captchaCode: String?,
|
||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||
) {
|
||||
@@ -81,7 +79,7 @@ fun LoginScreen(
|
||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||
val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle()
|
||||
val captchaArguments by viewModel.captchaArguments.collectAsStateWithLifecycle()
|
||||
val twoFaArguments by viewModel.twoFaArguments.collectAsStateWithLifecycle()
|
||||
val validationArguments by viewModel.validationArguments.collectAsStateWithLifecycle()
|
||||
val loginError by viewModel.loginError.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isNeedToOpenMain) {
|
||||
@@ -105,16 +103,16 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(twoFaArguments) {
|
||||
twoFaArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToTwoFa()
|
||||
onNavigateToTwoFa(arguments)
|
||||
LaunchedEffect(validationArguments) {
|
||||
validationArguments?.let { arguments ->
|
||||
viewModel.onNavigatedToValidation()
|
||||
onNavigateToValidation(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(twoFaCode) {
|
||||
if (twoFaCode != null) {
|
||||
viewModel.onTwoFaCodeReceived(twoFaCode)
|
||||
LaunchedEffect(validationCode) {
|
||||
if (validationCode != null) {
|
||||
viewModel.onValidationCodeReceived(validationCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,9 +122,41 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LoginScreen(
|
||||
screenState = screenState,
|
||||
onLoginAutoFilled = viewModel::onLoginInputChanged,
|
||||
onPasswordAutoFilled = viewModel::onPasswordInputChanged,
|
||||
onLoginInputChanged = viewModel::onLoginInputChanged,
|
||||
onPasswordInputChanged = viewModel::onPasswordInputChanged,
|
||||
onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked,
|
||||
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
||||
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
||||
onSignInButtonClicked = viewModel::onSignInButtonClicked
|
||||
)
|
||||
|
||||
HandleError(
|
||||
onDismiss = viewModel::onErrorDialogDismissed,
|
||||
error = loginError
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
screenState: LoginScreenState = LoginScreenState.EMPTY,
|
||||
onLoginAutoFilled: (String) -> Unit = {},
|
||||
onPasswordAutoFilled: (String) -> Unit = {},
|
||||
onLoginInputChanged: (String) -> Unit = {},
|
||||
onPasswordInputChanged: (String) -> Unit = {},
|
||||
onPasswordFieldEnterKeyClicked: () -> Unit = {},
|
||||
onPasswordVisibilityButtonClicked: () -> Unit = {},
|
||||
onPasswordFieldGoAction: () -> Unit = {},
|
||||
onSignInButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
|
||||
|
||||
// TODO: 13/07/2024, Danil Nikolaev: remove
|
||||
var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) }
|
||||
val showLoginError = screenState.loginError
|
||||
|
||||
@@ -135,7 +165,7 @@ fun LoginScreen(
|
||||
onFill = { value ->
|
||||
loginText =
|
||||
TextFieldValue(text = value, selection = TextRange(value.length))
|
||||
viewModel.onLoginInputChanged(value)
|
||||
onLoginAutoFilled(value)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -147,7 +177,7 @@ fun LoginScreen(
|
||||
onFill = { value ->
|
||||
passwordText =
|
||||
TextFieldValue(text = value, selection = TextRange(value.length))
|
||||
viewModel.onPasswordInputChanged(value)
|
||||
onPasswordAutoFilled(value)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -200,7 +230,7 @@ fun LoginScreen(
|
||||
}
|
||||
|
||||
loginText = newText
|
||||
viewModel.onLoginInputChanged(text)
|
||||
onLoginInputChanged(text)
|
||||
},
|
||||
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
||||
placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
||||
@@ -236,7 +266,7 @@ fun LoginScreen(
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.handleEnterKey {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onSignInButtonClicked()
|
||||
onPasswordFieldEnterKeyClicked()
|
||||
true
|
||||
}
|
||||
.focusRequester(passwordFocusable)
|
||||
@@ -250,7 +280,7 @@ fun LoginScreen(
|
||||
}
|
||||
|
||||
passwordText = newText
|
||||
viewModel.onPasswordInputChanged(text)
|
||||
onPasswordInputChanged(text)
|
||||
},
|
||||
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
||||
placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
||||
@@ -271,7 +301,7 @@ fun LoginScreen(
|
||||
else UiR.drawable.round_visibility_24
|
||||
)
|
||||
|
||||
IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) {
|
||||
IconButton(onClick = onPasswordVisibilityButtonClicked) {
|
||||
Icon(
|
||||
painter = imagePainter,
|
||||
contentDescription = if (screenState.passwordVisible) "Password visible icon"
|
||||
@@ -286,7 +316,7 @@ fun LoginScreen(
|
||||
keyboardActions = KeyboardActions(
|
||||
onGo = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onSignInButtonClicked()
|
||||
onPasswordFieldGoAction()
|
||||
}
|
||||
),
|
||||
isError = showPasswordError,
|
||||
@@ -310,10 +340,10 @@ fun LoginScreen(
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onSignInButtonClicked()
|
||||
onSignInButtonClicked()
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
modifier = Modifier.testTag("Sign in button")
|
||||
modifier = Modifier.testTag("sing_in_fab")
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_arrow_end),
|
||||
@@ -332,11 +362,6 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HandleError(
|
||||
onDismiss = viewModel::onErrorDialogDismissed,
|
||||
error = loginError
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -375,7 +400,7 @@ fun HandleError(
|
||||
}
|
||||
|
||||
|
||||
LoginError.WrongTwoFaCode -> {
|
||||
LoginError.WrongValidationCode -> {
|
||||
MaterialDialog(
|
||||
onDismissAction = onDismiss,
|
||||
title = UiText.Simple("Error"),
|
||||
@@ -384,7 +409,7 @@ fun HandleError(
|
||||
)
|
||||
}
|
||||
|
||||
LoginError.WrongTwoFaCodeFormat -> {
|
||||
LoginError.WrongValidationCodeFormat -> {
|
||||
MaterialDialog(
|
||||
onDismissAction = onDismiss,
|
||||
title = UiText.Simple("Error"),
|
||||
|
||||
+30
-9
@@ -34,11 +34,10 @@ import com.meloda.fast.auth.login.LoginViewModelImpl
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LogoScreen(
|
||||
fun LogoRoute(
|
||||
onNavigateToMain: () -> Unit,
|
||||
onShowCredentials: () -> Unit,
|
||||
onGoNextButtonClicked: () -> Unit,
|
||||
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||
) {
|
||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||
@@ -50,15 +49,37 @@ fun LogoScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LogoScreen(
|
||||
onLogoLongClicked = viewModel::onLogoLongClicked,
|
||||
onGoNextButtonClicked = onGoNextButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// TODO: 13/07/2024, Danil Nikolaev: replace with scaffold?
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LogoScreen(
|
||||
onLogoLongClicked: () -> Unit = {},
|
||||
onGoNextButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
Scaffold { padding ->
|
||||
val topPadding by animateDpAsState(targetValue = padding.calculateTopPadding())
|
||||
val bottomPadding by animateDpAsState(targetValue = padding.calculateBottomPadding())
|
||||
val topPadding by animateDpAsState(
|
||||
targetValue = padding.calculateTopPadding(),
|
||||
label = "topPaddingAnimation"
|
||||
)
|
||||
val bottomPadding by animateDpAsState(
|
||||
targetValue = padding.calculateBottomPadding(),
|
||||
label = "bottomPaddingAnimation"
|
||||
)
|
||||
|
||||
val endPadding by animateDpAsState(
|
||||
targetValue = padding.calculateEndPadding(LayoutDirection.Ltr)
|
||||
targetValue = padding.calculateEndPadding(LayoutDirection.Ltr),
|
||||
label = "endPaddingAnimation"
|
||||
)
|
||||
val startPadding by animateDpAsState(
|
||||
targetValue = padding.calculateStartPadding(LayoutDirection.Ltr)
|
||||
targetValue = padding.calculateStartPadding(LayoutDirection.Ltr),
|
||||
label = "startPaddingAnimation"
|
||||
)
|
||||
|
||||
Box(
|
||||
@@ -85,7 +106,7 @@ fun LogoScreen(
|
||||
modifier = Modifier.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onLongClick = viewModel::onLogoLongClicked,
|
||||
onLongClick = onLogoLongClicked,
|
||||
onClick = {}
|
||||
)
|
||||
)
|
||||
@@ -98,7 +119,7 @@ fun LogoScreen(
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = onShowCredentials,
|
||||
onClick = onGoNextButtonClicked,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
) {
|
||||
|
||||
@@ -3,20 +3,19 @@ package com.meloda.app.fast.auth
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.navigation
|
||||
import com.meloda.app.fast.auth.captcha.model.CaptchaArguments
|
||||
import com.meloda.app.fast.auth.captcha.navigation.captchaRoute
|
||||
import com.meloda.app.fast.auth.captcha.navigation.captchaScreen
|
||||
import com.meloda.app.fast.auth.captcha.navigation.navigateToCaptcha
|
||||
import com.meloda.app.fast.auth.captcha.navigation.setCaptchaResult
|
||||
import com.meloda.app.fast.auth.twofa.model.TwoFaArguments
|
||||
import com.meloda.app.fast.auth.twofa.navigation.navigateToTwoFa
|
||||
import com.meloda.app.fast.auth.twofa.navigation.setTwoFaResult
|
||||
import com.meloda.app.fast.auth.twofa.navigation.twoFaRoute
|
||||
import com.meloda.app.fast.auth.validation.model.ValidationArguments
|
||||
import com.meloda.app.fast.auth.validation.navigation.navigateToValidation
|
||||
import com.meloda.app.fast.auth.validation.navigation.setValidationResult
|
||||
import com.meloda.app.fast.auth.validation.navigation.validationScreen
|
||||
import com.meloda.app.fast.model.BaseError
|
||||
import com.meloda.app.fast.userbanned.model.UserBannedArguments
|
||||
import com.meloda.app.fast.userbanned.navigation.navigateToUserBanned
|
||||
import com.meloda.app.fast.userbanned.navigation.userBannedRoute
|
||||
import com.meloda.fast.auth.login.navigation.Logo
|
||||
import com.meloda.fast.auth.login.navigation.loginRoute
|
||||
import com.meloda.fast.auth.login.navigation.loginScreen
|
||||
import com.meloda.fast.auth.login.navigation.navigateToLogin
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.net.URLEncoder
|
||||
@@ -25,26 +24,21 @@ import java.net.URLEncoder
|
||||
object AuthGraph
|
||||
|
||||
fun NavGraphBuilder.authNavGraph(
|
||||
onError: (BaseError) -> Unit,
|
||||
onNavigateToMain: () -> Unit,
|
||||
navController: NavController
|
||||
) {
|
||||
navigation<AuthGraph>(
|
||||
startDestination = Logo
|
||||
) {
|
||||
loginRoute(
|
||||
onError = onError,
|
||||
loginScreen(
|
||||
onNavigateToCaptcha = { arguments ->
|
||||
navController.navigateToCaptcha(
|
||||
CaptchaArguments(
|
||||
arguments.captchaSid,
|
||||
URLEncoder.encode(arguments.captchaImage, "utf-8")
|
||||
)
|
||||
captchaImageUrl = URLEncoder.encode(arguments.captchaImageUrl, "utf-8")
|
||||
)
|
||||
},
|
||||
onNavigateToTwoFa = { arguments ->
|
||||
navController.navigateToTwoFa(
|
||||
TwoFaArguments(
|
||||
onNavigateToValidation = { arguments ->
|
||||
navController.navigateToValidation(
|
||||
ValidationArguments(
|
||||
validationSid = arguments.validationSid,
|
||||
redirectUri = URLEncoder.encode(arguments.redirectUri, "utf-8"),
|
||||
phoneMask = arguments.phoneMask,
|
||||
@@ -58,7 +52,7 @@ fun NavGraphBuilder.authNavGraph(
|
||||
onNavigateToUserBanned = { arguments ->
|
||||
navController.navigateToUserBanned(
|
||||
UserBannedArguments(
|
||||
name = arguments.name,
|
||||
userName = arguments.name,
|
||||
message = arguments.message,
|
||||
restoreUrl = arguments.restoreUrl,
|
||||
accessToken = arguments.accessToken
|
||||
@@ -69,18 +63,18 @@ fun NavGraphBuilder.authNavGraph(
|
||||
navController = navController
|
||||
)
|
||||
|
||||
twoFaRoute(
|
||||
validationScreen(
|
||||
onBack = {
|
||||
navController.navigateUp()
|
||||
navController.setTwoFaResult(null)
|
||||
navController.setValidationResult(null)
|
||||
},
|
||||
onResult = { code ->
|
||||
navController.popBackStack()
|
||||
navController.setTwoFaResult(code)
|
||||
navController.setValidationResult(code)
|
||||
}
|
||||
)
|
||||
|
||||
captchaRoute(
|
||||
captchaScreen(
|
||||
onBack = {
|
||||
navController.navigateUp()
|
||||
navController.setCaptchaResult(null)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.meloda.app.fast.auth
|
||||
|
||||
import com.meloda.app.fast.auth.captcha.di.captchaModule
|
||||
import com.meloda.app.fast.auth.twofa.di.twoFaModule
|
||||
import com.meloda.app.fast.auth.validation.di.validationModule
|
||||
import com.meloda.fast.auth.login.di.loginModule
|
||||
import org.koin.dsl.module
|
||||
|
||||
val authModule = module {
|
||||
includes(
|
||||
loginModule,
|
||||
twoFaModule,
|
||||
validationModule,
|
||||
captchaModule,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
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 phoneMask: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = TwoFaScreenState(
|
||||
twoFaSid = "",
|
||||
twoFaCode = null,
|
||||
twoFaText = UiText.Simple(""),
|
||||
canResendSms = false,
|
||||
codeError = null,
|
||||
delayTime = 0,
|
||||
phoneMask = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
|
||||
sealed class TwoFaValidationResult {
|
||||
data object Empty : TwoFaValidationResult()
|
||||
data object Valid : TwoFaValidationResult()
|
||||
|
||||
fun isValid() = this == Valid
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
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.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(
|
||||
onBack = onBack,
|
||||
onCodeResult = onResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToTwoFa(arguments: TwoFaArguments) {
|
||||
this.navigate(TwoFa(arguments))
|
||||
}
|
||||
|
||||
fun NavController.setTwoFaResult(code: String?) {
|
||||
this.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("twofacode", code)
|
||||
}
|
||||
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,4 +57,6 @@ dependencies {
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.kotlin.serialization)
|
||||
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class UserBannedArguments(
|
||||
val name: String,
|
||||
val userName: String,
|
||||
val message: String,
|
||||
val restoreUrl: String,
|
||||
val accessToken: String
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.meloda.app.fast.userbanned.model
|
||||
|
||||
data class UserBannedScreenState(
|
||||
val userName: String,
|
||||
val message: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY: UserBannedScreenState = UserBannedScreenState(
|
||||
userName = "",
|
||||
message = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -8,7 +8,7 @@ import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import com.meloda.app.fast.userbanned.model.UserBannedArguments
|
||||
import com.meloda.app.fast.userbanned.presentation.UserBannedScreen
|
||||
import com.meloda.app.fast.userbanned.presentation.UserBannedRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -32,18 +32,16 @@ val UserBannedNavType = object : NavType<UserBannedArguments>(isNullableAllowed
|
||||
override val name: String = "UserBannedArguments"
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.userBannedRoute(
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
fun NavGraphBuilder.userBannedRoute(onBack: () -> Unit) {
|
||||
composable<UserBanned>(
|
||||
typeMap = mapOf(typeOf<UserBannedArguments>() to UserBannedNavType)
|
||||
) { backStackEntry ->
|
||||
val arguments: UserBannedArguments = backStackEntry.toRoute()
|
||||
|
||||
UserBannedScreen(
|
||||
UserBannedRoute(
|
||||
onBack = onBack,
|
||||
name = arguments.name,
|
||||
message = arguments.message,
|
||||
userName = arguments.userName,
|
||||
message = arguments.message
|
||||
)
|
||||
}
|
||||
}
|
||||
+29
-11
@@ -14,6 +14,7 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
@@ -22,27 +23,44 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.meloda.app.fast.designsystem.AppTheme
|
||||
import com.meloda.app.fast.userbanned.model.UserBannedScreenState
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun UserBannedScreenPreview() {
|
||||
AppTheme {
|
||||
UserBannedScreen(
|
||||
onBack = {},
|
||||
name = "Calvin Harris",
|
||||
message = "Eto konets"
|
||||
UserBannedScreen(
|
||||
screenState = UserBannedScreenState(
|
||||
userName = "Andre Shultz",
|
||||
message = "Bruteforce"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserBannedRoute(
|
||||
onBack: () -> Unit,
|
||||
userName: String,
|
||||
message: String
|
||||
) {
|
||||
val screenState = remember(userName, message) {
|
||||
UserBannedScreenState(
|
||||
userName = userName,
|
||||
message = message
|
||||
)
|
||||
}
|
||||
|
||||
UserBannedScreen(
|
||||
screenState = screenState,
|
||||
onBack = onBack
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun UserBannedScreen(
|
||||
onBack: () -> Unit,
|
||||
name: String,
|
||||
message: String,
|
||||
screenState: UserBannedScreenState = UserBannedScreenState.EMPTY,
|
||||
onBack: () -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -80,7 +98,7 @@ fun UserBannedScreen(
|
||||
append(": ")
|
||||
}
|
||||
|
||||
append(name)
|
||||
append(screenState.userName)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
@@ -89,7 +107,7 @@ fun UserBannedScreen(
|
||||
append(stringResource(id = UiR.string.blocking_reason_title))
|
||||
append(": ")
|
||||
}
|
||||
append(message)
|
||||
append(screenState.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.meloda.app.fast.twofa"
|
||||
namespace = "com.meloda.app.fast.validation"
|
||||
compileSdk = Configs.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.meloda.app.fast.auth.twofa
|
||||
package com.meloda.app.fast.auth.validation
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.model.api.responses.SendSmsResponse
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.meloda.app.fast.auth.twofa
|
||||
package com.meloda.app.fast.auth.validation
|
||||
|
||||
import com.meloda.app.fast.data.State
|
||||
import com.meloda.app.fast.data.api.auth.AuthRepository
|
||||
+35
-34
@@ -1,12 +1,12 @@
|
||||
package com.meloda.app.fast.auth.twofa
|
||||
package com.meloda.app.fast.auth.validation
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.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
|
||||
@@ -22,9 +22,9 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
interface TwoFaViewModel {
|
||||
interface ValidationViewModel {
|
||||
|
||||
val screenState: StateFlow<TwoFaScreenState>
|
||||
val screenState: StateFlow<ValidationScreenState>
|
||||
|
||||
val isNeedToOpenLogin: StateFlow<Boolean>
|
||||
|
||||
@@ -33,36 +33,39 @@ interface TwoFaViewModel {
|
||||
fun onBackButtonClicked()
|
||||
fun onCancelButtonClicked()
|
||||
fun onRequestSmsButtonClicked()
|
||||
fun onTextFieldDoneClicked()
|
||||
fun onTextFieldDoneAction()
|
||||
fun onDoneButtonClicked()
|
||||
|
||||
fun onNavigatedToLogin()
|
||||
}
|
||||
|
||||
class TwoFaViewModelImpl(
|
||||
private val validator: TwoFaValidator,
|
||||
class ValidationViewModelImpl(
|
||||
private val validator: ValidationValidator,
|
||||
private val authUseCase: AuthUseCase,
|
||||
savedStateHandle: SavedStateHandle
|
||||
) : TwoFaViewModel, ViewModel() {
|
||||
) : ValidationViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(TwoFaScreenState.EMPTY)
|
||||
override val screenState = MutableStateFlow(ValidationScreenState.EMPTY)
|
||||
|
||||
override val isNeedToOpenLogin = MutableStateFlow(false)
|
||||
|
||||
private var validationSid: String? = null
|
||||
|
||||
private var delayJob: Job? = null
|
||||
|
||||
init {
|
||||
// TODO: 08/07/2024, Danil Nikolaev: use when fixed
|
||||
//savedStateHandle.toRoute<TwoFa>().arguments
|
||||
//savedStateHandle.toRoute<Validation>().arguments
|
||||
|
||||
val arguments = TwoFa.from(savedStateHandle).arguments
|
||||
val arguments = Validation.from(savedStateHandle).arguments
|
||||
|
||||
validationSid = arguments.validationSid
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
twoFaSid = arguments.validationSid,
|
||||
canResendSms = arguments.canResendSms,
|
||||
isSmsButtonVisible = arguments.canResendSms,
|
||||
codeError = arguments.wrongCodeError,
|
||||
twoFaText = getTwoFaText(TwoFaValidationType.parse(arguments.validationType)),
|
||||
validationText = getValidationText(ValidationType.parse(arguments.validationType)),
|
||||
phoneMask = arguments.phoneMask
|
||||
)
|
||||
}
|
||||
@@ -71,7 +74,7 @@ class TwoFaViewModelImpl(
|
||||
override fun onCodeInputChanged(newCode: String) {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(
|
||||
twoFaCode = newCode.trim(),
|
||||
code = newCode.trim(),
|
||||
codeError = null
|
||||
)
|
||||
)
|
||||
@@ -89,7 +92,7 @@ class TwoFaViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onCancelButtonClicked() {
|
||||
screenState.setValue { old -> old.copy(twoFaCode = null) }
|
||||
screenState.setValue { old -> old.copy(code = null) }
|
||||
isNeedToOpenLogin.update { true }
|
||||
}
|
||||
|
||||
@@ -97,7 +100,7 @@ class TwoFaViewModelImpl(
|
||||
sendValidationCode()
|
||||
}
|
||||
|
||||
override fun onTextFieldDoneClicked() {
|
||||
override fun onTextFieldDoneAction() {
|
||||
onDoneButtonClicked()
|
||||
}
|
||||
|
||||
@@ -108,7 +111,7 @@ class TwoFaViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onNavigatedToLogin() {
|
||||
screenState.updateValue(TwoFaScreenState.EMPTY)
|
||||
screenState.updateValue(ValidationScreenState.EMPTY)
|
||||
isNeedToOpenLogin.update { false }
|
||||
}
|
||||
|
||||
@@ -126,9 +129,7 @@ class TwoFaViewModelImpl(
|
||||
}
|
||||
|
||||
private fun sendValidationCode() {
|
||||
val validationSid = screenState.value.twoFaSid
|
||||
|
||||
authUseCase.sendSms(validationSid)
|
||||
authUseCase.sendSms(validationSid.orEmpty())
|
||||
.listenValue { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
@@ -140,9 +141,9 @@ class TwoFaViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
canResendSms = newCanResendSms,
|
||||
twoFaText = getTwoFaText(
|
||||
TwoFaValidationType.parse(newValidationType.orEmpty())
|
||||
isSmsButtonVisible = newCanResendSms,
|
||||
validationText = getValidationText(
|
||||
ValidationType.parse(newValidationType.orEmpty())
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -152,7 +153,7 @@ class TwoFaViewModelImpl(
|
||||
)
|
||||
|
||||
if (state.isLoading()) {
|
||||
screenState.emit(screenState.value.copy(canResendSms = false))
|
||||
screenState.emit(screenState.value.copy(isSmsButtonVisible = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +165,7 @@ class TwoFaViewModelImpl(
|
||||
time = delay,
|
||||
onStartAction = {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(canResendSms = false)
|
||||
screenState.value.copy(isSmsButtonVisible = false)
|
||||
)
|
||||
},
|
||||
onTickAction = { remainedTime ->
|
||||
@@ -175,24 +176,24 @@ class TwoFaViewModelImpl(
|
||||
onTimeoutAction = {
|
||||
screenState.updateValue(
|
||||
screenState.value.copy(
|
||||
canResendSms = true
|
||||
isSmsButtonVisible = true
|
||||
)
|
||||
)
|
||||
},
|
||||
).launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun getTwoFaText(validationType: TwoFaValidationType): UiText {
|
||||
private fun getValidationText(validationType: ValidationType): UiText {
|
||||
return when (validationType) {
|
||||
TwoFaValidationType.Sms -> {
|
||||
ValidationType.Sms -> {
|
||||
UiText.Simple("SMS with the code is sent to ${screenState.value.phoneMask}")
|
||||
}
|
||||
|
||||
TwoFaValidationType.TwoFaApp -> {
|
||||
ValidationType.App -> {
|
||||
UiText.Simple("Enter the code from the code generator application")
|
||||
}
|
||||
|
||||
is TwoFaValidationType.Another -> UiText.Simple(validationType.type)
|
||||
is ValidationType.Other -> UiText.Simple(validationType.type)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.meloda.app.fast.auth.validation.di
|
||||
|
||||
import com.meloda.app.fast.auth.validation.AuthUseCase
|
||||
import com.meloda.app.fast.auth.validation.AuthUseCaseImpl
|
||||
import com.meloda.app.fast.auth.validation.ValidationViewModel
|
||||
import com.meloda.app.fast.auth.validation.ValidationViewModelImpl
|
||||
import com.meloda.app.fast.auth.validation.validation.ValidationValidator
|
||||
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 validationModule = module {
|
||||
singleOf(::ValidationValidator)
|
||||
viewModelOf(::ValidationViewModelImpl) bind ValidationViewModel::class
|
||||
singleOf(::AuthUseCaseImpl) bind AuthUseCase::class
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package com.meloda.app.fast.auth.twofa.model
|
||||
package com.meloda.app.fast.auth.validation.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Parcelize
|
||||
data class TwoFaArguments(
|
||||
data class ValidationArguments(
|
||||
val validationSid: String,
|
||||
val redirectUri: String,
|
||||
val phoneMask: String,
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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 isSmsButtonVisible: Boolean,
|
||||
val delayTime: Int,
|
||||
val phoneMask: String,
|
||||
|
||||
// TODO: 13/07/2024, Danil Nikolaev: check wtf is this
|
||||
val validationText: UiText,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = ValidationScreenState(
|
||||
code = null,
|
||||
codeError = null,
|
||||
isSmsButtonVisible = false,
|
||||
delayTime = 0,
|
||||
phoneMask = "",
|
||||
|
||||
validationText = UiText.Simple("")
|
||||
)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.meloda.app.fast.auth.validation.model
|
||||
|
||||
sealed class ValidationValidationResult {
|
||||
data object Empty : ValidationValidationResult()
|
||||
data object Valid : ValidationValidationResult()
|
||||
|
||||
fun isValid() = this == Valid
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.meloda.app.fast.auth.validation.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.validation.model.ValidationArguments
|
||||
import com.meloda.app.fast.auth.validation.presentation.ValidationRoute
|
||||
import com.meloda.app.fast.common.customNavType
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@Serializable
|
||||
data class Validation(val arguments: ValidationArguments) {
|
||||
companion object {
|
||||
val typeMap = mapOf(typeOf<ValidationArguments>() to customNavType<ValidationArguments>())
|
||||
|
||||
fun from(savedStateHandle: SavedStateHandle) =
|
||||
savedStateHandle.toRoute<Validation>(typeMap)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.validationScreen(
|
||||
onBack: () -> Unit,
|
||||
onResult: (String) -> Unit
|
||||
) {
|
||||
composable<Validation>(typeMap = Validation.typeMap) {
|
||||
ValidationRoute(
|
||||
onBack = onBack,
|
||||
onResult = onResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToValidation(arguments: ValidationArguments) {
|
||||
this.navigate(Validation(arguments))
|
||||
}
|
||||
|
||||
fun NavController.setValidationResult(code: String?) {
|
||||
this.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set("validation_code", code)
|
||||
}
|
||||
|
||||
|
||||
+55
-34
@@ -1,4 +1,4 @@
|
||||
package com.meloda.app.fast.auth.twofa.presentation
|
||||
package com.meloda.app.fast.auth.validation.presentation
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -42,8 +43,9 @@ 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.validation.ValidationViewModel
|
||||
import com.meloda.app.fast.auth.validation.ValidationViewModelImpl
|
||||
import com.meloda.app.fast.auth.validation.model.ValidationScreenState
|
||||
import com.meloda.app.fast.common.UiText
|
||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||
import com.meloda.app.fast.designsystem.TextFieldErrorText
|
||||
@@ -52,17 +54,48 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import com.meloda.app.fast.designsystem.R as UiR
|
||||
|
||||
@Composable
|
||||
fun TwoFaScreen(
|
||||
fun ValidationRoute(
|
||||
onBack: () -> Unit,
|
||||
onCodeResult: (code: String) -> Unit,
|
||||
viewModel: TwoFaViewModel = koinViewModel<TwoFaViewModelImpl>(),
|
||||
onResult: (String) -> Unit,
|
||||
viewModel: ValidationViewModel = koinViewModel<ValidationViewModelImpl>()
|
||||
) {
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isNeedToOpenLogin) {
|
||||
if (isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
|
||||
val code = screenState.code
|
||||
if (code == null) {
|
||||
onBack()
|
||||
} else {
|
||||
onResult(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ValidationScreen(
|
||||
screenState = screenState,
|
||||
onBack = onBack,
|
||||
onCodeInputChanged = viewModel::onCodeInputChanged,
|
||||
onTextFieldDoneAction = viewModel::onTextFieldDoneAction,
|
||||
onRequestSmsButtonClicked = viewModel::onRequestSmsButtonClicked,
|
||||
onDoneButtonClicked = viewModel::onDoneButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ValidationScreen(
|
||||
screenState: ValidationScreenState = ValidationScreenState.EMPTY,
|
||||
onBack: () -> Unit = {},
|
||||
onCodeInputChanged: (String) -> Unit = {},
|
||||
onTextFieldDoneAction: () -> Unit = {},
|
||||
onRequestSmsButtonClicked: () -> Unit = {},
|
||||
onDoneButtonClicked: () -> Unit = {}
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||
|
||||
val isNeedToOpenLogin by viewModel.isNeedToOpenLogin.collectAsStateWithLifecycle()
|
||||
|
||||
var confirmedExit by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@@ -96,20 +129,7 @@ fun TwoFaScreen(
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(isNeedToOpenLogin) {
|
||||
if (isNeedToOpenLogin) {
|
||||
viewModel.onNavigatedToLogin()
|
||||
|
||||
val code = screenState.twoFaCode
|
||||
if (code == null) {
|
||||
onBack()
|
||||
} else {
|
||||
onCodeResult(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.twoFaCode.orEmpty())) }
|
||||
var code by remember { mutableStateOf(TextFieldValue(screenState.code.orEmpty())) }
|
||||
val codeError = screenState.codeError
|
||||
|
||||
Scaffold { padding ->
|
||||
@@ -140,7 +160,6 @@ fun TwoFaScreen(
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = "Two-Factor\nAuthentication",
|
||||
style = MaterialTheme.typography.displayMedium,
|
||||
@@ -148,16 +167,18 @@ fun TwoFaScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(38.dp))
|
||||
Text(
|
||||
text = screenState.twoFaText.getString().orEmpty(),
|
||||
text = screenState.validationText.getString().orEmpty(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
val delayRemainedTime = screenState.delayTime
|
||||
AnimatedVisibility(visible = delayRemainedTime > 0) {
|
||||
val isResendTextVisible by remember {
|
||||
derivedStateOf { screenState.delayTime > 0 }
|
||||
}
|
||||
AnimatedVisibility(visible = isResendTextVisible) {
|
||||
Text(
|
||||
text = "Can resend after $delayRemainedTime seconds",
|
||||
text = "Can resend after ${screenState.delayTime} seconds",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
@@ -169,7 +190,7 @@ fun TwoFaScreen(
|
||||
if (newText.text.length > 6) return@TextField
|
||||
|
||||
code = newText
|
||||
viewModel.onCodeInputChanged((newText.text))
|
||||
onCodeInputChanged((newText.text))
|
||||
},
|
||||
label = { Text(text = "Code") },
|
||||
placeholder = { Text(text = "Code") },
|
||||
@@ -195,7 +216,7 @@ fun TwoFaScreen(
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
focusManager.clearFocus()
|
||||
viewModel.onTextFieldDoneClicked()
|
||||
onTextFieldDoneAction()
|
||||
}
|
||||
),
|
||||
isError = codeError != null
|
||||
@@ -211,13 +232,13 @@ fun TwoFaScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
val canResendSms = screenState.canResendSms
|
||||
val canResendSms = screenState.isSmsButtonVisible
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = canResendSms,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = viewModel::onRequestSmsButtonClicked,
|
||||
onClick = onRequestSmsButtonClicked,
|
||||
text = {
|
||||
Text(
|
||||
text = "Request SMS",
|
||||
@@ -238,7 +259,7 @@ fun TwoFaScreen(
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = viewModel::onDoneButtonClicked,
|
||||
onClick = onDoneButtonClicked,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
) {
|
||||
Icon(
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.meloda.app.fast.auth.validation.validation
|
||||
|
||||
import com.meloda.app.fast.auth.validation.model.ValidationScreenState
|
||||
import com.meloda.app.fast.auth.validation.model.ValidationValidationResult
|
||||
|
||||
class ValidationValidator {
|
||||
|
||||
fun validate(screenState: ValidationScreenState): ValidationValidationResult {
|
||||
return when {
|
||||
screenState.code.isNullOrEmpty() -> ValidationValidationResult.Empty
|
||||
else -> ValidationValidationResult.Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user