Skip to content

Commit cc55af5

Browse files
authored
๐Ÿ”€ :: (#770) ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ๋กœ์ง ๋ฆฌํŒฉํ† ๋ง
๐Ÿ”€ :: (#770) ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ ๋กœ์ง ๋ฆฌํŒฉํ† ๋ง
2 parents 8764228 + 78ac4d1 commit cc55af5

File tree

4 files changed

+66
-27
lines changed

4 files changed

+66
-27
lines changed

โ€Žfeature/src/main/java/team/aliens/dms/android/feature/resetpassword/AccountVerificationScreen.ktโ€Ž

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,26 @@ import androidx.compose.foundation.layout.fillMaxSize
99
import androidx.compose.foundation.layout.fillMaxWidth
1010
import androidx.compose.foundation.layout.imePadding
1111
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.rememberScrollState
13+
import androidx.compose.foundation.text.KeyboardOptions
14+
import androidx.compose.foundation.verticalScroll
1215
import androidx.compose.material3.Card
1316
import androidx.compose.material3.CardDefaults
1417
import androidx.compose.material3.ExperimentalMaterial3Api
1518
import androidx.compose.material3.Icon
1619
import androidx.compose.material3.IconButton
1720
import androidx.compose.material3.Text
1821
import androidx.compose.runtime.Composable
19-
import androidx.compose.runtime.LaunchedEffect
2022
import androidx.compose.runtime.getValue
2123
import androidx.compose.runtime.mutableStateOf
2224
import androidx.compose.runtime.saveable.rememberSaveable
2325
import androidx.compose.ui.Modifier
2426
import androidx.compose.ui.platform.LocalContext
2527
import androidx.compose.ui.res.painterResource
2628
import androidx.compose.ui.res.stringResource
29+
import androidx.compose.ui.text.input.ImeAction
2730
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2831
import com.ramcosta.composedestinations.annotation.Destination
29-
import kotlinx.coroutines.delay
3032
import team.aliens.dms.android.core.designsystem.ContainedButton
3133
import team.aliens.dms.android.core.designsystem.DmsTheme
3234
import team.aliens.dms.android.core.designsystem.DmsTopAppBar
@@ -60,15 +62,6 @@ fun AccountVerificationScreen(
6062
val context = LocalContext.current
6163
val (idChecked, onChangeIdChecked) = rememberSaveable { mutableStateOf(false) }
6264

63-
LaunchedEffect(uiState.accountId) {
64-
if (uiState.accountId.isNotEmpty()) {
65-
delay(300L)
66-
viewModel.postIntent(ResetPasswordIntent.CheckAccountId)
67-
}
68-
}
69-
70-
val isAccountIdError by rememberSaveable(uiState.accountId) { mutableStateOf(false) }
71-
7265
viewModel.sideEffectFlow.collectInLaunchedEffectWithLifecycle { sideEffect ->
7366
when (sideEffect) {
7467
ResetPasswordSideEffect.AccountIdExists -> {
@@ -79,14 +72,17 @@ fun AccountVerificationScreen(
7972
message = context.getString(R.string.reset_password_account_verification_account_id_does_not_exist),
8073
)
8174

82-
ResetPasswordSideEffect.EmailVerificationTooManyRequest -> toast.showErrorToast(
83-
message = context.getString(R.string.reset_password_account_verification_error_too_many_request),
75+
ResetPasswordSideEffect.EmailVerificationUserNotFound -> toast.showErrorToast(
76+
message = context.getString(R.string.reset_password_account_verification_error_user_not_found),
77+
)
78+
79+
ResetPasswordSideEffect.InvalidEmailFormat -> toast.showErrorToast(
80+
message = context.getString(R.string.reset_password_account_verification_error_invalid_email_format)
8481
)
8582

8683
ResetPasswordSideEffect.SendEmailVerificationCodeSuccess -> navigator.openResetPasswordEnterEmailVerificationCode()
8784

88-
else -> { /* explicit blank */
89-
}
85+
else -> { /* explicit blank */ }
9086
}
9187
}
9288

@@ -112,7 +108,8 @@ fun AccountVerificationScreen(
112108
modifier = Modifier
113109
.fillMaxSize()
114110
.padding(padValues)
115-
.imePadding(),
111+
.imePadding()
112+
.verticalScroll(rememberScrollState()),
116113
verticalArrangement = Arrangement.spacedBy(DefaultVerticalSpace),
117114
) {
118115
Banner(
@@ -134,12 +131,13 @@ fun AccountVerificationScreen(
134131
Text(text = stringResource(id = R.string.reset_password_account_verification_enter_account_id))
135132
},
136133
onValueChange = { viewModel.postIntent(ResetPasswordIntent.UpdateAccountId(value = it)) },
137-
supportingText = if (isAccountIdError) {
134+
supportingText = if (uiState.isAccountIdError) {
138135
{ Text(text = stringResource(id = R.string.reset_password_account_verification_enter_account_id_invalid_format)) }
139136
} else {
140137
null
141138
},
142-
isError = isAccountIdError,
139+
isError = uiState.isAccountIdError,
140+
readOnly = idChecked,
143141
)
144142
AnimatedVisibility(
145143
modifier = Modifier.fillMaxWidth(),
@@ -167,6 +165,9 @@ fun AccountVerificationScreen(
167165
onValueChange = {
168166
viewModel.postIntent(ResetPasswordIntent.UpdateStudentName(value = it))
169167
},
168+
keyboardOptions = KeyboardOptions(
169+
imeAction = ImeAction.Next
170+
),
170171
)
171172
TextField(
172173
modifier = Modifier
@@ -189,6 +190,7 @@ fun AccountVerificationScreen(
189190
.horizontalPadding()
190191
.bottomPadding(),
191192
onClick = {
193+
192194
viewModel.postIntent(
193195
ResetPasswordIntent.SendEmailVerificationCode(
194196
uiState.email,

โ€Žfeature/src/main/java/team/aliens/dms/android/feature/resetpassword/ResetPasswordViewModel.ktโ€Ž

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package team.aliens.dms.android.feature.resetpassword
33
import androidx.lifecycle.viewModelScope
44
import dagger.hilt.android.lifecycle.HiltViewModel
55
import kotlinx.coroutines.Dispatchers
6+
import kotlinx.coroutines.FlowPreview
7+
import kotlinx.coroutines.flow.debounce
8+
import kotlinx.coroutines.flow.distinctUntilChanged
9+
import kotlinx.coroutines.flow.map
610
import kotlinx.coroutines.launch
711
import team.aliens.dms.android.core.ui.mvi.BaseMviViewModel
812
import team.aliens.dms.android.core.ui.mvi.Intent
@@ -11,10 +15,13 @@ import team.aliens.dms.android.core.ui.mvi.UiState
1115
import team.aliens.dms.android.data.auth.model.EmailVerificationType
1216
import team.aliens.dms.android.data.auth.repository.AuthRepository
1317
import team.aliens.dms.android.data.student.repository.StudentRepository
18+
import team.aliens.dms.android.shared.validator.checkIfEmailValid
1419
import team.aliens.dms.android.shared.validator.checkIfPasswordValid
1520
import java.util.UUID
1621
import javax.inject.Inject
1722

23+
const val SEARCH_DEBOUNCE_MILLIS = 1000L
24+
1825
@HiltViewModel
1926
class ResetPasswordViewModel @Inject constructor(
2027
private val studentRepository: StudentRepository,
@@ -27,6 +34,10 @@ class ResetPasswordViewModel @Inject constructor(
2734
๊ฒ€์‚ฌ์—์„œ ๊ฐ€๋Šฅ์ด ๋œจ๊ฒŒ ๋œ๋‹ค๋ฉด "์ด๋ฉ”์ผ ์ธ์ฆ๋ฒˆํ˜ธ ๋ณด๋‚ด๊ธฐ APi"๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ์— ์ด๋ฉ”์ผ์„ ๋ฐœ์†กํ•ฉ๋‹ˆ๋‹ค.
2835
๊ทธ๋ฆฌ๊ณ  ์ด๋ฉ”์ผ ์ธ์ฆ๋ฒˆํ˜ธ ํ™•์ธ Api๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ์„ ์™„๋ฃŒํ•˜๊ณ  Students์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • Api๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.*/
2936

37+
init {
38+
debounceName()
39+
}
40+
3041
override fun processIntent(intent: ResetPasswordIntent) {
3142
when (intent) {
3243
ResetPasswordIntent.SetPassword -> resetPassword()
@@ -43,6 +54,17 @@ class ResetPasswordViewModel @Inject constructor(
4354
}
4455
}
4556

57+
@OptIn(FlowPreview::class)
58+
private fun debounceName() {
59+
viewModelScope.launch {
60+
stateFlow.map { it.accountId }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS).collect {
61+
if (it.isNotBlank()) {
62+
checkIdExists()
63+
}
64+
}
65+
}
66+
}
67+
4668
private fun resetPassword() = viewModelScope.launch(Dispatchers.IO) {
4769
val capturedState = stateFlow.value
4870
if (capturedState.newPassword != capturedState.newPasswordRepeat) {
@@ -82,30 +104,40 @@ class ResetPasswordViewModel @Inject constructor(
82104
reduce(
83105
newState = stateFlow.value.copy(
84106
hashedEmail = it,
107+
isAccountIdError = false,
85108
),
86109
)
87110
postSideEffect(ResetPasswordSideEffect.AccountIdExists)
88111
}.onFailure {
112+
reduce(
113+
newState = stateFlow.value.copy(
114+
isAccountIdError = true,
115+
),
116+
)
89117
postSideEffect(ResetPasswordSideEffect.AccountIdNotExists)
90118
}
91119
}
92120

93121
private fun sendEmailVerificationCode(email: String) =
94-
runCatching {
95-
viewModelScope.launch(Dispatchers.IO) {
122+
viewModelScope.launch(Dispatchers.IO) {
123+
if (!checkIfEmailValid(email)) {
124+
postSideEffect(ResetPasswordSideEffect.InvalidEmailFormat)
125+
return@launch
126+
}
127+
runCatching {
96128
authRepository.sendEmailVerificationCode(
97129
email = email,
98130
type = EmailVerificationType.PASSWORD,
99131
)
132+
}.onSuccess {
133+
postSideEffect(ResetPasswordSideEffect.SendEmailVerificationCodeSuccess)
134+
}.onFailure {
135+
postSideEffect(ResetPasswordSideEffect.EmailVerificationUserNotFound)
100136
}
101-
}.onSuccess {
102-
postSideEffect(ResetPasswordSideEffect.SendEmailVerificationCodeSuccess)
103-
}.onFailure {
104-
postSideEffect(ResetPasswordSideEffect.EmailVerificationTooManyRequest)
105137
}
106138

107139
private fun updateEmailVerificationCode(value: String) = run {
108-
if (value.length > ResetPasswordViewModel.EMAIL_VERIFICATION_CODE_LENGTH) {
140+
if (value.length > EMAIL_VERIFICATION_CODE_LENGTH) {
109141
return@run false
110142
}
111143
reduce(newState = stateFlow.value.copy(emailVerificationCode = value))
@@ -182,6 +214,7 @@ data class ResetPasswordUiState(
182214
val newPasswordRepeat: String,
183215
val hashedEmail: String,
184216
val sessionId: UUID,
217+
val isAccountIdError: Boolean,
185218
) : UiState() {
186219
companion object {
187220
fun initial() = ResetPasswordUiState(
@@ -193,6 +226,7 @@ data class ResetPasswordUiState(
193226
newPasswordRepeat = "",
194227
hashedEmail = "",
195228
sessionId = UUID.randomUUID(),
229+
isAccountIdError = false
196230
)
197231
}
198232
}
@@ -223,5 +257,6 @@ sealed class ResetPasswordSideEffect : SideEffect() {
223257
data object EmailVerificationCodeIncorrect : ResetPasswordSideEffect()
224258
data object EmailVerificationSessionReset : ResetPasswordSideEffect()
225259
data object EmailVerificationSessionResetFailed : ResetPasswordSideEffect()
226-
data object EmailVerificationTooManyRequest : ResetPasswordSideEffect()
260+
data object EmailVerificationUserNotFound : ResetPasswordSideEffect()
261+
data object InvalidEmailFormat : ResetPasswordSideEffect()
227262
}

โ€Žfeature/src/main/res/values/strings.xmlโ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,8 @@
377377
<string name="reset_password_account_verification_success_account_id_matches_email">์•„์ด๋””์™€ ์ผ์น˜ํ•˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค</string>
378378
<string name="reset_password_account_verification_enter_account_id_invalid_format">์•„์ด๋”” ํ˜•์‹์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.</string>
379379
<string name="reset_password_account_verification_error_too_many_request">์š”์ฒญ์ด ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”</string>
380+
<string name="reset_password_account_verification_error_user_not_found">์ด๋ฆ„์ด๋‚˜ ์ด๋ฉ”์ผ์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.</string>
381+
<string name="reset_password_account_verification_error_invalid_email_format">์ด๋ฉ”์ผ ํ˜•์‹์— ๋งž์ถฐ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”</string>
380382
<string name="reset_password_account_verification_enter_student_name">์ด๋ฆ„ ์ž…๋ ฅ</string>
381383
<string name="reset_password_account_verification_enter_email">์ด๋ฉ”์ผ ์ž…๋ ฅ</string>
382384
<string name="reset_password_set_password_password_success_changed">๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.</string>

โ€Žshared/validator/src/main/java/team/aliens/dms/android/shared/validator/EmailValidator.ktโ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package team.aliens.dms.android.shared.validator
22

33
object EmailValidator : Validator<String>() {
44
override val regex =
5-
Regex("^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}\$")
5+
Regex("^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\\.[a-zA-Z]{2,3}\$")
66

77
override fun validate(value: String): Boolean = value.matches(regex)
88
}

0 commit comments

Comments
ย (0)