Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,26 @@ 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
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
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
Expand Down Expand Up @@ -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 -> {
Expand All @@ -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 */ }
}
}

Expand All @@ -112,7 +108,8 @@ fun AccountVerificationScreen(
modifier = Modifier
.fillMaxSize()
.padding(padValues)
.imePadding(),
.imePadding()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(DefaultVerticalSpace),
) {
Banner(
Expand All @@ -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(),
Expand Down Expand Up @@ -167,6 +165,9 @@ fun AccountVerificationScreen(
onValueChange = {
viewModel.postIntent(ResetPasswordIntent.UpdateStudentName(value = it))
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
)
TextField(
modifier = Modifier
Expand All @@ -189,6 +190,7 @@ fun AccountVerificationScreen(
.horizontalPadding()
.bottomPadding(),
onClick = {

viewModel.postIntent(
ResetPasswordIntent.SendEmailVerificationCode(
uiState.email,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -27,6 +34,10 @@ class ResetPasswordViewModel @Inject constructor(
κ²€μ‚¬μ—μ„œ κ°€λŠ₯이 뜨게 λœλ‹€λ©΄ "이메일 인증번호 보내기 APi"λ₯Ό μ‚¬μš©ν•΄μ„œ μ‚¬μš©μž 이메일에 이메일을 λ°œμ†‘ν•©λ‹ˆλ‹€.
그리고 이메일 인증번호 확인 Apiλ₯Ό μ‚¬μš©ν•˜μ—¬ 인증을 μ™„λ£Œν•˜κ³  Students의 λΉ„λ°€λ²ˆν˜Έ μž¬μ„€μ • Apiλ₯Ό μ‚¬μš©ν•˜μ—¬ μž¬μ„€μ •ν•©λ‹ˆλ‹€.*/

init {
debounceName()
}

override fun processIntent(intent: ResetPasswordIntent) {
when (intent) {
ResetPasswordIntent.SetPassword -> resetPassword()
Expand All @@ -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) {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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(
Expand All @@ -193,6 +226,7 @@ data class ResetPasswordUiState(
newPasswordRepeat = "",
hashedEmail = "",
sessionId = UUID.randomUUID(),
isAccountIdError = false
)
}
}
Expand Down Expand Up @@ -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()
}
2 changes: 2 additions & 0 deletions feature/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@
<string name="reset_password_account_verification_success_account_id_matches_email">아이디와 μΌμΉ˜ν•˜λŠ” μ΄λ©”μΌμž…λ‹ˆλ‹€</string>
<string name="reset_password_account_verification_enter_account_id_invalid_format">아이디 ν˜•μ‹μ΄ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.</string>
<string name="reset_password_account_verification_error_too_many_request">μš”μ²­μ΄ λ„ˆλ¬΄ λ§ŽμŠ΅λ‹ˆλ‹€ μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”</string>
<string name="reset_password_account_verification_error_user_not_found">μ΄λ¦„μ΄λ‚˜ 이메일이 μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.</string>
<string name="reset_password_account_verification_error_invalid_email_format">이메일 ν˜•μ‹μ— 맞좰 μž‘μ„±ν•΄μ£Όμ„Έμš”</string>
<string name="reset_password_account_verification_enter_student_name">이름 μž…λ ₯</string>
<string name="reset_password_account_verification_enter_email">이메일 μž…λ ₯</string>
<string name="reset_password_set_password_password_success_changed">λΉ„λ°€λ²ˆν˜Έκ°€ λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package team.aliens.dms.android.shared.validator

object EmailValidator : Validator<String>() {
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)
}
Expand Down