diff --git a/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/AccountVerificationScreen.kt b/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/AccountVerificationScreen.kt index e8b1a6b5b..75511a084 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/AccountVerificationScreen.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/AccountVerificationScreen.kt @@ -9,6 +9,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -16,7 +19,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -24,9 +26,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.ramcosta.composedestinations.annotation.Destination -import kotlinx.coroutines.delay import team.aliens.dms.android.core.designsystem.ContainedButton import team.aliens.dms.android.core.designsystem.DmsTheme import team.aliens.dms.android.core.designsystem.DmsTopAppBar @@ -60,15 +62,6 @@ fun AccountVerificationScreen( val context = LocalContext.current val (idChecked, onChangeIdChecked) = rememberSaveable { mutableStateOf(false) } - LaunchedEffect(uiState.accountId) { - if (uiState.accountId.isNotEmpty()) { - delay(300L) - viewModel.postIntent(ResetPasswordIntent.CheckAccountId) - } - } - - val isAccountIdError by rememberSaveable(uiState.accountId) { mutableStateOf(false) } - viewModel.sideEffectFlow.collectInLaunchedEffectWithLifecycle { sideEffect -> when (sideEffect) { ResetPasswordSideEffect.AccountIdExists -> { @@ -79,14 +72,17 @@ fun AccountVerificationScreen( message = context.getString(R.string.reset_password_account_verification_account_id_does_not_exist), ) - ResetPasswordSideEffect.EmailVerificationTooManyRequest -> toast.showErrorToast( - message = context.getString(R.string.reset_password_account_verification_error_too_many_request), + ResetPasswordSideEffect.EmailVerificationUserNotFound -> toast.showErrorToast( + message = context.getString(R.string.reset_password_account_verification_error_user_not_found), + ) + + ResetPasswordSideEffect.InvalidEmailFormat -> toast.showErrorToast( + message = context.getString(R.string.reset_password_account_verification_error_invalid_email_format) ) ResetPasswordSideEffect.SendEmailVerificationCodeSuccess -> navigator.openResetPasswordEnterEmailVerificationCode() - else -> { /* explicit blank */ - } + else -> { /* explicit blank */ } } } @@ -112,7 +108,8 @@ fun AccountVerificationScreen( modifier = Modifier .fillMaxSize() .padding(padValues) - .imePadding(), + .imePadding() + .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(DefaultVerticalSpace), ) { Banner( @@ -134,12 +131,13 @@ fun AccountVerificationScreen( Text(text = stringResource(id = R.string.reset_password_account_verification_enter_account_id)) }, onValueChange = { viewModel.postIntent(ResetPasswordIntent.UpdateAccountId(value = it)) }, - supportingText = if (isAccountIdError) { + supportingText = if (uiState.isAccountIdError) { { Text(text = stringResource(id = R.string.reset_password_account_verification_enter_account_id_invalid_format)) } } else { null }, - isError = isAccountIdError, + isError = uiState.isAccountIdError, + readOnly = idChecked, ) AnimatedVisibility( modifier = Modifier.fillMaxWidth(), @@ -167,6 +165,9 @@ fun AccountVerificationScreen( onValueChange = { viewModel.postIntent(ResetPasswordIntent.UpdateStudentName(value = it)) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), ) TextField( modifier = Modifier @@ -189,6 +190,7 @@ fun AccountVerificationScreen( .horizontalPadding() .bottomPadding(), onClick = { + viewModel.postIntent( ResetPasswordIntent.SendEmailVerificationCode( uiState.email, diff --git a/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/ResetPasswordViewModel.kt b/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/ResetPasswordViewModel.kt index cb4dca42b..3ca99aa47 100644 --- a/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/ResetPasswordViewModel.kt +++ b/feature/src/main/java/team/aliens/dms/android/feature/resetpassword/ResetPasswordViewModel.kt @@ -3,6 +3,10 @@ package team.aliens.dms.android.feature.resetpassword import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import team.aliens.dms.android.core.ui.mvi.BaseMviViewModel import team.aliens.dms.android.core.ui.mvi.Intent @@ -11,10 +15,13 @@ import team.aliens.dms.android.core.ui.mvi.UiState import team.aliens.dms.android.data.auth.model.EmailVerificationType import team.aliens.dms.android.data.auth.repository.AuthRepository import team.aliens.dms.android.data.student.repository.StudentRepository +import team.aliens.dms.android.shared.validator.checkIfEmailValid import team.aliens.dms.android.shared.validator.checkIfPasswordValid import java.util.UUID import javax.inject.Inject +const val SEARCH_DEBOUNCE_MILLIS = 1000L + @HiltViewModel class ResetPasswordViewModel @Inject constructor( private val studentRepository: StudentRepository, @@ -27,6 +34,10 @@ class ResetPasswordViewModel @Inject constructor( 검사에서 가능이 뜨게 된다면 "이메일 인증번호 보내기 APi"를 사용해서 사용자 이메일에 이메일을 발송합니다. 그리고 이메일 인증번호 확인 Api를 사용하여 인증을 완료하고 Students의 비밀번호 재설정 Api를 사용하여 재설정합니다.*/ + init { + debounceName() + } + override fun processIntent(intent: ResetPasswordIntent) { when (intent) { ResetPasswordIntent.SetPassword -> resetPassword() @@ -43,6 +54,17 @@ class ResetPasswordViewModel @Inject constructor( } } + @OptIn(FlowPreview::class) + private fun debounceName() { + viewModelScope.launch { + stateFlow.map { it.accountId }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS).collect { + if (it.isNotBlank()) { + checkIdExists() + } + } + } + } + private fun resetPassword() = viewModelScope.launch(Dispatchers.IO) { val capturedState = stateFlow.value if (capturedState.newPassword != capturedState.newPasswordRepeat) { @@ -82,30 +104,40 @@ class ResetPasswordViewModel @Inject constructor( reduce( newState = stateFlow.value.copy( hashedEmail = it, + isAccountIdError = false, ), ) postSideEffect(ResetPasswordSideEffect.AccountIdExists) }.onFailure { + reduce( + newState = stateFlow.value.copy( + isAccountIdError = true, + ), + ) postSideEffect(ResetPasswordSideEffect.AccountIdNotExists) } } private fun sendEmailVerificationCode(email: String) = - runCatching { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { + if (!checkIfEmailValid(email)) { + postSideEffect(ResetPasswordSideEffect.InvalidEmailFormat) + return@launch + } + runCatching { authRepository.sendEmailVerificationCode( email = email, type = EmailVerificationType.PASSWORD, ) + }.onSuccess { + postSideEffect(ResetPasswordSideEffect.SendEmailVerificationCodeSuccess) + }.onFailure { + postSideEffect(ResetPasswordSideEffect.EmailVerificationUserNotFound) } - }.onSuccess { - postSideEffect(ResetPasswordSideEffect.SendEmailVerificationCodeSuccess) - }.onFailure { - postSideEffect(ResetPasswordSideEffect.EmailVerificationTooManyRequest) } private fun updateEmailVerificationCode(value: String) = run { - if (value.length > ResetPasswordViewModel.EMAIL_VERIFICATION_CODE_LENGTH) { + if (value.length > EMAIL_VERIFICATION_CODE_LENGTH) { return@run false } reduce(newState = stateFlow.value.copy(emailVerificationCode = value)) @@ -182,6 +214,7 @@ data class ResetPasswordUiState( val newPasswordRepeat: String, val hashedEmail: String, val sessionId: UUID, + val isAccountIdError: Boolean, ) : UiState() { companion object { fun initial() = ResetPasswordUiState( @@ -193,6 +226,7 @@ data class ResetPasswordUiState( newPasswordRepeat = "", hashedEmail = "", sessionId = UUID.randomUUID(), + isAccountIdError = false ) } } @@ -223,5 +257,6 @@ sealed class ResetPasswordSideEffect : SideEffect() { data object EmailVerificationCodeIncorrect : ResetPasswordSideEffect() data object EmailVerificationSessionReset : ResetPasswordSideEffect() data object EmailVerificationSessionResetFailed : ResetPasswordSideEffect() - data object EmailVerificationTooManyRequest : ResetPasswordSideEffect() + data object EmailVerificationUserNotFound : ResetPasswordSideEffect() + data object InvalidEmailFormat : ResetPasswordSideEffect() } diff --git a/feature/src/main/res/values/strings.xml b/feature/src/main/res/values/strings.xml index d2a8d28df..19e00761c 100644 --- a/feature/src/main/res/values/strings.xml +++ b/feature/src/main/res/values/strings.xml @@ -377,6 +377,8 @@ 아이디와 일치하는 이메일입니다 아이디 형식이 일치하지 않습니다. 요청이 너무 많습니다 잠시 후 다시 시도해주세요 + 이름이나 이메일이 일치하지 않습니다. + 이메일 형식에 맞춰 작성해주세요 이름 입력 이메일 입력 비밀번호가 변경되었습니다. diff --git a/shared/validator/src/main/java/team/aliens/dms/android/shared/validator/EmailValidator.kt b/shared/validator/src/main/java/team/aliens/dms/android/shared/validator/EmailValidator.kt index adc8a8660..1a4f3ad88 100644 --- a/shared/validator/src/main/java/team/aliens/dms/android/shared/validator/EmailValidator.kt +++ b/shared/validator/src/main/java/team/aliens/dms/android/shared/validator/EmailValidator.kt @@ -2,7 +2,7 @@ package team.aliens.dms.android.shared.validator object EmailValidator : Validator() { override val regex = - Regex("^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}\$") + Regex("^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\\.[a-zA-Z]{2,3}\$") override fun validate(value: String): Boolean = value.matches(regex) }