feat: set new password ui and viewModel (#2857)

This commit is contained in:
Nagarjuna 2025-07-09 15:34:53 +05:30 committed by GitHub
parent 5b16aa380b
commit ca5bb5902b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 685 additions and 14 deletions

View File

@ -147,6 +147,22 @@
<string name="feature_otp_required_error">OTP is required</string>
<string name="feature_otp_invalid_error">Enter valid OTP</string>
<!--Set New Password-->
<string name="feature_set_new_password_title">Set Password</string>
<string name="feature_set_new_password_message">Set your new high security password now!</string>
<string name="feature_set_new_password_new_password_label">New Password</string>
<string name="feature_set_new_password_confirm_password_label">Confirm New Password</string>
<string name="feature_set_new_password_submit">Submit</string>
<string name="feature_set_new_password_tip">Remember your password?</string>
<string name="feature_set_new_password_action_tip">Log in</string>
<string name="feature_common_error_password_required_error">Password is Required</string>
<string name="feature_common_error_password_short">Password must be at least 6 characters</string>
<string name="feature_common_error_password_mismatch">Passwords do not match</string>
<!--Recover Password-->
<string name="feature_recover_now_title">Recover Now</string>
<string name="feature_recover_now_message">Forgot your password, dont worry!, we got you covered. Reset your password now!</string>

View File

@ -15,6 +15,7 @@ import org.mifos.mobile.feature.auth.login.LoginViewModel
import org.mifos.mobile.feature.auth.otpAuthentication.OtpAuthenticationViewModel
import org.mifos.mobile.feature.auth.recoverPassword.RecoverPasswordViewModel
import org.mifos.mobile.feature.auth.registration.RegistrationViewModel
import org.mifos.mobile.feature.auth.setNewPassword.SetPasswordViewModel
import org.mifos.mobile.feature.auth.uploadId.UploadIdViewModel
val AuthModule = module {
@ -22,5 +23,6 @@ val AuthModule = module {
viewModelOf(::RegistrationViewModel)
viewModelOf(::UploadIdViewModel)
viewModelOf(::OtpAuthenticationViewModel)
viewModelOf(::SetPasswordViewModel)
viewModelOf(::RecoverPasswordViewModel)
}

View File

@ -27,6 +27,8 @@ import org.mifos.mobile.feature.auth.recoverPassword.navigateToRecoverPasswordSc
import org.mifos.mobile.feature.auth.recoverPassword.recoverPasswordDestination
import org.mifos.mobile.feature.auth.registration.navigateToRegisterScreen
import org.mifos.mobile.feature.auth.registration.registrationDestination
import org.mifos.mobile.feature.auth.setNewPassword.navigateToSetPasswordScreen
import org.mifos.mobile.feature.auth.setNewPassword.setPasswordDestination
import org.mifos.mobile.feature.auth.status.navigateToStatusScreen
import org.mifos.mobile.feature.auth.status.statusDestination
import org.mifos.mobile.feature.auth.uploadId.navigateToUploadIdScreen
@ -48,8 +50,7 @@ fun NavGraphBuilder.authenticationNavGraph(
startDestination = LoginRoute,
) {
loginDestination(
// navigateToRegisterScreen = navController::navigateToRegisterScreen,
navigateToRegisterScreen = navController::navigateToOtpAuthScreen,
navigateToRegisterScreen = navController::navigateToRegisterScreen,
navigateToPasscodeScreen = navigateToPasscodeScreen,
navigateToForgotPasswordScreen = navController::navigateToRecoverPasswordScreen,
)
@ -66,6 +67,7 @@ fun NavGraphBuilder.authenticationNavGraph(
otpAuthenticationDestination(
navigateToStatusScreen = navController::navigateToStatusScreen,
navigateToSetPasswordScreen = navController::navigateToSetPasswordScreen,
)
statusDestination(
@ -78,5 +80,10 @@ fun NavGraphBuilder.authenticationNavGraph(
navigateToLoginScreen = navController::navigateToLoginScreen,
navigateToOtpAuthenticationScreen = navController::navigateToOtpAuthScreen,
)
setPasswordDestination(
navigateToStatusScreen = navController::navigateToStatusScreen,
navigateToLoginScreen = navController::navigateToLoginScreen,
)
}
}

View File

@ -14,22 +14,33 @@ package org.mifos.mobile.feature.auth.otpAuthentication
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import org.mifos.mobile.core.ui.composableWithStayTransitions
import org.mifos.mobile.feature.auth.status.StatusNavigationRoute
@OptIn(ExperimentalSerializationApi::class)
@Serializable
data object OtpAuthenticationRoute
data class OtpAuthenticationRoute(
val nextRoute: String = StatusNavigationRoute.serializer().descriptor.serialName,
)
fun NavController.navigateToOtpAuthScreen(navOptions: NavOptions? = null) {
this.navigate(route = OtpAuthenticationRoute, navOptions = navOptions)
@OptIn(ExperimentalSerializationApi::class)
fun NavController.navigateToOtpAuthScreen(
nextRoute: String = StatusNavigationRoute.serializer().descriptor.serialName,
navOptions: NavOptions? = null,
) {
this.navigate(OtpAuthenticationRoute(nextRoute), navOptions)
}
fun NavGraphBuilder.otpAuthenticationDestination(
navigateToStatusScreen: (EventType, String) -> Unit,
navigateToSetPasswordScreen: () -> Unit,
) {
composableWithStayTransitions<OtpAuthenticationRoute> {
OtpAuthenticationScreen(
navigateToStatusScreen = navigateToStatusScreen,
navigateToSetPasswordScreen = navigateToSetPasswordScreen,
)
}
}

View File

@ -67,6 +67,7 @@ import org.mifos.mobile.core.ui.utils.EventsEffect
@Composable
internal fun OtpAuthenticationScreen(
navigateToStatusScreen: (EventType, String) -> Unit,
navigateToSetPasswordScreen: () -> Unit,
modifier: Modifier = Modifier,
viewModel: OtpAuthenticationViewModel = koinViewModel(),
) {
@ -80,6 +81,12 @@ internal fun OtpAuthenticationScreen(
event.eventDestination,
)
}
is OtpAuthEvent.NavigateNext -> {
if (uiState.nextRoute == "set_password") {
navigateToSetPasswordScreen.invoke()
}
}
}
}

View File

@ -9,7 +9,9 @@
*/
package org.mifos.mobile.feature.auth.otpAuthentication
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.update
@ -24,9 +26,22 @@ import org.jetbrains.compose.resources.StringResource
import org.mifos.mobile.core.ui.utils.BaseViewModel
import org.mifos.mobile.feature.auth.login.LoginRoute
internal class OtpAuthenticationViewModel : BaseViewModel<OtpAuthState, OtpAuthEvent, OtpAuthAction>(
internal class OtpAuthenticationViewModel(
savedStateHandle: SavedStateHandle,
) : BaseViewModel<
OtpAuthState,
OtpAuthEvent,
OtpAuthAction,
>(
initialState = OtpAuthState(dialogState = null),
) {
init {
val nextRoute = savedStateHandle.toRoute<OtpAuthenticationRoute>()
mutableStateFlow.update {
it.copy(nextRoute = nextRoute.nextRoute)
}
}
private var validationJob: Job? = null
@ -79,7 +94,24 @@ internal class OtpAuthenticationViewModel : BaseViewModel<OtpAuthState, OtpAuthE
}
if (otpError == null) {
registerUser()
when {
state.nextRoute == "set_password" -> handleRecoverPassword()
else -> registerUser()
}
}
}
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
private fun handleRecoverPassword() {
viewModelScope.launch {
mutableStateFlow.update {
it.copy(
dialogState = OtpAuthState.DialogState.Loading,
)
}
delay(3000)
dismissDialog()
sendEvent(OtpAuthEvent.NavigateNext)
}
}
@ -124,6 +156,9 @@ internal class OtpAuthenticationViewModel : BaseViewModel<OtpAuthState, OtpAuthE
}
internal data class OtpAuthState(
val nextRoute: String = "",
val otp: String = "",
val otpError: StringResource? = null,
@ -151,6 +186,8 @@ internal sealed interface OtpAuthAction {
}
internal sealed interface OtpAuthEvent {
data object NavigateNext : OtpAuthEvent
data class NavigateToStatus(
val eventType: EventType,
val eventDestination: String,

View File

@ -0,0 +1,40 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
*/
@file:Suppress("MatchingDeclarationName")
package org.mifos.mobile.feature.auth.setNewPassword
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.mifos.mobile.core.ui.composableWithSlideTransitions
import org.mifos.mobile.feature.auth.otpAuthentication.EventType
@Serializable
@SerialName("set_password")
data object SetPasswordRoute
fun NavController.navigateToSetPasswordScreen(navOptions: NavOptions? = null) {
this.navigate(SetPasswordRoute, navOptions)
}
fun NavGraphBuilder.setPasswordDestination(
navigateToStatusScreen: (EventType, String) -> Unit,
navigateToLoginScreen: () -> Unit,
) {
composableWithSlideTransitions<SetPasswordRoute> {
SetPasswordScreen(
navigateToStatusScreen = navigateToStatusScreen,
navigateToLoginScreen = navigateToLoginScreen,
)
}
}

View File

@ -0,0 +1,290 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
*/
package org.mifos.mobile.feature.auth.setNewPassword
import androidx.compose.foundation.clickable
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.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import mifos_mobile.feature.auth.generated.resources.Res
import mifos_mobile.feature.auth.generated.resources.feature_set_new_password_action_tip
import mifos_mobile.feature.auth.generated.resources.feature_set_new_password_confirm_password_label
import mifos_mobile.feature.auth.generated.resources.feature_set_new_password_message
import mifos_mobile.feature.auth.generated.resources.feature_set_new_password_new_password_label
import mifos_mobile.feature.auth.generated.resources.feature_set_new_password_submit
import mifos_mobile.feature.auth.generated.resources.feature_set_new_password_tip
import mifos_mobile.feature.auth.generated.resources.feature_set_new_password_title
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
import org.mifos.mobile.core.designsystem.component.BasicDialogState
import org.mifos.mobile.core.designsystem.component.LoadingDialogState
import org.mifos.mobile.core.designsystem.component.MifosBasicDialog
import org.mifos.mobile.core.designsystem.component.MifosButton
import org.mifos.mobile.core.designsystem.component.MifosLoadingDialog
import org.mifos.mobile.core.designsystem.component.MifosPasswordField
import org.mifos.mobile.core.designsystem.component.MifosScaffold
import org.mifos.mobile.core.designsystem.theme.DesignToken
import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme
import org.mifos.mobile.core.designsystem.theme.MifosTypography
import org.mifos.mobile.core.ui.CombinedPasswordErrorCard
import org.mifos.mobile.core.ui.PasswordStrengthIndicator
import org.mifos.mobile.core.ui.component.MifosPoweredCard
import org.mifos.mobile.core.ui.utils.EventsEffect
import org.mifos.mobile.feature.auth.otpAuthentication.EventType
@Composable
internal fun SetPasswordScreen(
navigateToStatusScreen: (EventType, String) -> Unit,
navigateToLoginScreen: () -> Unit,
modifier: Modifier = Modifier,
viewModel: SetPasswordViewModel = koinViewModel(),
) {
val uiState by viewModel.stateFlow.collectAsStateWithLifecycle()
EventsEffect(viewModel.eventFlow) { event ->
when (event) {
is SetPasswordEvent.NavigateToLogin -> navigateToLoginScreen.invoke()
is SetPasswordEvent.NavigateToStatus -> navigateToStatusScreen(
event.eventType,
event.eventDestination,
)
}
}
SetPasswordDialogs(
dialogState = uiState.dialogState,
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(SetPasswordAction.OnDismissDialog) }
},
)
SetPasswordScreen(
modifier = modifier,
state = uiState,
onAction = remember(viewModel) {
{ viewModel.trySendAction(it) }
},
)
}
@Composable
private fun SetPasswordDialogs(
dialogState: SetPasswordState.DialogState?,
onDismissRequest: () -> Unit,
) {
when (dialogState) {
is SetPasswordState.DialogState.Error -> MifosBasicDialog(
visibilityState = BasicDialogState.Shown(
message = dialogState.message,
),
onDismissRequest = onDismissRequest,
)
is SetPasswordState.DialogState.Loading -> MifosLoadingDialog(
visibilityState = LoadingDialogState.Shown,
)
null -> Unit
}
}
@Composable
internal fun SetPasswordScreen(
state: SetPasswordState,
modifier: Modifier = Modifier,
onAction: (SetPasswordAction) -> Unit,
) {
MifosScaffold(
modifier = modifier.fillMaxSize(),
bottomBar = {
Surface {
MifosPoweredCard(
modifier = Modifier.fillMaxWidth(),
)
}
},
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(top = DesignToken.padding.large)
.padding(DesignToken.padding.large)
.statusBarsPadding(),
) {
Text(
text = stringResource(Res.string.feature_set_new_password_title),
style = MifosTypography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = stringResource(Res.string.feature_set_new_password_message),
style = MifosTypography.bodySmall,
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.height(24.dp))
SetPasswordInputBox(
state = state,
onAction = onAction,
)
}
}
}
@Composable
internal fun SetPasswordInputBox(
state: SetPasswordState,
modifier: Modifier = Modifier,
onAction: (SetPasswordAction) -> Unit,
) {
val hasError = state.passwordError != null || state.passwordFeedback.isNotEmpty()
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.largeIncreased),
) {
Column(
verticalArrangement = Arrangement.spacedBy(DesignToken.spacing.extraSmall),
) {
MifosPasswordField(
label = stringResource(Res.string.feature_set_new_password_new_password_label),
value = state.password,
onValueChange = { onAction(SetPasswordAction.OnPasswordChange(it)) },
shape = DesignToken.shapes.medium,
modifier = Modifier.fillMaxWidth(),
showPassword = state.isPasswordVisible,
showPasswordChange = {
onAction(SetPasswordAction.OnTogglePassword)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
errorBorderColor = MaterialTheme.colorScheme.error,
),
isError = state.passwordError != null,
hint = state.passwordError?.let { stringResource(it) },
)
if (state.password.isNotEmpty() && !hasError) {
PasswordStrengthIndicator(
state = state.passwordStrengthState,
currentCharacterCount = state.password.length,
minimumCharacterCount = 8,
modifier = Modifier.fillMaxWidth(),
)
}
// Show combined error card with integrated strength indicator when there are errors
if (hasError && state.password.isNotEmpty()) {
CombinedPasswordErrorCard(
passwordStrengthState = state.passwordStrengthState,
currentCharacterCount = state.password.length,
errorText = state.passwordError,
errors = state.passwordFeedback,
minimumCharacterCount = 8,
)
}
}
MifosPasswordField(
label = stringResource(Res.string.feature_set_new_password_confirm_password_label),
value = state.confirmPassword,
onValueChange = { onAction(SetPasswordAction.OnConfirmPasswordChange(it)) },
shape = DesignToken.shapes.medium,
modifier = Modifier.fillMaxWidth(),
showPassword = state.isConfirmPasswordVisible,
showPasswordChange = {
onAction(SetPasswordAction.OnToggleConfirmPassword)
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
unfocusedBorderColor = MaterialTheme.colorScheme.secondaryContainer,
errorBorderColor = MaterialTheme.colorScheme.error,
),
isError = state.confirmPasswordError != null,
hint = state.confirmPasswordError?.let { stringResource(it) },
keyboardType = KeyboardType.Password,
)
MifosButton(
modifier = Modifier.fillMaxWidth().height(DesignToken.sizes.buttonHeight),
enabled = state.isSubmitButtonEnabled,
onClick = {
onAction(SetPasswordAction.OnSubmit)
},
shape = DesignToken.shapes.medium,
) {
Text(
text = stringResource(Res.string.feature_set_new_password_submit),
style = MifosTypography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Text(
text = stringResource(Res.string.feature_set_new_password_tip),
style = MifosTypography.labelMedium,
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier = Modifier.width(DesignToken.spacing.small))
Text(
modifier = Modifier.clickable { onAction.invoke(SetPasswordAction.OnLogIn) },
text = stringResource(Res.string.feature_set_new_password_action_tip),
style = MifosTypography.labelMediumEmphasized,
color = MaterialTheme.colorScheme.primary,
)
}
}
}
@Preview
@Composable
private fun Set_Password_Preview() {
MifosMobileTheme {
SetPasswordScreen(
state = SetPasswordState(dialogState = null),
onAction = {},
)
}
}

View File

@ -0,0 +1,267 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md
*/
package org.mifos.mobile.feature.auth.setNewPassword
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
import mifos_mobile.feature.auth.generated.resources.Res
import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_mismatch
import mifos_mobile.feature.auth.generated.resources.feature_signup_error_password_required_error
import org.jetbrains.compose.resources.StringResource
import org.mifos.mobile.core.ui.PasswordStrengthState
import org.mifos.mobile.core.ui.utils.BaseViewModel
import org.mifos.mobile.core.ui.utils.PasswordChecker
import org.mifos.mobile.core.ui.utils.PasswordStrength
import org.mifos.mobile.core.ui.utils.PasswordStrengthResult
import org.mifos.mobile.feature.auth.login.LoginRoute
import org.mifos.mobile.feature.auth.otpAuthentication.EventType
internal class SetPasswordViewModel : BaseViewModel<SetPasswordState, SetPasswordEvent, SetPasswordAction>(
initialState = SetPasswordState(dialogState = null),
) {
private var validationJob: Job? = null
private var passwordStrengthJob: Job = Job()
override fun handleAction(action: SetPasswordAction) {
when (action) {
is SetPasswordAction.OnConfirmPasswordChange -> handleConfirmPasswordChange(action.confirmPassword)
is SetPasswordAction.OnLogIn -> sendEvent(SetPasswordEvent.NavigateToLogin)
is SetPasswordAction.OnPasswordChange -> handlePasswordChange(action.password)
is SetPasswordAction.OnSubmit -> handleSubmit()
is SetPasswordAction.OnToggleConfirmPassword -> toggleConfirmPasswordVisibility()
is SetPasswordAction.OnTogglePassword -> togglePasswordVisibility()
is SetPasswordAction.Internal.ReceivePasswordStrengthResult -> handlePasswordStrengthResult(
action,
)
SetPasswordAction.OnDismissDialog -> dismissDialog()
}
}
private fun handlePasswordChange(password: String) {
mutableStateFlow.update { it.copy(password = password, passwordError = null) }
passwordStrengthJob.cancel()
if (password.isEmpty()) {
mutableStateFlow.update {
it.copy(
passwordStrengthState = PasswordStrengthState.NONE,
passwordFeedback = emptyList(),
)
}
} else {
passwordStrengthJob = viewModelScope.launch {
val result = PasswordChecker.getPasswordStrengthResult(password)
val feedback = PasswordChecker.getPasswordFeedback(password)
trySendAction(SetPasswordAction.Internal.ReceivePasswordStrengthResult(result))
mutableStateFlow.update {
it.copy(passwordFeedback = feedback)
}
}
}
validationJob?.cancel()
validationJob = viewModelScope.launch {
delay(300)
val newResult = validatePassword(password)
val confirmResult = if (state.confirmPassword.isNotEmpty()) {
validateConfirmPassword(state.confirmPassword, password)
} else {
null
}
mutableStateFlow.update {
it.copy(
passwordError = newResult,
confirmPasswordError = confirmResult,
)
}
}
}
@Suppress("ReturnCount")
private fun validatePassword(password: String): StringResource? {
if (password.isEmpty()) {
return Res.string.feature_signup_error_password_required_error
}
return when (val result = PasswordChecker.getPasswordStrengthResult(password)) {
is PasswordStrengthResult.Error -> {
result.message
}
is PasswordStrengthResult.Success -> {
null
}
}
}
private fun handleConfirmPasswordChange(confirmPassword: String) {
mutableStateFlow.update {
it.copy(
confirmPassword = confirmPassword,
confirmPasswordError = null,
)
}
debounceValidation {
val result = validateConfirmPassword(confirmPassword, state.password)
mutableStateFlow.update {
it.copy(
confirmPasswordError = result,
)
}
}
}
private fun validateConfirmPassword(confirmPassword: String, password: String): StringResource? =
when {
confirmPassword.isEmpty() -> Res.string.feature_signup_error_password_required_error
password != confirmPassword -> Res.string.feature_signup_error_password_mismatch
else -> null
}
private fun handlePasswordStrengthResult(
action: SetPasswordAction.Internal
.ReceivePasswordStrengthResult,
) {
when (val result = action.result) {
is PasswordStrengthResult.Success -> {
val updatedState = when (result.passwordStrength) {
PasswordStrength.LEVEL_0 -> PasswordStrengthState.WEAK_1
PasswordStrength.LEVEL_1 -> PasswordStrengthState.WEAK_2
PasswordStrength.LEVEL_2 -> PasswordStrengthState.WEAK_3
PasswordStrength.LEVEL_3 -> PasswordStrengthState.GOOD
PasswordStrength.LEVEL_4 -> PasswordStrengthState.STRONG
PasswordStrength.LEVEL_5 -> PasswordStrengthState.VERY_STRONG
}
mutableStateFlow.update { oldState ->
oldState.copy(passwordStrengthState = updatedState)
}
}
is PasswordStrengthResult.Error -> {
mutableStateFlow.update {
it.copy(
passwordError = result.message,
passwordStrengthState = PasswordStrengthState.NONE,
)
}
}
}
}
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
private fun handleSubmit() {
viewModelScope.launch {
mutableStateFlow.update { it.copy(dialogState = SetPasswordState.DialogState.Loading) }
delay(3000)
dismissDialog()
sendEvent(
SetPasswordEvent.NavigateToStatus(
EventType.SUCCESS,
LoginRoute::class.serializer().descriptor.serialName,
),
)
}
}
private fun togglePasswordVisibility() {
mutableStateFlow.update { it.copy(isPasswordVisible = !state.isPasswordVisible) }
}
private fun toggleConfirmPasswordVisibility() {
mutableStateFlow.update { it.copy(isConfirmPasswordVisible = !state.isConfirmPasswordVisible) }
}
private fun debounceValidation(validation: suspend () -> Unit) {
validationJob?.cancel()
validationJob = viewModelScope.launch {
delay(300)
validation()
}
}
private fun dismissDialog() {
mutableStateFlow.update {
it.copy(dialogState = null)
}
}
}
internal data class SetPasswordState(
val password: String = "",
val passwordError: StringResource? = null,
val confirmPassword: String = "",
val confirmPasswordError: StringResource? = null,
val isPasswordVisible: Boolean = false,
val isConfirmPasswordVisible: Boolean = false,
val passwordFeedback: List<StringResource> = emptyList(),
val passwordStrengthState: PasswordStrengthState = PasswordStrengthState.NONE,
val dialogState: DialogState?,
) {
sealed interface DialogState {
data class Error(val message: String) : DialogState
data object Loading : DialogState
}
val isSubmitButtonEnabled: Boolean
get() = password.isNotBlank() && password.isNotBlank()
}
internal sealed interface SetPasswordEvent {
data object NavigateToLogin : SetPasswordEvent
data class NavigateToStatus(
val eventType: EventType,
val eventDestination: String,
) : SetPasswordEvent
}
internal sealed interface SetPasswordAction {
data class OnPasswordChange(val password: String) : SetPasswordAction
data class OnConfirmPasswordChange(val confirmPassword: String) : SetPasswordAction
data object OnTogglePassword : SetPasswordAction
data object OnToggleConfirmPassword : SetPasswordAction
data object OnSubmit : SetPasswordAction
data object OnLogIn : SetPasswordAction
data object OnDismissDialog : SetPasswordAction
sealed class Internal : SetPasswordAction {
data class ReceivePasswordStrengthResult(
val result: PasswordStrengthResult,
) : Internal()
}
}

View File

@ -17,7 +17,6 @@ import androidx.navigation.toRoute
import kotlinx.serialization.Serializable
import org.mifos.mobile.core.ui.composableWithStayTransitions
import org.mifos.mobile.feature.auth.otpAuthentication.EventType
import org.mifos.mobile.feature.auth.otpAuthentication.OtpAuthenticationRoute
@Serializable
data class StatusNavigationRoute(
@ -43,12 +42,7 @@ fun NavController.navigateToStatusScreen(
title = title,
subtitle = subtitle,
),
) {
popUpTo(OtpAuthenticationRoute) {
inclusive = true
}
launchSingleTop = true
}
)
}
fun NavGraphBuilder.statusDestination(