From 7f0af76eada867ce03aee4c479541e6ee253e7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=96=91=EC=A3=BC=ED=98=84?= Date: Wed, 30 Oct 2024 23:54:50 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../snutt2/components/compose/EditText.kt | 7 +- .../components/compose/EditTextFieldValue.kt | 117 +++++++ .../components/compose/IOSStyleTopBar.kt | 78 +++++ .../components/compose/ProgressDialog.kt | 7 + .../components/compose/WebViewStyleButton.kt | 5 +- .../snutt2/data/user/UserRepository.kt | 2 +- .../snutt2/data/user/UserRepositoryImpl.kt | 4 +- .../snutt2/lib/data/SNUTTStringUtils.kt | 2 + .../network/dto/PostResetPasswordParams.kt | 1 + .../java/com/wafflestudio/snutt2/ui/Colors.kt | 4 + .../wafflestudio/snutt2/views/RootActivity.kt | 3 +- .../logged_in/home/settings/UserViewModel.kt | 4 +- .../views/logged_out/FindPasswordPage.kt | 331 ------------------ .../logged_out/reset_password/CheckIdStep.kt | 123 +++++++ .../reset_password/EnterFullEmailStep.kt | 162 +++++++++ .../reset_password/FindPasswordViewModel.kt | 84 +++++ .../reset_password/NewPasswordStep.kt | 213 +++++++++++ .../reset_password/ResetPasswordPage.kt | 134 +++++++ .../reset_password/VerifyCodeStep.kt | 191 ++++++++++ app/src/main/res/values/strings.xml | 37 +- 20 files changed, 1155 insertions(+), 354 deletions(-) create mode 100644 app/src/main/java/com/wafflestudio/snutt2/components/compose/EditTextFieldValue.kt create mode 100644 app/src/main/java/com/wafflestudio/snutt2/components/compose/IOSStyleTopBar.kt delete mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_out/FindPasswordPage.kt create mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/CheckIdStep.kt create mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/EnterFullEmailStep.kt create mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/FindPasswordViewModel.kt create mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/NewPasswordStep.kt create mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/ResetPasswordPage.kt create mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/VerifyCodeStep.kt diff --git a/app/src/main/java/com/wafflestudio/snutt2/components/compose/EditText.kt b/app/src/main/java/com/wafflestudio/snutt2/components/compose/EditText.kt index b1b9d0a4a..b31c74e21 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/components/compose/EditText.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/components/compose/EditText.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.wafflestudio.snutt2.ui.SNUTTColors import com.wafflestudio.snutt2.ui.SNUTTTypography @@ -46,6 +47,8 @@ fun EditText( value: String, onValueChange: (String) -> Unit, hint: String? = null, + hintTextColor: Color = SNUTTColors.EditTextHint, + hintTextStyle: TextStyle = SNUTTTypography.body1.copy(fontSize = 15.sp), underlineEnabled: Boolean = true, underlineColor: Color = SNUTTColors.Gray200, underlineColorFocused: Color = SNUTTColors.Black900, @@ -86,11 +89,11 @@ fun EditText( modifier = Modifier.weight(1f), ) { it() - if (value.isEmpty() && isFocused.not()) { // FIXME: lectureDetail 에서는 focus 되어 있어도 empty이면 hint 가 나와야 한다. + if (value.isEmpty()) { hint?.let { Text( text = it, - style = textStyle.copy(color = SNUTTColors.Gray200), + style = hintTextStyle.copy(color = hintTextColor), ) } } diff --git a/app/src/main/java/com/wafflestudio/snutt2/components/compose/EditTextFieldValue.kt b/app/src/main/java/com/wafflestudio/snutt2/components/compose/EditTextFieldValue.kt new file mode 100644 index 000000000..ad9d81b39 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/components/compose/EditTextFieldValue.kt @@ -0,0 +1,117 @@ +package com.wafflestudio.snutt2.components.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wafflestudio.snutt2.ui.SNUTTColors +import com.wafflestudio.snutt2.ui.SNUTTTypography + +@Composable +fun EditTextFieldValue( + modifier: Modifier = Modifier, + leadingIcon: @Composable (() -> Unit) = {}, + trailingIcon: @Composable (() -> Unit) = {}, + keyboardOptions: KeyboardOptions = KeyboardOptions(), + keyboardActions: KeyboardActions = KeyboardActions(), + singleLine: Boolean = false, + enabled: Boolean = true, + visualTransformation: VisualTransformation = VisualTransformation.None, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + hint: String? = null, + hintTextColor: Color = SNUTTColors.EditTextHint, + hintTextStyle: TextStyle = SNUTTTypography.body1.copy(fontSize = 15.sp), + underlineEnabled: Boolean = true, + underlineColor: Color = SNUTTColors.Gray200, + underlineColorFocused: Color = SNUTTColors.Black900, + underlineWidth: Dp = 1.dp, + clearFocusFlag: Boolean = false, + textStyle: TextStyle = SNUTTTypography.subtitle1.copy(color = SNUTTColors.Black900), +) { + val focusManager = LocalFocusManager.current + LaunchedEffect(clearFocusFlag) { + if (clearFocusFlag) focusManager.clearFocus() + } + + var isFocused by remember { mutableStateOf(false) } + val customTextSelectionColors = TextSelectionColors( + handleColor = SNUTTColors.Black900, + backgroundColor = SNUTTColors.Black300, + ) + CompositionLocalProvider( + LocalTextSelectionColors provides customTextSelectionColors, + ) { + BasicTextField( + modifier = modifier + .onFocusChanged { isFocused = it.isFocused }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + value = value, + textStyle = textStyle, + enabled = enabled, + onValueChange = onValueChange, + singleLine = singleLine, + visualTransformation = visualTransformation, + cursorBrush = SolidColor(SNUTTColors.Black900), + decorationBox = { + Column { + Row(modifier = Modifier.fillMaxWidth()) { + leadingIcon() + Box( + modifier = Modifier.weight(1f), + ) { + it() + if (value.text.isEmpty()) { + hint?.let { + Text( + text = it, + style = hintTextStyle.copy(color = hintTextColor), + ) + } + } + } + trailingIcon() + } + + if (underlineEnabled) { + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(if (isFocused) underlineColorFocused else underlineColor) + .fillMaxWidth() + .height(underlineWidth), + ) + } + } + }, + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/components/compose/IOSStyleTopBar.kt b/app/src/main/java/com/wafflestudio/snutt2/components/compose/IOSStyleTopBar.kt new file mode 100644 index 000000000..b03956e7b --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/components/compose/IOSStyleTopBar.kt @@ -0,0 +1,78 @@ +package com.wafflestudio.snutt2.components.compose + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.wafflestudio.snutt2.ui.SNUTTColors +import com.wafflestudio.snutt2.ui.SNUTTTypography + +@Composable +fun IOSStyleTopBar( + modifier: Modifier = Modifier, + title: String, + backButtonText: String, + onBack: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(color = SNUTTColors.White900) + .height(40.dp) + .drawWithCache { + onDrawWithContent { + drawLine( + color = SNUTTColors.EditTextHint, + start = Offset(0f, this.size.height), end = Offset(this.size.width, this.size.height), + strokeWidth = 0.5.dp.toPx(), + ) + drawContent() + } + } + .padding(horizontal = 5.dp), + contentAlignment = Alignment.CenterStart, + ) { + Row( + modifier = Modifier.clicks(1000L) { onBack() }, + verticalAlignment = Alignment.CenterVertically, + ) { + ArrowBackIcon( + colorFilter = ColorFilter.tint(SNUTTColors.Black900), + ) + AnimatedContent(backButtonText, label = "") { + Text( + text = it, + style = SNUTTTypography.button, + ) + } + } + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text( + text = title, + style = SNUTTTypography.h3, + ) + } + } +} + +@Preview +@Composable +fun IOSStyleTopBarPreview() { + IOSStyleTopBar( + title = "비밀번호 재설정", + backButtonText = "로그인", + ) { } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/components/compose/ProgressDialog.kt b/app/src/main/java/com/wafflestudio/snutt2/components/compose/ProgressDialog.kt index 3647a8886..ffbac05e7 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/components/compose/ProgressDialog.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/components/compose/ProgressDialog.kt @@ -108,6 +108,13 @@ fun CustomDialog( } } } + } ?: positiveButtonText?.let { + Row(modifier = Modifier.padding(vertical = 20.dp, horizontal = 30.dp)) { + Box(modifier = Modifier.weight(1f)) + Box(modifier = Modifier.clicks { onConfirm() }) { + Text(text = positiveButtonText, style = SNUTTTypography.body1) + } + } } } } diff --git a/app/src/main/java/com/wafflestudio/snutt2/components/compose/WebViewStyleButton.kt b/app/src/main/java/com/wafflestudio/snutt2/components/compose/WebViewStyleButton.kt index 78afe6442..a3136f3e6 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/components/compose/WebViewStyleButton.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/components/compose/WebViewStyleButton.kt @@ -3,6 +3,7 @@ package com.wafflestudio.snutt2.components.compose import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,8 +23,8 @@ fun WebViewStyleButton( Box( modifier = modifier .clicks { if (enabled) onClick() } - .background(if (enabled) enabledColor else disabledColor) - .height(60.dp), + .background(shape = RoundedCornerShape(6.dp), color = if (enabled) enabledColor else disabledColor) + .height(47.dp), contentAlignment = Alignment.Center, ) { content() diff --git a/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepository.kt b/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepository.kt index 562b46b61..226a5487c 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepository.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepository.kt @@ -92,7 +92,7 @@ interface UserRepository { suspend fun verifyPwResetCode(id: String, code: String) - suspend fun resetPassword(id: String, password: String) + suspend fun resetPassword(id: String, password: String, code: String) suspend fun sendCodeToEmail(email: String) diff --git a/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepositoryImpl.kt b/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepositoryImpl.kt index 60aa671b6..bc887ed76 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepositoryImpl.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/data/user/UserRepositoryImpl.kt @@ -258,9 +258,9 @@ class UserRepositoryImpl @Inject constructor( ) } - override suspend fun resetPassword(id: String, password: String) { + override suspend fun resetPassword(id: String, password: String, code: String) { api._postResetPassword( - PostResetPasswordParams(id, password), + PostResetPasswordParams(id, password, code), ) } diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt index 28ceb131d..c2696cee6 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt @@ -173,4 +173,6 @@ object SNUTTStringUtils { append("(${quota - freshmanQuota})") } }.toString() + + fun String.isValidPassword(): Boolean = Regex("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,20}$").matches(this) } diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/PostResetPasswordParams.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/PostResetPasswordParams.kt index 581104807..9252bec40 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/PostResetPasswordParams.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/network/dto/PostResetPasswordParams.kt @@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass data class PostResetPasswordParams( @Json(name = "user_id") val id: String, @Json(name = "password") val password: String, + @Json(name = "code") val code: String, ) diff --git a/app/src/main/java/com/wafflestudio/snutt2/ui/Colors.kt b/app/src/main/java/com/wafflestudio/snutt2/ui/Colors.kt index 503512fb7..0f456a043 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/ui/Colors.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/ui/Colors.kt @@ -58,6 +58,10 @@ object SNUTTColors { val VacancyRed = Color(0xffed6c58) + val EditTextLabel = Color(0xff8a898e) + val EditTextHint = Color(0xffc4c4c4) + val EditTextUnderline = Color(0xffdcdcde) + val Colors.VacancyBlue @Composable get() = if (isLight) Color(0xff446cc2) else Color(0xff7aaaf3) val VacancyBlue @Composable get() = MaterialTheme.colors.VacancyBlue diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt b/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt index 3d3964f92..fa425be8a 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/RootActivity.kt @@ -77,6 +77,7 @@ import com.wafflestudio.snutt2.views.logged_in.table_lectures.LecturesOfTablePag import com.wafflestudio.snutt2.views.logged_in.vacancy_noti.VacancyPage import com.wafflestudio.snutt2.views.logged_in.vacancy_noti.VacancyViewModel import com.wafflestudio.snutt2.views.logged_out.* +import com.wafflestudio.snutt2.views.logged_out.reset_password.ResetPasswordPage import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import javax.inject.Inject @@ -348,7 +349,7 @@ class RootActivity : AppCompatActivity() { } composable2(NavigationDestination.FindPassword) { - FindPasswordPage() + ResetPasswordPage() } composable2(NavigationDestination.EmailVerification) { diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/UserViewModel.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/UserViewModel.kt index 9f6a5c840..d1900a8ab 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/UserViewModel.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/settings/UserViewModel.kt @@ -136,8 +136,8 @@ class UserViewModel @Inject constructor( userRepository.verifyPwResetCode(id, code) } - suspend fun resetPassword(id: String, password: String) { - userRepository.resetPassword(id, password) + suspend fun resetPassword(id: String, password: String, code: String) { + userRepository.resetPassword(id, password, code) } suspend fun sendCodeToEmail(email: String) { diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/FindPasswordPage.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/FindPasswordPage.kt deleted file mode 100644 index 9695cf71f..000000000 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/FindPasswordPage.kt +++ /dev/null @@ -1,331 +0,0 @@ -package com.wafflestudio.snutt2.views.logged_out - -import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.wafflestudio.snutt2.R -import com.wafflestudio.snutt2.components.compose.* -import com.wafflestudio.snutt2.lib.android.toast -import com.wafflestudio.snutt2.ui.SNUTTColors -import com.wafflestudio.snutt2.ui.SNUTTTypography -import com.wafflestudio.snutt2.views.LocalApiOnError -import com.wafflestudio.snutt2.views.LocalApiOnProgress -import com.wafflestudio.snutt2.views.LocalNavController -import com.wafflestudio.snutt2.views.launchSuspendApi -import com.wafflestudio.snutt2.views.logged_in.home.settings.UserViewModel -import kotlinx.coroutines.launch - -private enum class FlowState { - CheckEmail, SendCode, ResetPassword, -} - -@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class) -@Composable -fun FindPasswordPage() { - val navController = LocalNavController.current - val apiOnError = LocalApiOnError.current - val apiOnProgress = LocalApiOnProgress.current - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val focusManager = LocalFocusManager.current - val keyboardManager = LocalSoftwareKeyboardController.current - val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - - val userViewModel = hiltViewModel() - - var flowState by remember { mutableStateOf(FlowState.CheckEmail) } - - var idField by remember { mutableStateOf("") } - var codeField by remember { mutableStateOf("") } - var passwordField by remember { mutableStateOf("") } - var passwordConfirmField by remember { mutableStateOf("") } - var checkEmailDialogState by remember { mutableStateOf(false) } - - val buttonEnabled by remember { - derivedStateOf { - when (flowState) { - FlowState.CheckEmail -> idField.isNotEmpty() - FlowState.SendCode -> codeField.isNotEmpty() - FlowState.ResetPassword -> passwordField.isNotEmpty() && passwordConfirmField.isNotEmpty() - } - } - } - - var emailResponse by remember { mutableStateOf("") } - - val timerState = rememberTimerState( - initialValue = TimerValue.Initial, - durationInSecond = 180, - ) - val handleCheckEmailById = { - coroutineScope.launch { - if (idField.isEmpty()) { - context.toast(context.getString(R.string.find_password_enter_id_hint)) - } else { - launchSuspendApi(apiOnProgress, apiOnError) { - emailResponse = userViewModel.checkEmailById(idField) - keyboardManager?.hide() - checkEmailDialogState = true - } - } - } - } - - val handleEnterCode = { - coroutineScope.launch { - if (codeField.isEmpty()) { - context.toast(context.getString(R.string.find_password_enter_verification_code_empty_alert)) - } else if (timerState.isEnded) { - context.toast(context.getString(R.string.find_password_enter_verification_code_expire_message)) - } else { - launchSuspendApi(apiOnProgress, apiOnError) { - userViewModel.verifyPwResetCode(idField, codeField) - keyboardManager?.hide() - context.toast(context.getString(R.string.find_password_enter_verification_code_success_alert)) - timerState.pause() - flowState = FlowState.ResetPassword - } - } - } - } - - val handleResetPassword = { - coroutineScope.launch { - if (passwordField.isEmpty() || passwordConfirmField.isEmpty()) { - context.toast(context.getString(R.string.find_password_enter_password_empty_alert)) - } else if (passwordConfirmField != passwordField) { - context.toast(context.getString(R.string.find_password_enter_password_confirm_fail_alert)) - } else { - launchSuspendApi(apiOnProgress, apiOnError) { - userViewModel.resetPassword(idField, passwordField) - keyboardManager?.hide() - context.toast(context.getString(R.string.find_password_enter_password_success_alert)) - navController.popBackStack() - } - } - } - } - - val onBackPressed: () -> Unit = { - when (flowState) { - FlowState.CheckEmail -> navController.popBackStack() - FlowState.SendCode -> { - flowState = FlowState.CheckEmail - codeField = "" - timerState.reset() - } - FlowState.ResetPassword -> flowState = FlowState.SendCode - } - } - - BackHandler { - onBackPressed() - } - - Column( - modifier = Modifier - .fillMaxSize() - .background(SNUTTColors.White900) - .clicks { focusManager.clearFocus() }, - ) { - SimpleTopBar(title = stringResource(R.string.find_password_title), onClickNavigateBack = { onBackPressed() }) - AnimatedContent(targetState = flowState) { targetState -> - Column(modifier = Modifier.padding(horizontal = 25.dp)) { - when (targetState) { - FlowState.CheckEmail -> { - Text( - text = stringResource(R.string.find_password_check_email_content), - style = SNUTTTypography.h3, - modifier = Modifier.padding(vertical = 25.dp), - ) - Text( - text = stringResource(R.string.sign_in_id_hint), - style = SNUTTTypography.h4, - ) - EditText( - value = idField, - onValueChange = { idField = it }, - hint = stringResource(R.string.find_password_enter_id_hint), - keyboardActions = KeyboardActions(onNext = { - focusManager.moveFocus( - FocusDirection.Down, - ) - },), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - ) - } - FlowState.SendCode -> { - Text( - text = stringResource(R.string.find_password_verification_code_content).format( - emailResponse, - ), - style = SNUTTTypography.h3, - modifier = Modifier.padding(vertical = 25.dp), - ) - Text( - text = stringResource(R.string.find_password_send_code_label), - style = SNUTTTypography.h4, - ) - EditText( - value = codeField, - onValueChange = { codeField = it }, - hint = stringResource(R.string.find_password_send_code_hint), - keyboardActions = KeyboardActions(onNext = { - focusManager.moveFocus( - FocusDirection.Down, - ) - },), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done, keyboardType = KeyboardType.Ascii, - ), - singleLine = true, - trailingIcon = { - Row( - modifier = Modifier.padding(start = 10.dp), - ) { - Timer( - state = timerState, - endMessage = stringResource(R.string.find_password_send_code_resend), - ) { timerText -> - Text( - text = timerText, - style = SNUTTTypography.subtitle2.copy( - color = if (timerState.isRunning) { - SNUTTColors.Red - } else { - SNUTTColors.SNUTTTheme - }, - ), - modifier = Modifier.clicks { - if (timerState.isEnded) { - coroutineScope.launch { - launchSuspendApi(apiOnProgress, apiOnError) { - userViewModel.sendPwResetCodeToEmail(emailResponse) - timerState.reset() - timerState.start() - } - } - } - }, - ) - } - } - }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - ) - if (timerState.isEnded) { - Text( - text = stringResource(R.string.find_password_enter_verification_code_expire_message), - style = SNUTTTypography.body2.copy(color = SNUTTColors.Red), - ) - } - } - FlowState.ResetPassword -> { - Spacer(modifier = Modifier.height(25.dp)) - Text( - text = stringResource(R.string.find_password_enter_password_label), - style = SNUTTTypography.h4, - ) - EditText( - value = passwordField, - onValueChange = { passwordField = it }, - hint = stringResource(R.string.sign_up_password_hint), - keyboardActions = KeyboardActions(onNext = { - focusManager.moveFocus( - FocusDirection.Down, - ) - },), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - ) - Spacer(modifier = Modifier.height(10.dp)) - Text( - text = stringResource(R.string.find_password_enter_password_confirm_label), - style = SNUTTTypography.h4, - ) - EditText( - value = passwordConfirmField, - onValueChange = { passwordConfirmField = it }, - hint = stringResource(R.string.sign_up_password_confirm_hint), - keyboardActions = KeyboardActions(onNext = { - focusManager.moveFocus( - FocusDirection.Down, - ) - },), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp), - ) - } - } - Spacer(modifier = Modifier.height(30.dp)) - WebViewStyleButton( - modifier = Modifier.fillMaxWidth(), - enabled = buttonEnabled, - onClick = { - when (flowState) { - FlowState.CheckEmail -> handleCheckEmailById() - FlowState.SendCode -> handleEnterCode() - FlowState.ResetPassword -> handleResetPassword() - } - }, - ) { - Text( - text = stringResource(R.string.common_ok), - style = SNUTTTypography.h3.copy(color = SNUTTColors.AllWhite), - ) - } - } - } - } - - if (checkEmailDialogState) { - CustomDialog( - title = stringResource(R.string.find_password_check_email_dialog_title), - onDismiss = { checkEmailDialogState = false }, - onConfirm = { - checkEmailDialogState = false - coroutineScope.launch { - launchSuspendApi(apiOnProgress, apiOnError) { - userViewModel.sendPwResetCodeToEmail(emailResponse) - flowState = FlowState.SendCode - timerState.start() - } - } - }, - positiveButtonText = stringResource(R.string.common_ok), - negativeButtonText = stringResource(R.string.find_password_check_email_dialog_negative), - ) { - Text(text = stringResource(R.string.find_password_check_email_dialog_content).format(emailResponse), style = SNUTTTypography.body1) - } - } -} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/CheckIdStep.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/CheckIdStep.kt new file mode 100644 index 000000000..3ce8b611c --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/CheckIdStep.kt @@ -0,0 +1,123 @@ +package com.wafflestudio.snutt2.views.logged_out.reset_password + +import androidx.compose.foundation.layout.Column +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.components.compose.EditTextFieldValue +import com.wafflestudio.snutt2.components.compose.WebViewStyleButton +import com.wafflestudio.snutt2.lib.android.toast +import com.wafflestudio.snutt2.ui.SNUTTColors +import com.wafflestudio.snutt2.ui.SNUTTTypography + +@Composable +fun CheckIdStep( + uiState: FindPasswordViewModel.UIState.CheckId, + onSubmit: (String) -> Unit, +) { + val focusRequester = remember { FocusRequester() } + val context = LocalContext.current + + var idField by remember { + mutableStateOf( + TextFieldValue( + text = uiState.userId, + selection = TextRange(uiState.userId.length), // 초기 커서를 텍스트 끝으로 설정 + ), + ) + } + val buttonEnabled by remember { + derivedStateOf { + idField.text.isNotEmpty() + } + } + + val sendIdAndRequestMaskedEmail = { + if (idField.text.isEmpty()) { + context.toast(context.getString(R.string.find_password_enter_id_hint)) + } else { + onSubmit(idField.text) + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 44.dp, horizontal = 20.dp), + ) { + Text( + text = stringResource(R.string.find_password_check_email_content), + style = SNUTTTypography.h2.copy(fontSize = 17.sp), + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = stringResource(R.string.sign_in_id_title), + style = SNUTTTypography.body1.copy(color = SNUTTColors.EditTextLabel), + ) + + Spacer(modifier = Modifier.height(12.dp)) + + EditTextFieldValue( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .onFocusChanged { + idField = idField.copy(selection = TextRange(idField.text.length)) + }, + value = idField, + onValueChange = { idField = it }, + hint = stringResource(R.string.find_password_enter_id_hint), + keyboardActions = KeyboardActions( + onDone = { + onSubmit(idField.text) + }, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + singleLine = true, + underlineColorFocused = if (buttonEnabled) SNUTTColors.SNUTTTheme else SNUTTColors.EditTextUnderline, + ) + + Spacer(modifier = Modifier.height(48.dp)) + + WebViewStyleButton( + modifier = Modifier.fillMaxWidth(), + enabled = buttonEnabled, + onClick = sendIdAndRequestMaskedEmail, + ) { + Text( + text = stringResource(R.string.common_ok), + style = SNUTTTypography.h3.copy(color = if (buttonEnabled) SNUTTColors.AllWhite else SNUTTColors.VacancyGray), + ) + } + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/EnterFullEmailStep.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/EnterFullEmailStep.kt new file mode 100644 index 000000000..8c3fdb941 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/EnterFullEmailStep.kt @@ -0,0 +1,162 @@ +package com.wafflestudio.snutt2.views.logged_out.reset_password + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.components.compose.EditText +import com.wafflestudio.snutt2.components.compose.EditTextFieldValue +import com.wafflestudio.snutt2.components.compose.WebViewStyleButton +import com.wafflestudio.snutt2.components.compose.clicks +import com.wafflestudio.snutt2.lib.android.toast +import com.wafflestudio.snutt2.lib.data.SNUTTStringUtils.isEmailInvalid +import com.wafflestudio.snutt2.ui.SNUTTColors +import com.wafflestudio.snutt2.ui.SNUTTTypography + +@Composable +fun EnterFullEmailStep( + uiState: FindPasswordViewModel.UIState.EnterFullEmail, + notMyEmail: () -> Unit, + onSubmitFullEmail: (String) -> Unit, +) { + val focusRequester = remember { FocusRequester() } + val context = LocalContext.current + + var emailField by remember { + mutableStateOf( + TextFieldValue( + text = uiState.fullEmail, + selection = TextRange(uiState.fullEmail.length), // 초기 커서를 텍스트 끝으로 설정 + ), + ) + } + val buttonEnabled by remember { + derivedStateOf { + !emailField.text.isEmailInvalid() + } + } + + val sendIdAndRequestMaskedEmail: () -> Unit = { + if (buttonEnabled) { + if (emailField.text.isEmpty()) { + context.toast(context.getString(R.string.find_password_enter_id_hint)) + } else { + onSubmitFullEmail(emailField.text) + } + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 44.dp, horizontal = 20.dp), + ) { + Text( + text = stringResource(R.string.find_password_check_email_enter_full_email), + style = SNUTTTypography.h3, + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = stringResource(R.string.sign_in_id_title), + style = SNUTTTypography.body1.copy(color = SNUTTColors.EditTextLabel), + ) + + Spacer(modifier = Modifier.height(12.dp)) + + EditText( + modifier = Modifier.fillMaxWidth(), + value = "", + onValueChange = {}, + hint = uiState.userId, + enabled = false, + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Text( + text = stringResource(R.string.find_password_check_email_enter_full_email_label), + style = SNUTTTypography.body1.copy(color = SNUTTColors.EditTextLabel), + ) + + Spacer(modifier = Modifier.height(12.dp)) + + EditTextFieldValue( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = emailField, + onValueChange = { emailField = it }, + hint = stringResource(R.string.find_password_check_email_enter_full_email_hint), + keyboardActions = KeyboardActions( + onDone = { + sendIdAndRequestMaskedEmail() + }, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + singleLine = true, + underlineColorFocused = if (emailField.text.isBlank()) SNUTTColors.EditTextUnderline else SNUTTColors.SNUTTTheme, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = uiState.maskedEmail, + style = SNUTTTypography.body1.copy(color = SNUTTColors.EditTextLabel), + ) + + Spacer(modifier = Modifier.height(48.dp)) + + WebViewStyleButton( + modifier = Modifier.fillMaxWidth(), + enabled = buttonEnabled, + onClick = sendIdAndRequestMaskedEmail, + ) { + Text( + text = stringResource(R.string.find_password_check_email_enter_full_email_enter), + style = SNUTTTypography.h3.copy(color = if (buttonEnabled) SNUTTColors.AllWhite else SNUTTColors.VacancyGray), + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + text = stringResource(R.string.find_password_check_email_enter_full_email_not_mine), + style = SNUTTTypography.body1.copy(color = SNUTTColors.VacancyGray), + modifier = Modifier.clicks { + notMyEmail() + }, + ) + } + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/FindPasswordViewModel.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/FindPasswordViewModel.kt new file mode 100644 index 000000000..eb6495b40 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/FindPasswordViewModel.kt @@ -0,0 +1,84 @@ +package com.wafflestudio.snutt2.views.logged_out.reset_password + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.wafflestudio.snutt2.data.user.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@HiltViewModel +class FindPasswordViewModel @Inject constructor( + private val userRepository: UserRepository, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val _uiState = MutableStateFlow(UIState.CheckId("")) + val uiState: StateFlow = _uiState + + sealed class UIState { + data class CheckId( + val userId: String, + ) : UIState() + + data class EnterFullEmail( + val userId: String, + val maskedEmail: String, + val fullEmail: String, + ) : UIState() + + data class VerifyCode( + val fullEmail: String, + ) : UIState() + + data object EnterNewPassword : UIState() + } + + fun goToPreviousStep() { + when (_uiState.value) { + is UIState.CheckId -> {} + is UIState.EnterFullEmail -> { + val savedUserId = savedStateHandle["userId"] ?: "" + _uiState.value = UIState.CheckId(savedUserId) + } + is UIState.VerifyCode -> { + val savedUserId = savedStateHandle["userId"] ?: "" + val savedMaskedEmail = savedStateHandle["maskedEmail"] ?: "" + val savedFullEmail = savedStateHandle["fullEmail"] ?: "" + _uiState.value = UIState.EnterFullEmail(savedUserId, savedMaskedEmail, savedFullEmail) + } + is UIState.EnterNewPassword -> { + val savedUserId = savedStateHandle["userId"] ?: "" + _uiState.value = UIState.CheckId(savedUserId) + } + } + } + + suspend fun checkEmailById(userId: String) { + savedStateHandle["userId"] = userId + val maskedEmail = userRepository.checkEmailById(userId) + savedStateHandle["maskedEmail"] = maskedEmail + val savedFullEmail = savedStateHandle["fullEmail"] ?: "" + _uiState.value = UIState.EnterFullEmail(userId, maskedEmail, savedFullEmail) + } + + suspend fun sendFullEmailAndRequestCode(fullEmail: String) { + savedStateHandle["fullEmail"] = fullEmail + userRepository.sendPwResetCodeToEmail(fullEmail) + _uiState.value = UIState.VerifyCode(fullEmail) + } + + suspend fun verifyCode(code: String) { + val savedUserId = savedStateHandle["userId"] ?: "" + userRepository.verifyPwResetCode(savedUserId, code) + savedStateHandle["code"] = code + _uiState.value = UIState.EnterNewPassword + } + + suspend fun resetPassword(password: String) { + val savedUserId = savedStateHandle["userId"] ?: "" + val savedCode = savedStateHandle["code"] ?: "" + userRepository.resetPassword(savedUserId, password, savedCode) + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/NewPasswordStep.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/NewPasswordStep.kt new file mode 100644 index 000000000..4a6e61444 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/NewPasswordStep.kt @@ -0,0 +1,213 @@ +package com.wafflestudio.snutt2.views.logged_out.reset_password + +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.components.compose.CustomDialog +import com.wafflestudio.snutt2.components.compose.EditText +import com.wafflestudio.snutt2.components.compose.Timer +import com.wafflestudio.snutt2.components.compose.TimerValue +import com.wafflestudio.snutt2.components.compose.WebViewStyleButton +import com.wafflestudio.snutt2.components.compose.rememberTimerState +import com.wafflestudio.snutt2.lib.data.SNUTTStringUtils.isValidPassword +import com.wafflestudio.snutt2.ui.SNUTTColors +import com.wafflestudio.snutt2.ui.SNUTTTypography + +@Composable +fun NewPasswordStep( + onSubmit: (String) -> Unit, + showCompleteDialog: State, + onComplete: () -> Unit, +) { + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + val context = LocalContext.current + + var newPasswordField by remember { mutableStateOf("") } + var newPasswordConfirmField by remember { mutableStateOf("") } + + var showErrorDialog by remember { mutableStateOf(false) } + var errorDialogTitle by remember { mutableStateOf("") } + + val timerState = rememberTimerState( + initialValue = TimerValue.Initial, + durationInSecond = 180, + ) + val buttonEnabled by remember { + derivedStateOf { + timerState.isRunning && newPasswordField.isNotBlank() && newPasswordConfirmField.isNotBlank() + } + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + timerState.start() + } + LaunchedEffect(timerState.currentValue) { + if (timerState.isEnded) { + errorDialogTitle = context.getString(R.string.find_password_enter_password_confirm_expired_alert) + showErrorDialog = true + } + } + LaunchedEffect(showCompleteDialog.value) { + if (showCompleteDialog.value) { + timerState.pause() + } + } + + val validateNewPasswordAndSubmit = { + if (timerState.isRunning) { + if (newPasswordField != newPasswordConfirmField) { + errorDialogTitle = context.getString(R.string.find_password_enter_password_confirm_fail_alert) + showErrorDialog = true + } else if (newPasswordField.isValidPassword().not()) { + errorDialogTitle = context.getString(R.string.find_password_enter_password_confirm_validation_fail_alert) + showErrorDialog = true + } else { + onSubmit(newPasswordField) + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 44.dp, horizontal = 20.dp), + ) { + Text( + text = stringResource(R.string.find_password_enter_password_body), + style = SNUTTTypography.h2.copy(fontSize = 17.sp), + ) + + Spacer(modifier = Modifier.height(40.dp)) + + EditText( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = newPasswordField, + onValueChange = { newPasswordField = it }, + hint = stringResource(R.string.find_password_enter_password_hint), + visualTransformation = PasswordVisualTransformation(), + keyboardActions = KeyboardActions( + onNext = { + focusManager.moveFocus(FocusDirection.Down) + }, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + singleLine = true, + underlineColor = SNUTTColors.EditTextUnderline, + underlineColorFocused = if (newPasswordField.isBlank()) SNUTTColors.EditTextUnderline else SNUTTColors.SNUTTTheme, + trailingIcon = { + Row( + modifier = Modifier.padding(start = 10.dp), + ) { + Timer( + state = timerState, + endMessage = stringResource(R.string.find_password_enter_password_confirm_expired), + ) { timerText -> + Text( + text = timerText, + style = SNUTTTypography.subtitle2.copy( + fontSize = 15.sp, + color = if (timerState.isRunning) { + SNUTTColors.Red + } else { + SNUTTColors.SNUTTTheme + }, + ), + modifier = Modifier + .padding(end = 10.dp), + ) + } + } + }, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + EditText( + modifier = Modifier.fillMaxWidth(), + value = newPasswordConfirmField, + onValueChange = { newPasswordConfirmField = it }, + hint = stringResource(R.string.find_password_enter_password_confirm_hint), + visualTransformation = PasswordVisualTransformation(), + keyboardActions = KeyboardActions( + onDone = { + validateNewPasswordAndSubmit() + }, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + singleLine = true, + underlineColor = SNUTTColors.EditTextUnderline, + underlineColorFocused = if (newPasswordConfirmField.isBlank()) SNUTTColors.EditTextUnderline else SNUTTColors.SNUTTTheme, + ) + + Spacer(modifier = Modifier.height(48.dp)) + + WebViewStyleButton( + modifier = Modifier.fillMaxWidth(), + enabled = buttonEnabled, + onClick = { + validateNewPasswordAndSubmit() + }, + ) { + Text( + text = stringResource(R.string.common_ok), + style = SNUTTTypography.h3.copy(color = if (buttonEnabled) SNUTTColors.AllWhite else SNUTTColors.VacancyGray), + ) + } + } + + if (showErrorDialog) { + CustomDialog( + title = errorDialogTitle, + onConfirm = { + showErrorDialog = false + focusRequester.requestFocus() + }, + onDismiss = {}, + positiveButtonText = stringResource(R.string.common_ok), + negativeButtonText = null, + ) { + } + } + + if (showCompleteDialog.value) { + CustomDialog( + title = stringResource(R.string.find_password_enter_password_success_alert), + onConfirm = onComplete, + onDismiss = {}, + positiveButtonText = stringResource(R.string.common_ok), + negativeButtonText = null, + ) { + } + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/ResetPasswordPage.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/ResetPasswordPage.kt new file mode 100644 index 000000000..73d9e9d74 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/ResetPasswordPage.kt @@ -0,0 +1,134 @@ +package com.wafflestudio.snutt2.views.logged_out.reset_password + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.components.compose.IOSStyleTopBar +import com.wafflestudio.snutt2.views.LocalApiOnError +import com.wafflestudio.snutt2.views.LocalApiOnProgress +import com.wafflestudio.snutt2.views.LocalNavController +import com.wafflestudio.snutt2.views.launchSuspendApi +import com.wafflestudio.snutt2.views.logged_out.reset_password.FindPasswordViewModel.UIState.CheckId +import com.wafflestudio.snutt2.views.logged_out.reset_password.FindPasswordViewModel.UIState.EnterFullEmail +import com.wafflestudio.snutt2.views.logged_out.reset_password.FindPasswordViewModel.UIState.EnterNewPassword +import com.wafflestudio.snutt2.views.logged_out.reset_password.FindPasswordViewModel.UIState.VerifyCode +import kotlinx.coroutines.launch + +@Composable +fun ResetPasswordPage() { + val scope = rememberCoroutineScope() + val apiOnProgress = LocalApiOnProgress.current + val apiOnError = LocalApiOnError.current + val navController = LocalNavController.current + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + BackHandler { + if (uiState is CheckId) { + navController.popBackStack() + } else { + viewModel.goToPreviousStep() + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .imePadding(), + ) { + IOSStyleTopBar( + title = stringResource(R.string.find_password_title), + backButtonText = when (uiState) { + is CheckId -> stringResource(R.string.find_password_back_login) + is EnterFullEmail -> stringResource(R.string.find_password_back_check_id) + is VerifyCode -> stringResource(R.string.find_password_back_check_email) + is EnterNewPassword -> stringResource(R.string.find_password_back_initial) + }, + ) { + if (uiState is CheckId) { + navController.popBackStack() + } else { + viewModel.goToPreviousStep() + } + } + + AnimatedContent(targetState = uiState, label = "") { state -> + when (state) { + is CheckId -> CheckIdStep( + uiState = state, + onSubmit = { userId -> + scope.launch { + launchSuspendApi(apiOnProgress, apiOnError) { + viewModel.checkEmailById(userId) + } + } + }, + ) + + is EnterFullEmail -> EnterFullEmailStep( + uiState = state, + notMyEmail = viewModel::goToPreviousStep, + onSubmitFullEmail = { fullEmail -> + scope.launch { + launchSuspendApi(apiOnProgress, apiOnError) { + viewModel.sendFullEmailAndRequestCode(fullEmail) + } + } + }, + ) + + is VerifyCode -> VerifyCodeStep( + uiState = state, + onRequestResend = { + scope.launch { + launchSuspendApi(apiOnProgress, apiOnError) { + viewModel.sendFullEmailAndRequestCode(state.fullEmail) + } + } + }, + onSubmit = { code -> + scope.launch { + launchSuspendApi(apiOnProgress, apiOnError) { + viewModel.verifyCode(code) + } + } + }, + ) + + is EnterNewPassword -> { + val showCompleteDialog = remember { mutableStateOf(false) } + NewPasswordStep( + onSubmit = { newPassword -> + scope.launch { + launchSuspendApi(apiOnProgress, apiOnError) { + viewModel.resetPassword(newPassword) + showCompleteDialog.value = true + } + } + }, + showCompleteDialog = showCompleteDialog, + onComplete = { + showCompleteDialog.value = false + navController.popBackStack() + }, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/VerifyCodeStep.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/VerifyCodeStep.kt new file mode 100644 index 000000000..8a118454a --- /dev/null +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_out/reset_password/VerifyCodeStep.kt @@ -0,0 +1,191 @@ +package com.wafflestudio.snutt2.views.logged_out.reset_password + +import androidx.compose.foundation.layout.Box +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.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +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 +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.components.compose.CustomDialog +import com.wafflestudio.snutt2.components.compose.EditText +import com.wafflestudio.snutt2.components.compose.Timer +import com.wafflestudio.snutt2.components.compose.TimerValue +import com.wafflestudio.snutt2.components.compose.WebViewStyleButton +import com.wafflestudio.snutt2.components.compose.clicks +import com.wafflestudio.snutt2.components.compose.rememberTimerState +import com.wafflestudio.snutt2.ui.SNUTTColors +import com.wafflestudio.snutt2.ui.SNUTTTypography + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun VerifyCodeStep( + uiState: FindPasswordViewModel.UIState.VerifyCode, + onRequestResend: () -> Unit, + onSubmit: (String) -> Unit, +) { + val keyboardManager = LocalSoftwareKeyboardController.current + val focusRequester = remember { FocusRequester() } + var codeField by remember { mutableStateOf("") } + var showWhyNotCodeComingDialog by remember { mutableStateOf(false) } + val timerState = rememberTimerState( + initialValue = TimerValue.Initial, + durationInSecond = 180, + ) + val buttonEnabled by remember { + derivedStateOf { + codeField.length == 8 && timerState.isRunning + } + } + + LaunchedEffect(Unit) { + timerState.start() + } + LaunchedEffect(showWhyNotCodeComingDialog) { + if (showWhyNotCodeComingDialog.not()) { + focusRequester.requestFocus() + keyboardManager?.show() + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 44.dp, horizontal = 20.dp), + ) { + Text( + text = stringResource(R.string.find_password_verification_code_content, uiState.fullEmail), + style = SNUTTTypography.h2.copy(fontSize = 17.sp), + ) + + Spacer(modifier = Modifier.height(48.dp)) + + Text( + text = stringResource(R.string.find_password_send_code_label), + style = SNUTTTypography.body1.copy(color = SNUTTColors.EditTextLabel), + ) + + Spacer(modifier = Modifier.height(12.dp)) + + EditText( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = codeField, + onValueChange = { codeField = it }, + hint = stringResource(R.string.find_password_send_code_hint), + keyboardActions = KeyboardActions( + onDone = { + if (buttonEnabled) { + onSubmit(codeField) + } + }, + ), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + singleLine = true, + underlineColorFocused = if (codeField.isBlank()) SNUTTColors.EditTextUnderline else SNUTTColors.SNUTTTheme, + trailingIcon = { + Row( + modifier = Modifier.padding(start = 10.dp), + ) { + Timer( + state = timerState, + endMessage = stringResource(R.string.find_password_send_code_resend), + ) { timerText -> + Text( + text = timerText, + style = SNUTTTypography.subtitle2.copy( + fontSize = 15.sp, + color = if (timerState.isRunning) { + SNUTTColors.Red + } else { + SNUTTColors.SNUTTTheme + }, + ), + modifier = Modifier + .clicks { + if (timerState.isEnded) { + onRequestResend() + timerState.reset() + timerState.start() + } + } + .padding(end = 10.dp), + ) + } + } + }, + ) + + if (timerState.isEnded) { + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = stringResource(R.string.find_password_enter_verification_code_expire_message), + style = SNUTTTypography.body1.copy(fontSize = 13.sp, color = SNUTTColors.Red), + ) + } + + Spacer(modifier = Modifier.height(48.dp)) + + WebViewStyleButton( + modifier = Modifier.fillMaxWidth(), + enabled = buttonEnabled, + onClick = { + onSubmit(codeField) + }, + ) { + Text( + text = stringResource(R.string.common_ok), + style = SNUTTTypography.h3.copy(color = if (buttonEnabled) SNUTTColors.AllWhite else SNUTTColors.VacancyGray), + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + text = stringResource(R.string.find_password_send_code_not_coming), + style = SNUTTTypography.body1.copy(color = SNUTTColors.VacancyGray), + modifier = Modifier.clicks { + showWhyNotCodeComingDialog = true + }, + ) + } + } + + if (showWhyNotCodeComingDialog) { + CustomDialog( + title = stringResource(R.string.find_password_enter_verification_code_why_not_coming), + onConfirm = { + showWhyNotCodeComingDialog = false + }, + onDismiss = {}, + positiveButtonText = stringResource(R.string.common_ok), + negativeButtonText = null, + ) { + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8712349d..4678131bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -284,24 +284,35 @@ 올바른 형식의 이메일을 입력해주세요. \"%s\"로 아이디가 전송되었습니다. 비밀번호 재설정 + 로그인 + 아이디 입력 + 이메일 입력 + 처음으로 비밀번호 재설정을 위해\n연동된 아이디가 필요합니다. - 이메일 확인 - 연동된 이메일은 %s입니다. 해당 이메일로 인증 코드를 발송하시겠습니까? - 다른 아이디 확인하기 - 아이디를 입력해주세요. - 인증 코드 입력 - 인증 번호 + 해당 아이디로 연동된 이메일입니다.\n전체 주소를 입력하여 인증코드를 받으세요. + 이메일 + 전체 주소를 입력하세요 + 인증코드 받기 + 나의 이메일 주소가 아닌가요? + 아이디를 입력하세요. + 인증코드 8자리를 입력하세요 + 인증코드 다시 요청 - %s으로 전송된\n인증번호를 입력해주세요. + 인증번호가 오지 않나요? + %s으로 전송된\n인증코드를 입력해주세요. 인증 코드를 입력해주세요. - 인증 코드를 입력해주세요. - 시간이 초과되었습니다. 다시 시도해주세요. + 시간이 초과되었습니다. 다시 요청해주세요. 인증에 성공하였습니다. - 새로운 비밀번호 - 새로운 비밀번호 확인 + 전송에 시간이 소요될 수 있습니다.\n스팸함을 확인하거나,\n3분 초과 시 재요청을 해주세요. + 새로운 비밀번호를 입력해주세요. + 영문, 숫자 모두 포함 6–20자 이내 + 비밀번호를 한번 더 입력하세요 새 비밀번호를 입력해주세요. - 비밀번호 확인이 일치하지 않습니다. - 비밀번호 재설정이 완료되었습니다. + 비밀번호가 일치하지 않습니다.\n다시 시도해주세요. + 조건에 맞지 않는 비밀번호입니다.\n영문, 숫자 포함 6-20자 이내로\n입력해주세요. + 시간이 초과되어\n인증코드 재입력이 필요합니다. + 만료 + 비밀번호가 변경되었습니다. 이메일 인증 강의평 서비스를 이용하기 위해\n이메일 인증이 필요합니다.\n%s에 대한 이메일 인증을\n진행하시겠습니까? "나중에 하기"를 선택하더라도 강의평 서비스를 제외한 SNUTT의 모든 기능을 이용할 수 있습니다. From 74c3b7aba202ddafbe489f6a2db6451dc8cacba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=98=84=EB=8F=84?= <141345525+plgafhd@users.noreply.github.com> Date: Sat, 9 Nov 2024 00:29:42 +0900 Subject: [PATCH 2/8] =?UTF-8?q?Github=20Actions=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C=20(v3=20to=20v4)?= =?UTF-8?q?=20(#377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 8 ++++---- .github/workflows/ci.yml | 10 +++++----- .github/workflows/manual_deploy.yml | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index e2ea4aa33..e072bd52a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,9 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'adopt' @@ -77,9 +77,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'adopt' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf95c1d07..ecf21b802 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,9 @@ jobs: needs: cancel-workflow steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'adopt' @@ -33,7 +33,7 @@ jobs: - name: Run ktlintDebug run: ./gradlew ktlintMainSourceSetCheck - name: Upload ktlint report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: ktlint-result @@ -44,9 +44,9 @@ jobs: needs: static-check steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'adopt' diff --git a/.github/workflows/manual_deploy.yml b/.github/workflows/manual_deploy.yml index 31b4cdb46..a194fde81 100644 --- a/.github/workflows/manual_deploy.yml +++ b/.github/workflows/manual_deploy.yml @@ -26,9 +26,9 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: 17 distribution: 'adopt' From ba3656503a8e366aabe8f24a0f7924401f452c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=86=A1=EB=8F=99=EC=97=BD?= Date: Wed, 1 Jan 2025 21:27:17 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=EA=B0=95=EC=9D=98=20=EA=B2=B9=EC=B9=A0=20?= =?UTF-8?q?=EC=8B=9C=20alert=EC=97=90=EC=84=9C=20displayMessage=EB=A5=BC?= =?UTF-8?q?=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../snutt2/views/logged_in/home/search/SearchPageDialogs.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt index 8aa8ec2dc..6933ea949 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt @@ -59,7 +59,7 @@ fun checkLectureOverlap( when (e) { is ErrorParsedHttpException -> { if (e.errorDTO?.code == ErrorCode.LECTURE_TIME_OVERLAP) { - onLectureOverlap(e.errorDTO.ext?.get("confirm_message") ?: "") + onLectureOverlap(e.errorDTO.displayMessage ?: "") } else { apiOnError(e) } From 356f34477857296a30b0aff81e9c020b9a0ab712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=98=84=EB=8F=84?= <141345525+plgafhd@users.noreply.github.com> Date: Sat, 4 Jan 2025 17:31:53 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=9E=AC=EC=84=A4=EC=A0=95=20regex=20=EC=88=98=EC=A0=95=20(#38?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt index c2696cee6..8a08a0724 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/lib/data/SNUTTStringUtils.kt @@ -174,5 +174,5 @@ object SNUTTStringUtils { } }.toString() - fun String.isValidPassword(): Boolean = Regex("^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,20}$").matches(this) + fun String.isValidPassword(): Boolean = Regex("^(?=.*\\d)(?=.*[a-zA-Z])\\S{6,20}\$").matches(this) } From bab2fbdde7fc05013f8ab8f0b4142415c533cc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=98=84=EB=8F=84?= <141345525+plgafhd@users.noreply.github.com> Date: Mon, 13 Jan 2025 00:52:46 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EB=8B=A4=ED=81=AC=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#384)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../snutt2/components/view/TimetableView.kt | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/wafflestudio/snutt2/components/view/TimetableView.kt b/app/src/main/java/com/wafflestudio/snutt2/components/view/TimetableView.kt index f54caeffe..30a2a15eb 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/components/view/TimetableView.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/components/view/TimetableView.kt @@ -8,6 +8,7 @@ import android.view.MotionEvent import android.view.View import androidx.core.content.res.ResourcesCompat import com.wafflestudio.snutt2.R +import com.wafflestudio.snutt2.data.SNUTTStorage import com.wafflestudio.snutt2.lib.contains import com.wafflestudio.snutt2.lib.getFittingTrimParam import com.wafflestudio.snutt2.lib.network.dto.core.ClassTimeDto @@ -18,12 +19,12 @@ import com.wafflestudio.snutt2.lib.rx.sp import com.wafflestudio.snutt2.lib.toDayString import com.wafflestudio.snutt2.model.BuiltInTheme import com.wafflestudio.snutt2.model.TableTrimParam +import com.wafflestudio.snutt2.ui.isSystemDarkMode import io.reactivex.rxjava3.core.Observable import kotlin.math.max import kotlin.math.min class TimetableView : View { - private val hourLabelWidth = 24.5f.dp(context) private val dayLabelHeight = 28.5f.dp(context) private val cellPadding = 4.dp(context) @@ -132,7 +133,29 @@ class TimetableView : View { } private fun init() { - setBackgroundColor(Color.rgb(255, 255, 255)) + val sharedPreferences = context.getSharedPreferences(SNUTTStorage.DOMAIN_SCOPE_CURRENT_VERSION, Context.MODE_PRIVATE) + val themeMode = sharedPreferences.getString("theme_mode", null) ?: "" + val isDarkMode = when (themeMode) { + "\"DARK\"" -> true + "\"LIGHT\"" -> false + else -> isSystemDarkMode(context) + } + + setBackgroundColor( + if (isDarkMode) Color.rgb(43, 43, 43) else Color.rgb(255, 255, 255), + ) + linePaint.apply { + color = if (isDarkMode) Color.rgb(60, 60, 60) else Color.rgb(235, 235, 235) + } + subLinePaint.apply { + color = if (isDarkMode) Color.rgb(60, 60, 60) else Color.rgb(243, 243, 243) + } + hourLabelTextPaint.apply { + color = if (isDarkMode) Color.rgb(119, 119, 119) else Color.rgb(0, 0, 0) + } + dayLabelTextPaint.apply { + color = if (isDarkMode) Color.rgb(119, 119, 119) else Color.rgb(0, 0, 0) + } } override fun onDraw(canvas: Canvas) { From 8c12693aeee71cfa96853ef0c60c53fc27b659da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=ED=98=84=EB=8F=84?= <141345525+plgafhd@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:26:39 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=EA=B0=95=EC=9D=98=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=EC=84=9C=20pdf=EA=B0=80=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C?= =?UTF-8?q?=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=98=84=EC=83=81?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0=20(#387)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webview/SyllabusWebViewContainer.kt | 31 ------------------- .../home/search/SearchPageDialogs.kt | 14 --------- .../home/syllabus/SyllabusWebView.kt | 30 ------------------ .../lecture_detail/LectureDetailPage.kt | 16 ++++------ 4 files changed, 6 insertions(+), 85 deletions(-) delete mode 100644 app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/SyllabusWebViewContainer.kt delete mode 100644 app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/syllabus/SyllabusWebView.kt diff --git a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/SyllabusWebViewContainer.kt b/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/SyllabusWebViewContainer.kt deleted file mode 100644 index c9a777bfb..000000000 --- a/app/src/main/java/com/wafflestudio/snutt2/lib/android/webview/SyllabusWebViewContainer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.wafflestudio.snutt2.lib.android.webview - -import android.content.Context -import android.webkit.CookieManager -import android.webkit.WebResourceRequest -import android.webkit.WebView -import android.webkit.WebViewClient - -class SyllabusWebViewContainer(context: Context) : WebViewContainer { - override val webView: WebView = WebView(context).apply { - this.webViewClient = object : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { - val url = request?.url.toString() - - // NOTE: 수강스누에서 Referer 헤더를 검사해서, 레퍼러가 수강스누가 아니면 홈으로 리다이렉트 시켜 버린다. - view?.loadUrl(url, mapOf("Referer" to "https://sugang.snu.ac.kr/sugang/cc/cc100InterfaceSrch.action")) - return true - } - } - this.settings.javaScriptEnabled = true - } - - override suspend fun openPage(url: String?) { - CookieManager.getInstance().apply { - // NOTE: 웹뷰에 다른 url을 로드했을 때 "이미 수강신청 프로그램을 사용중입니다"가 뜨는 것을 방지한다. - removeAllCookies(null) - flush() - } - webView.loadUrl(url ?: "https://sugang.snu.ac.kr/") - } -} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt index 6933ea949..a4be975bf 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/search/SearchPageDialogs.kt @@ -6,14 +6,12 @@ import com.wafflestudio.snutt2.R import com.wafflestudio.snutt2.components.compose.BottomSheet import com.wafflestudio.snutt2.components.compose.ComposableStates import com.wafflestudio.snutt2.lib.android.webview.ReviewWebViewContainer -import com.wafflestudio.snutt2.lib.android.webview.WebViewContainer import com.wafflestudio.snutt2.lib.network.ErrorCode import com.wafflestudio.snutt2.lib.network.call_adapter.ErrorParsedHttpException import com.wafflestudio.snutt2.ui.SNUTTTypography import com.wafflestudio.snutt2.views.LocalReviewWebView import com.wafflestudio.snutt2.views.launchSuspendApi import com.wafflestudio.snutt2.views.logged_in.home.reviews.ReviewWebView -import com.wafflestudio.snutt2.views.logged_in.home.syllabus.SyllabusWebView import kotlinx.coroutines.launch suspend fun openReviewBottomSheet( @@ -30,18 +28,6 @@ suspend fun openReviewBottomSheet( bottomSheet.show() } -suspend fun openSyllabusBottomSheet( - url: String?, - syllabusWebViewContainer: WebViewContainer, - bottomSheet: BottomSheet, -) { - syllabusWebViewContainer.openPage(url) - bottomSheet.setSheetContent { - SyllabusWebView(syllabusWebViewContainer) - } - bottomSheet.show() -} - fun checkLectureOverlap( composableStates: ComposableStates, onLectureOverlap: (String) -> Unit, diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/syllabus/SyllabusWebView.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/syllabus/SyllabusWebView.kt deleted file mode 100644 index 90e6d2b93..000000000 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/home/syllabus/SyllabusWebView.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.wafflestudio.snutt2.views.logged_in.home.syllabus - -import android.view.ViewGroup -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import com.wafflestudio.snutt2.lib.android.webview.WebViewContainer - -@Composable -fun SyllabusWebView(syllabusWebViewContainer: WebViewContainer) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.95f), - ) { - AndroidView( - factory = { - syllabusWebViewContainer.webView.apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - } - }, - ) - } -} diff --git a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/lecture_detail/LectureDetailPage.kt b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/lecture_detail/LectureDetailPage.kt index 1ddab9b11..534a5c0d3 100644 --- a/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/lecture_detail/LectureDetailPage.kt +++ b/app/src/main/java/com/wafflestudio/snutt2/views/logged_in/lecture_detail/LectureDetailPage.kt @@ -1,5 +1,7 @@ package com.wafflestudio.snutt2.views.logged_in.lecture_detail +import android.content.Intent +import android.net.Uri import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState @@ -41,7 +43,6 @@ import com.wafflestudio.snutt2.components.compose.* import com.wafflestudio.snutt2.components.compose.embed_map.FoldableEmbedMap import com.wafflestudio.snutt2.lib.android.webview.CloseBridge import com.wafflestudio.snutt2.lib.android.webview.ReviewWebViewContainer -import com.wafflestudio.snutt2.lib.android.webview.SyllabusWebViewContainer import com.wafflestudio.snutt2.lib.data.SNUTTStringUtils import com.wafflestudio.snutt2.lib.data.SNUTTStringUtils.creditStringToLong import com.wafflestudio.snutt2.lib.data.SNUTTStringUtils.getFullQuota @@ -152,10 +153,6 @@ fun LectureDetailPage( this.webView.addJavascriptInterface(CloseBridge(onClose = { scope.launch { bottomSheet.hide() } }), "Snutt") } } - val syllabusBottomSheetWebViewContainer = - remember { - SyllabusWebViewContainer(context) - } ModalBottomSheetLayout( sheetContent = bottomSheet.content, @@ -606,11 +603,10 @@ fun LectureDetailPage( LectureDetailButton(title = stringResource(R.string.lecture_detail_syllabus_button)) { scope.launch { launchSuspendApi(apiOnProgress, apiOnError) { - openSyllabusBottomSheet( - vm.getCourseBookUrl(), - syllabusBottomSheetWebViewContainer, - bottomSheet, - ) + vm.getCourseBookUrl().let { url -> + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + context.startActivity(intent) + } } } } From 7a10b90dfe41da277eb3f257ab4807f72173e511 Mon Sep 17 00:00:00 2001 From: plgafhd Date: Wed, 22 Jan 2025 15:09:46 +0900 Subject: [PATCH 7/8] release snutt 3.8.5-rc.1 --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 27261fd12..10d5897ad 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -snuttVersionName=3.8.3 +snuttVersionName=3.8.5-rc.1 From ca4d08d7966a6bfa77ff859f06255a40ce6b881c Mon Sep 17 00:00:00 2001 From: plgafhd Date: Wed, 22 Jan 2025 15:27:05 +0900 Subject: [PATCH 8/8] release snutt 3.8.5 --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 10d5897ad..033984934 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -snuttVersionName=3.8.5-rc.1 +snuttVersionName=3.8.5